Amazon DynamoDBを利用して、LangChain.jsでの会話履歴を保存する
AlexaやLex / LINEなどのチャットボットは、ステートレスに作られており、過去の入力と返答は保存する必要があります。この記事では、Amazon DynamoDBを会話履歴DBとして使用する方法について調査しました。会話のセッションIDをキーとして、DynamoDBのテーブルを作成し、LangChain.jsを使用してDynamoDBを呼び出す方法を紹介しています。また、実際に会話データがDynamoDBに保存される構造と、セッションを復元する方法も説明されています。ただし、データの維持には注意が必要であり、Momentoなどのデータストアを検討することも推奨されています。
目次
AlexaやLex / LINEなどのチャットbotもそうですが、この手の対話式インターフェイスは原則としてステートレスに作られています。そのため「過去にどんな入力があって、どんな返答を返したか」はDynamoDBやCloudflare Workers KV / MomentoなどのDBまたはストレージに保存する必要があります。
今回はAmazon DynamoDBを会話履歴DBとして利用する方法を調べました。
DynamoDBのテーブルを作る
まずはテーブルを用意します。会話のセッションIDをキーにGET / PUTを行うイメージですので、設定自体はかなりシンプルにしました。
LangChain.jsからDynamoDBをよび出す
LangChainがDB呼び出しなども抽象化してくれます。が、内部的にAWS SDKが必要ですので、インストールしておきましょう。
npm i @aws-sdk/client-dynamodb
会話履歴の保存は、LangChainではmemory
とよびます。セッションIDを今回は暫定でDate
から生成していますが、なにかしらキーにできるID(ユーザーのSIDなど)があればそちらを使う方が良さそうです。
import { BufferMemory } from "langchain/memory";
import { DynamoDBChatMessageHistory } from "langchain/stores/message/dynamodb";
const memory = new BufferMemory({
chatHistory: new DynamoDBChatMessageHistory({
tableName: "langchain",
partitionKey: "id",
sessionId: new Date().toISOString(), // Or some other unique identifier for the conversation
config: {
region: "us-east-2",
credentials: {
accessKeyId: "<your AWS access key id>",
secretAccessKey: "<your AWS secret access key>",
},
},
}),
});
あとはChainにmemory
としてつないでやるだけです。
const model = new BedrockChat({
model: "anthropic.claude-v2",
region: env.aws.region,
credentials: {
accessKeyId: env.aws.credentials.accessKeyId,
secretAccessKey: env.aws.credentials.secretAccessKey,
},
});
const conversationChain = new ConversationChain({ llm: model, memory });
const response = await conversationChain.invoke({
input: "Stripeでのサブスクリプションの作り方"
});
どのKVS / DBを利用するかを抽象化できているので、あとから変更するのもかなり簡単そうです。
どのようにデータが保存されるか
先ほどのコードを動かして、どのようにデータがDynamoDBに送られるか見てみました。
messages
にList形式で履歴を入れている様子です。もし外から使う必要が出た場合や、Streamで分析・Fine Tuningのデータソースに使ったりしたい場合は、この構造を覚えておくとよいでしょう。
セッションを復元させてみる
前回の会話内容を元にやりとりができるようにしてみましょう。今回はsessionId
をDynamoDBに保存されているアイテムのIDに直接変更して実現させます。
const memory = new BufferMemory({
chatHistory: new DynamoDBChatMessageHistory({
tableName: env.aws.dynamodb.tableName,
partitionKey: env.aws.dynamodb.partitionKey,
sessionId: "2023-12-24T14:48:21.943Z", // Or some other unique identifier for the conversation
config: {
region: env.aws.region,
credentials: {
accessKeyId: env.aws.credentials.accessKeyId,
secretAccessKey: env.aws.credentials.secretAccessKey,
},
},
}),
});
せっかくなので、コンテキストが必要そうな質問文に変えておきましょう。
const conversationChain = new ConversationChain({ llm: model, memory });
const response = await conversationChain.invoke({
input: "JavaScriptで教えてください"
});
console.log(response);
実行結果がこちらです。「Stripe」と明言していないにも関わらず、Stripeに関する回答が出てきました。
{
"response": " はい、JavaScriptを使ってStripeでサブスクリプションを実装する方法について説明します。\n\nまずStripeアカウントを取得し、公開"
}
DynamoDB側にも履歴が追加されています。
余談
Alexaの時の経験上、この手のデータストアはExpireさせる仕組みが必要です。仕組みを設けない限り、セッションの数だけデータが増えていくため、2〜3年後くらいに無料枠を使い切ることがあります。そしてその頃になると、どのデータを消すべきかの判断が難しくなるので、課金するかアプリごと消すかの判断を迫られます。
「揮発して欲しいデータ」であることを踏まえると、LangChain.jsでMemoryを使うならばMomentoも検討するのがよいかもしれません。ただしこちらはこちらでキャッシュの上限が24時間なので、それを超える期間で保存したい場合には、TTL相当の機能を持つデータストアを探すか、機構を作るかになりそうです。
参考
https://js.langchain.com/docs/integrations/chat_memory/dynamodb