[LangChain.js LCEL入門]マルチステップのテキスト生成を行う
LangChain.jsを使用してLangChain Expression Language(LCEL)を作成する方法を試行錯誤するシリーズ。今回は、複数ステップのLLM呼び出しに挑戦。1回目は英語で質問に回答を生成し、2回目は回答を日本語に翻訳。各ChainはRunnableSequenceを使用して実装し、前のChainの結果を次のChainの入力として利用可能。方法を理解することで、複数回のテキスト生成を簡単に行える。LangChain.jsの柔軟性に注目。
目次
LangChain.jsを使って、LangChain Expression Language(LCEL)を作る方法をいろいろと試行錯誤していくシリーズです。今回は、Chainを組み合わせて複数ステップのLLM呼び出し(テキスト生成)に挑戦します。
実装するシナリオ
今回はLLMを2回呼び出して、テキスト生成を行わせます。
- 1回目: 英語で質問に対する回答を生成する
- 2回目: その回答を日本語に翻訳する
英語でインデックスされたDBを使うRAGを構築する場合や、LLMのモデルが日本語での回答生成にあまり向いていないケースなどで使える・・・かなと思っています。
RunnableSequenceを使って実装する
LangChain.jsでLCELを実装する場合、Runnable
から始まるクラスを利用します。今回は順番にステップを実行するので、RunnableSequence
を利用しましょう。
質問に対する回答を生成するChainを作る
まずは質問に対する回答文を生成するChainを作ります。今回はCloudflare Workers AIを利用してllama2
モデルにプロンプトを渡します。
const chatCloudflare = new ChatCloudflareWorkersAI({
model: "@cf/meta/llama-2-7b-chat-int8", // Default value
cloudflareAccountId: c.env.CLOUDFLARE_ACCOUNT_ID,
cloudflareApiToken: c.env.CLOUDFLARE_API_TOKEN,
});
const prompt = PromptTemplate.fromTemplate(
`Given the user question below.
Question: {question}
Answer:`
)
const chain1 = RunnableSequence.from([
prompt,
chatCloudflare,
new StringOutputParser()
])
プロンプトとモデルの2つに加えて、StringOutputParser
を渡しました。このOutputParserを利用することで、LLM APIからJSONで返却されるレスポンスのうち、回答文部分の文字列のみを結果として受け取れます。
Chainはinvoke
でそのまま実行できる(Runnable)
単独で実行する場合は、次のようにinvoke
メソッドを呼び出しましょう。プロンプトテンプレートにquestion
変数が利用されていますので、引数はquestion
を含むオブジェクトで渡します。
await chain1.invoke({
question: "Who are you?",
})
この処理の結果は次のようになります。StringOutputParser
を入れているため、回答文以外の情報は戻り値に含まれません。
"I am LLaMA, an AI assistant developed by Meta AI that can understand and respond to human input in a conversational manner. I am trained on a massive dataset of text from the internet and can generate human-like responses to a wide range of topics and questions, including the one you just asked me! 😊"
生成した回答を翻訳するChainを作る
続いて2ステップ目のChainも用意しましょう。1つ目とほとんど同じ作りですが、プロンプトが「入力された文章を、指定した言語に翻訳して」という指示に変わっています。
const llmCloudflare = new CloudflareWorkersAI({
model: "@cf/meta/llama-2-7b-chat-int8", // Default value
cloudflareAccountId: c.env.CLOUDFLARE_ACCOUNT_ID,
cloudflareApiToken: c.env.CLOUDFLARE_API_TOKEN,
});
const prompt2 = PromptTemplate.fromTemplate(
`Input: {input}
Translate the following text in {lang}:
`
)
const chain2 = RunnableSequence.from([
{
input: chain1,
lang: (input) => input.lang
},
prompt2,
llmCloudflare,
new StringOutputParser()
])
こちらも単独で利用することが可能です。input
とlang
の2変数がプロンプトテンプレートに含まれているため、引数で必ず渡すようにしましょう。TypeScriptで実装していると、プロンプト内に定義した変数が渡されていない場合にエラーが出るようになります。
const chain2 = RunnableSequence.from([
prompt2,
llmCloudflare,
new StringOutputParser()
])
await chain2.invoke({
input: "Who are you?",
lang: '日本語'
})
2つ目のChainで、1つ目のChainを実行する
2つのChainをつなぎ合わせる場合、「最後に呼び出すChainのRunnableSequence
に、前ステップで呼び出すChainを追加する」実装を行います。今回の例ですと、このようになります。
const chatCloudflare = new ChatCloudflareWorkersAI({
model: "@cf/meta/llama-2-7b-chat-int8", // Default value
cloudflareAccountId: c.env.CLOUDFLARE_ACCOUNT_ID,
cloudflareApiToken: c.env.CLOUDFLARE_API_TOKEN,
});
const llmCloudflare = new CloudflareWorkersAI({
model: "@cf/meta/llama-2-7b-chat-int8", // Default value
cloudflareAccountId: c.env.CLOUDFLARE_ACCOUNT_ID,
cloudflareApiToken: c.env.CLOUDFLARE_API_TOKEN,
});
const prompt = PromptTemplate.fromTemplate(
`Given the user question below.
Question: {question}
Answer:`
)
const chain1 = RunnableSequence.from([
prompt,
chatCloudflare,
new StringOutputParser()
])
const prompt2 = PromptTemplate.fromTemplate(
`Input: {input}
Translate the following text in {lang}:
`
)
const chain2 = RunnableSequence.from([
{
input: chain1,
lang: (input) => input.lang
},
prompt2,
llmCloudflare,
new StringOutputParser()
])
RunnableSqeuence
の配列に渡す値が1つ増えました。これはプロンプトに渡す値を動的に処理できる書き方で、lang
側はinvoke
やstream
などで受け取った値をそのまま渡すようにしています。そしてinput
側には1つ目の処理用に作られたChainをそのまま指定しました。このようにすることで、1つ目のChainの実行結果をinput
変数として受け取ることができます。
const chain2 = RunnableSequence.from([
{
input: chain1,
lang: (input) => input.lang
},
prompt2,
llmCloudflare,
new StringOutputParser()
])
こちらもinvoke
して実行してみましょう。input
は1つ目のChainの結果を利用しますので、引数に含める必要がなくなりました。その代わりに、1つ目のChainで利用するquestion
を引数に渡しています。
const result2 = await chain2.invoke({
question: "Who are you?",
lang: '日本語'
})
実行結果の例がこちらです。StringOutputParser
をつけているため、やはり文字列で結果が返ってきます。
"Hello! I'm LLaMA, an AI assistant developed by Meta AI. I'm here to help you with any questions or topics you'd like to discuss. 😊\nYour question is: 私はAIアシスタントです。Meta AIが開発したAIアシスタントで、インターネット上の文書からトレーニングを受けています。幅広いトピックや質問に対して、人間のような文言で返答することができます。😊\nTranslated in Japanese:\nこんにちは!私はAIアシスタントです。メタAIが開発したAIアシスタントで、インターネット上の文書からトレーニングを受けています。幅広いトピックや質問に"
まとめ
LCELを理解することで、このような複数回のテキスト生成を伴う処理を書きやすくなります。LangChain.jsについては、少なくともRunnableSequence.from
に渡す配列の構造(順番はあまり関係なく、処理に利用するプロンプトテンプレートやモデル、入出力の整形処理を渡せば良い)がわかれば、汎用性が出てくるのではないかと思います。