白猫のメモ帳

C#とかJavaとかJavaScriptとかHTMLとか機械学習とか。

TypeScriptでLangChainを使ってみる その1 基礎編

こんばんは。
リボンライトの接触がおかしくなって部屋の片隅が妖しく赤く光っています。私です。

今回から何回かに渡ってLangChainを触っていこうかと思います。
LangChainはPython版とJavaScript(TypeScript)版があるのですが、Pythonの方が開発が先行しているためJS版は影が薄めです。
検索してもPythonのコードばかりがヒットしてとてもつらい。ので、自分で少しでも書こうかなと。

今回から触るコードはこのリポジトリで公開しています。
github.com

LangChainとは

LangChainはざっくりいうと大規模言語モデル(LLM)を使ったアプリケーションを簡単に作れるようにしたフレームワークです。
更新もとても活発で、迷ったらこれでいいとは思います。
www.langchain.com

ちなみにLangChain以外にもいくつかフレームワークがありますが、LlamaIndexとSemantic Kernelあたりが有名かなと思います。
www.llamaindex.ai
learn.microsoft.com

とりあえず動かしてみる(Model)

インストールするパッケージは詳しくはGitHubリポジトリを参照してください。
環境変数の利用のために「dotenv」、LangChain自体は「langchain」「@langchain/community」「@langchain/openai」「@langchain/google-genai」とかそのあたりです。
OpenAIやGeminiのAPIキーなどは.envファイルに記載して参照できるようにしておきましょう。

OPENAI_API_KEY="ほにゃらら"
GOOGLE_API_KEY="ふにゃらら"

まずはこんな感じで書くととりあえず簡単にチャットができます。モデルはたぶん「gpt-3.5-turbo」ですかね。

const chat = new ChatOpenAI({
    openAIApiKey: process.env.OPENAI_API_KEY
});

const result = await chat.invoke("こんばんは");
console.log(result);

結果はこんな感じ。

AIMessage {
  lc_kwargs: {
    content: 'こんばんは!今日はどのようなお話しをしましょうか?',
    additional_kwargs: { function_call: undefined, tool_calls: undefined }
  },
  lc_namespace: [ 'langchain_core', 'messages' ],
  content: 'こんばんは!今日はどのようなお話しをしましょうか?',
  name: undefined,
  additional_kwargs: { function_call: undefined, tool_calls: undefined }
}

実は環境変数の名前を「OPENAI_API_KEY」にしていると特にコンストラクタで何も渡さなくても勝手にそれを使ってくれます。
なので、これでも同じ結果になります。

const chat = new ChatOpenAI();

const result = await chat.invoke("こんばんは");
console.log(result);

で、便利なのがたったこれだけでGoogle Generative AIへのリクエストができてしまいます。たぶんモデルは「gemini-pro」。
いろいろ余計なものがついてるので、result.contentに変えてみました。

const chat = new ChatGoogleGenerativeAI();

const result = await chat.invoke("こんばんは");
console.log(result.content);

Geminiの方が詩的…なんですかね。

こんばんは。あなたの夜をより良くするために何かお手伝いできることはありますか?私はあなたとチャットしたり、ジョークを言ったり、質問に答えたりすることができます。

プロンプトを作ってみる(Prompt)

LLMに渡すプロンプトは固定の文字列ではなく、ユーザーの入力など何らかの変数を埋め込むことが良くあります。
そんな場合にはPromptTemplateの機能を使います。

const promptTemplate1 = new PromptTemplate({
    template: "今日は{day_of_week}、明日は?",
    inputVariables: [ "day_of_week" ]   // 変数の定義を書く
});
const prompt1 = await promptTemplate1.format({ day_of_week: "金曜日" });
console.log(prompt1);

// fromTemplateを使うと変数は勝手にいい感じにしてくれる
const promptTemplate2 = PromptTemplate.fromTemplate("今日は{day_of_week}、明日は?");
const prompt2 = await promptTemplate2.format({ day_of_week: "金曜日" });
console.log(prompt2);

チャットによる会話を作る場合にはChatPromptTemplateを使うと良いです。
ロールごとのPromptTemplateもあります。

const chatTemplate1 = ChatPromptTemplate.fromMessages([
    [ "system", "あなたは{role}です" ],
    [ "human", "好きな{kinds}は何ですか?" ]
]);
const prompt1 = await chatTemplate1.format({ role: "賢い猫", kinds: "食べ物" });
console.log(prompt1);

const chatTemplate2 = ChatPromptTemplate.fromMessages([
    SystemMessagePromptTemplate.fromTemplate("あなたは{role}です"),
    HumanMessagePromptTemplate.fromTemplate("好きな{kinds}は何ですか?")
]);
const prompt2 = await chatTemplate2.format({ role: "賢い猫", kinds: "食べ物" });
console.log(prompt2);

出力を制御してみる(OutputParser)

result.contentに変えないと邪魔なものがみたいなことを書きましたが、StringOutputParserを使うと結果の文字列だけを取り出すことができます。

const message = new AIMessage({
    content: 'こんばんは!なんかご用ですか?'
});

const outputParser = new StringOutputParser();
const result = await outputParser.invoke(message);
console.log(result);

CommaSeparatedListOutputParserを使うとカンマ区切りの文字列を配列で返してくれます。

const message = new AIMessage({
    content: '春,夏,秋,冬'
});

const outputParser = new CommaSeparatedListOutputParser();
const result = await outputParser.invoke(message);
console.log(result);

つなげてみよう(Chain)

ここまで作ったものをつなげてみます。

const chatTemplate = ChatPromptTemplate.fromMessages([
    SystemMessagePromptTemplate.fromTemplate("あなたは{role}です"),
    HumanMessagePromptTemplate.fromTemplate("好きな{kinds}は何ですか?")
]);
const prompt = await chatTemplate.format({ role: "賢い猫", kinds: "食べ物" });

const chat = new ChatGoogleGenerativeAI();
const chatResult = await chat.invoke(prompt);

const outputParser = new StringOutputParser();
const result = await outputParser.invoke(chatResult);

console.log(result);

いや…賢いけどちょっとイメージと違う…。

* **魚:** マグロ、サケ、タラ、ティラピア、イワシなど、ほとんどの猫は魚が大好きです。魚は猫にとって優れたタンパク質、オメガ3脂肪酸、その他の栄養素の供給源です。
* **鶏肉:** 鶏肉は、猫にとってもう一つの人気のあるタンパク質源です。鶏肉は調理しているか、生であり、味付けや調味料は含まれていないことを確認してください。
* **牛肉:** 牛肉は、猫が食べることができるもう一つのタンパク質源です。牛肉は調理しているか、生であり、味付けや調味料は含まれていないことを確認してください。
* **ラム肉:** ラム肉は、猫に人気のあるもう一つのタンパク質源です。ラム肉は調理しているか、生であり、味付けや調味料は含まれていないことを確認してください。
* **臓器肉:** 肝臓、心臓、腎臓などの臓器肉は、猫にとって優れたビタミン、ミネラル、その他の栄養素の供給源です。しかし、臓器肉は少量で与えてください。
* **卵:** 卵は、猫にとって優れたタンパク質、ビタミン、ミネラルの供給源です。卵は調理されているか、生であり、味付けや調味料は含まれていないことを確認してください。
* **乳製品:** 猫は一部の乳製品を食べることができますが、多くは乳糖不耐症です。猫に乳製品を与える場合は、牛乳よりもヨーグルトやケフィアなどの発酵乳製品を選ぶようにしましょう。
* **野菜:** 猫は一部の野菜を食べることができますが、ほとんどの猫は肉食動物です。猫に野菜を与える場合は、少量で、生よりも調理しているものを選ぶようにしましょう。
* **果物:** 猫は一部の果物を食べることができますが、ほとんどの猫は肉食動物です。猫に果物を与える場合は、少量で、砂糖が少ないものを選ぶようにしましょう。

実はpipe関数を使ってこんな風に書けます。まさにChainですね。

const chatTemplate = ChatPromptTemplate.fromMessages([
    SystemMessagePromptTemplate.fromTemplate("あなたは{role}です"),
    HumanMessagePromptTemplate.fromTemplate("好きな{kinds}は何ですか?")
]);
const chat = new ChatGoogleGenerativeAI();
const outputParser = new StringOutputParser();

const result = await chatTemplate.pipe(chat).pipe(outputParser).invoke({ role: "賢い猫", kinds: "食べ物" });

console.log(result);

で、そういうのってよく使うでしょってことで、LLMChainという名前であらかじめ用意されたりしています。
他にもいろいろなChainがあります。

const chatTemplate = ChatPromptTemplate.fromMessages([
    SystemMessagePromptTemplate.fromTemplate("あなたは{role}です"),
    HumanMessagePromptTemplate.fromTemplate("好きな{kinds}は何ですか?")
]);

const chat = new ChatGoogleGenerativeAI();
const outputParser = new StringOutputParser();

const chain = new LLMChain({
    prompt: chatTemplate,
    llm: chat,
    outputParser: outputParser
});

const result = await chain.invoke({ role: "賢い猫", kinds: "食べ物" });
console.log(result);

スマートだ

とりあえず今回は基本編ということでここまで。
ライブラリとしての出来が良くて参考になります。
LLMを使ったプログラムが簡単に書けるのは素敵ですね。

それでは。