fbpx

LangChainでLLMを活用したアプリを開発しよう! ~その①~

この記事は1年以上前に投稿されました。情報が古い可能性がありますので、ご注意ください。

OpenAIのChatGPTにより急速にLLMを活用が進んでいます。LLMを用いたアプリケーションにより様々な問題が解決されています。例えば書類の作成やコーディングの補助、感情分析やテキストの分類・要約・翻訳、運用の自動化や、賢いBotなど多岐にわたります。ChatGPTのWebサービスを用いたことがある方は当然ご存知だとは思いますが、それ単体でも汎用的で非常に強力です。しかし、単体では直接外部のツールを呼び出すことはできません。こういったことをするには、外部のツールとLLMの橋渡しをする何かを作る必要があります。

またLLMには入力トークン数に制限があったり、LLMが知らない社内の非公開情報はもちろん回答できません。汎用的であるとはいえ、今はまだ苦手なことや制限もあります。そのため、LLMを用いて実現したいことによっては、その苦手な部分を上手に補う必要があります。ここでは、LLMを上手に扱うためのライブラリであるLangChainをご紹介します。

LangChainとは?

LangChainはLLMを利用したアプリケーションを開発するためのフレームワークです。LLMと外部のデータソースを連携させたり、LLMの出力結果をさらにLLM考えさせたりなどLLMを用いた少し複雑なアプリケーションを比較的簡単に実装できるようになっています。LangChainにはPythonとJavaScript/TypeScript向けの実装があります。今回はおそらくLangChainを試す上で一番手軽であろうDenoを用いてJavaScript/TypeScript版を説明していきます。この投稿ではやりたいことをベースにLangChainの使い方を紹介していきます。細かい使い方・概念等については公式ドキュメントをご参照ください。またLLMはOpenAIのモデルを利用します。

Denoのセットアップ

公式ドキュメントの通りにセットアップしてください。例えばMacの場合はbrewを用いて以下のコマンドでインストールできます。

$ brew install deno

LLMを利用する

事前にOpenAIのAPIキーを準備してください。利用に応じて課金されますのでご留意ください。
用意したAPIキーは環境変数 OPENAI_API_KEY へセットします。
今回利用するLangChainのバージョンは0.0.125です。以下のコードをllm.tsというファイル名で保存して実行します。

import { OpenAI } from "npm:langchain@0.0.125/llms/openai";

const llm = new OpenAI();
const res = await llm.call("卵焼きのレシピを教えてください");
console.log(res);
# ネットワークへのアクセスと環境変数へのアクセスが必要なため、それらを許可します。
$ deno run --allow-env --allow-net llm.ts


1. 卵2個、小麦粉大さじ1、お好みで小麦粉を少し

2. 卵を卵切り器で割り、小麦粉を卵の中に混ぜる

3. フライパンに油を入れ、中火にして温める

4. 卵を入れて、お好みの程度まで火を通す

5. フライパンを反対にして、パンをゆっくり焼く

6. パンが焦げないようにお好みの程度まで焼き色をつける

7. 焼き上がったら、皿に取り出して完成!

卵焼きではなくパンを作っていますが、実行できました :tada:
上記の new OpenAI() はデフォルトでInstructGPTモデルである text-davinci-003 を利用します。LangChainを利用すると、chat用に最適化されたモデルである gpt-3.5-turbo-0301 もOpenAIコンストラクタの引数で指定することで簡単に利用できます。

import { OpenAI } from "npm:langchain@0.0.125/llms/openai";

const llm = new OpenAI({modelName: "gpt-3.5-turbo"});
const res = await llm.call("卵焼きのレシピを教えてください");
console.log(res);
# ネットワークへのアクセスと環境変数へのアクセスが必要なため、それらを許可します。
$ deno run --allow-env --allow-net llm.ts
卵焼きのレシピです。

材料:
- 卵 4個
- しょうゆ 小さじ1
- 醤油 大さじ1
- 砂糖 小さじ1/2
- 顆粒だし 小さじ1/2
- 油 適量

作り方:
1. ボウルに卵を割り入れ、よく混ぜます。
2. しょうゆ、砂糖、顆粒だしを加え、よく混ぜます。
3.フライパンを熱し、油を薄く引きます。
4.卵液をフライパンに注ぎ入れ、広げます。
5.半熟になるまで焼き、端から巻いていきます。
6.巻き終わったら、巻き終わりの部分をフライパンの反対側に寄せて焼きます。
7.全体的にしっかりと焼き色がつくまで焼きます。
8.卵焼きを取り出して、一度冷まし、食べやすい大きさに切り分けます。

お好みでネギや焼き海苔をトッピングすることもできます。完成です!お召し上がりください。

またChatに特化したモデルとそうでないモデルも同じように利用できるのは、全てのモデルが共通のインターフェースのための BaseLanguageModel を実装しているからです。OpenAIのモデルに限らず、様々なモデルが利用できます。詳細は公式ドキュメントのLLMsをご参照ください。

Chatに特化したモデルとやり取りするためのインターフェースもLangChainでは用意されています。先ほどはプロンプトのテキストを入力としてLLMをcallしましたが、Chatモデル用のインターフェースではプレーンテキストではなくMessageを渡します。Messageにはいくつかのroleがあり、LangChainでは事前にHumanMessage, AIMessage(OpenAIのassistant role相当のもの), SystemMessageなど予めroleが指定されているものと、任意のroleが指定できるChatMessageが用意されています。
実際に動かしてみましょう。以下のコードをchat.tsというファイル名で保存し実行します。

import { ChatOpenAI } from "npm:langchain@0.0.125/chat_models/openai";
import { HumanMessage } from "npm:langchain@0.0.125/schema";

const chat = new ChatOpenAI();
const res = await chat.call([
  new HumanMessage("親子丼の作り方を教えてください。"),
]);
console.log(res);
$ deno run --allow-env --allow-net chat.ts
AIMessage {
  lc_serializable: true,
  lc_kwargs: {
    content: "親子丼は日本料理の一つで、鶏肉と卵を使った人気のある丼物です。以下に、親子丼の基本的な作り方をご紹介します。\n" +
      "\n" +
      "【材料】(2人分)\n" +
      "- 鶏もも肉 2枚\n" +
      "- 卵 4個\n" +
      "- ごはん 2膳\n" +
      "- みりん 大さ"... 398 more characters,
    additional_kwargs: { function_call: undefined }
  },
  ...(省略)
}

上記のようにLLMからの返答がAIMessageとして返されていることがわかります。

PromptTemplate

LLMを利用するアプリケーションでは、プロンプトの一部を変更できるようにしたいことはあるでしょう。例えば先ほどの 卵焼きのレシピを教えてください というプロンプトの料理名を変更可能にしたい場合、JavaScript/TypeScriptでは以下のような文字列中の展開を利用することで、プロンプトをテンプレート化できます。

import { OpenAI } from "npm:langchain@0.0.125/llms/openai";

const food = "親子丼";
const llm = new OpenAI({modelName: "gpt-3.5-turbo"});
const res = await llm.call(`${food}のレシピを教えてください`);
console.log(res);
$ deno run --allow-env --allow-net main.ts
親子丼のレシピをご説明いたします。以下の材料と手順に従って作ってください。

【材料】
- 鶏もも肉: 200g
- たまご: 3個
- ごはん: 2合(約360g)
- 醤油: 大さじ3
- みりん: 大さじ2
- 砂糖: 大さじ1
- だし汁: 100ml
- ねぎ(みじん切り): 適量

【手順】
1. 鶏もも肉を一口大に切り、醤油、みりん、砂糖、だし汁を合わせたタレにつけ込んで10分ほどおく。
2. 鍋かフライパンにタレを入れ、鶏もも肉も一緒に入れて弱火で煮込む。鶏もも肉が火を通してタレがとろみがつくまで煮る。
3. 別の鍋でご飯を炊くか、レンジで温めておく。
4. たまごをボウルに割り、よく溶く。
5. 鶏もも肉が十分に火を通ったら、たまごを加えて全体をかき混ぜながら火を通す。タレが半熟状になるまで火を加え続ける。
6. ご飯を丼に盛り、上に鶏もも肉とたまごをのせる。
7. ねぎをみじん切りにし、親子丼の上にトッピングする。

これで親子丼の完成です。お好みで七味や天かすを加えると風味が増します。お召し上がりください。

しかし、LangChainではプロンプトをテンプレート化して扱うための専用のクラスが存在しています。これはLangChainではLLMやプロンプトテンプレートなどをコンポーネントとして扱い、それらの組み合わせでアプリケーションを開発できるような仕組みがあることも理由となっています。それがPromptTemplateという機能です。以下はそのサンプルコードです。

import { PromptTemplate } from "langchain/prompts";

const template = PromptTemplate.fromTemplate(`{food}のレシピを教えてください`);
const prompt = await template.format({
  food: "親子丼",
});

console.log(prompt);

上記をprompt.tsというファイル名で保存し実行します。

$ deno run prompt.ts
親子丼のレシピを教えてください

PromptTemplateオブジェクトはPromptTemplate.fromTemplateメソッドで生成すると簡単です。変数となる部分は{}で表現します。生成されたオブジェクトはformatメソッドで実際のテンプレートを生成します。このformatメソッドの引数には、変数名とそれに対する値を渡します。このformatメソッドを呼び出すとき、テンプレートで必要とされている変数の値がセットされていない場合、エラーとして検知可能です。

import { PromptTemplate } from "npm:langchain@0.0.125/prompts";

const template = PromptTemplate.fromTemplate(`{food}のレシピを教えてください`);
const prompt = await template.format({
  drink: "親子丼",
});

console.log(prompt);
$ deno run prompt.ts
error: Uncaught Error: Missing value for input food
...(以下略)

Chatモデルのための専用のインターフェースでも同様のテンプレート機能があります。それぞれのMessage毎にテンプレートを生成する関数が用意されており、以下のように利用することができます。

import { HumanMessagePromptTemplate } from "npm:langchain@0.0.125/prompts";

const template = HumanMessagePromptTemplate.fromTemplate(`{food}のレシピを教えてください`);
const prompt = await template.format({
  food: "親子丼",
});

console.log(prompt);

上記をprompt_chat.tsというファイル名で保存し実行します。

$ deno run prompt_chat.ts
HumanMessage {
  lc_serializable: true,
  lc_kwargs: { content: "親子丼のレシピを教えてください", additional_kwargs: {} },
  lc_namespace: [ "langchain", "schema" ],
  content: "親子丼のレシピを教えてください",
  name: undefined,
  additional_kwargs: {}
}

Chain

LangChainではLLMやテンプレートなどのコンポーネントを組み立てることで、LLMを利用する複雑なアプリケーションを構築することができます。それがChainとよばれるインターフェースです。Chainにより複数のコンポーネントを単一の首尾一貫した形でアプリケーションを構築できます。例えば、ユーザの入力を受け取り、それをPromptTemplateでフォーマットし、フォーマットされたレスポンスをLLMに渡すようなChainを作ることができます。複数のChainを組み合わせることで、より複雑なアプリケーションも構築可能です。Chain自体を自分で定義することもできますが、LangChainで既に定義されているいくつかのChainを試していきます。最初はLLMChainという最も基本的なChainです。これはLLMとプロンプトテンプレートをもとにChainオブジェクトを作成します。

import { OpenAI } from "npm:langchain@0.0.125/llms/openai";
import { PromptTemplate } from "npm:langchain@0.0.125/prompts";
import { LLMChain } from "npm:langchain@0.0.125/chains";

const model = new OpenAI({ modelName: "gpt-3.5-turbo" });
const prompt = PromptTemplate.fromTemplate(
  "{food}のレシピを教えてください",
);

const chain = new LLMChain({ llm: model, prompt });
const res = await chain.call({ food: "ハンバーグ" });
console.log(res);
$ deno run --allow-net --allow-env chain.ts
{
  text: "ハンバーグの基本的なレシピをご紹介いたします。\n" +
    "\n" +
    "【材料】(2人分)\n" +
    "- 牛ひき肉:300g\n" +
    "- 玉ねぎ:1個(中サイズ)\n" +
    "- パン粉:大さじ3\n" +
    "- 牛乳:大さじ3\n" +
    "- ケチャップ:大さじ1\n" +
    "- ウス"... 536 more characters
}

LLMChainは、callやrunメソッドへPromptTemplateの引数を渡すことで実行できます。LLMChainによりPromptTemplateとLLMの繋がりを知ることなく、PromptTemplateをfomratした値がLLMの呼び出しに利用されました。これは単純なChainの例ですので、他のChainも確認します。

次はConversationChainというChainです。ConversationChainの説明をする上で大事な知識について軽く紹介します。OpenAIのAPIに触れたことがある方はご存じかもしれませんが、Chatインターフェースを持つLLMのAPI呼び出しはStatelessとなっています。つまりLLM側には過去の会話履歴が保存されていません。では、どうやって以前の会話の内容も踏まえた上で回答してもらうかというと、APIのリクエストを投げるときに過去の会話も含めてリクエストします。これによりLLM側が過去の会話の内容も踏まえた回答を返してくれるという仕組みとなっています。つまり、過去の会話も考慮した回答が必要なアプリケーションを作りたい場合は、何かに記録しておく必要があるということです。この記録の仕組みにLangChainではMemoryというコンポーネントを用意してくれています。
ここまで説明すれば予想できると思いますが、ConversationChainは会話の内容をMemoryしてLLMへリクエストをいい感じにリクエストを投げてくれるChainです。以下はその例です。
数字をLLMに数えてもらいます。最初のリクエストではもちろん1と返してくれ、その会話を踏まえた上で、続きを次の数字をLLMに確認すると、きちんと2であることを回答してくれます。

import { ChatOpenAI } from "npm:langchain@0.0.125/chat_models/openai";
import { ConversationChain } from "npm:langchain@0.0.125/chains";
import { BufferMemory } from "npm:langchain@0.0.125/memory";

const chat = new ChatOpenAI();
const memory = new BufferMemory();
const chain = new ConversationChain({ llm: chat, memory });

const res1 = await chain.run(
  "1, 2, 3...と順番に数えてください。最初の数字は?",
);
console.log(res1);

const res2 = await chain.run("次の数字は?");
console.log(res2);
$ deno run --allow-net --allow-env conversationchain.ts
最初の数字は1です。
次の数字は2です。

まとめ

今回はLangChainでLLMを直接呼び出したり、プロンプトのテンプレート化、さらにLangChainで用意されたコンポーネントを組み合わせることで、複雑なアプリケーションを構築する仕組みのChainを学びました。ChainによりLangChainで複雑なアプリケーションがシンプルな表現で構築できることが少しは体感できたでしょうか?次回は、より複雑なアプリケーションを作る仕組みを紹介していこうと思います。

新規CTA