Closed7
LangChain DynamoDBChatMessageHistoryを用いてマルチターン会話をやってみる
やりたいこと
- LangChainを用いてマルチターン会話を実現する
- 会話履歴はDynamoDBに保持したいのでDynamoDBChatMessageHistoryを用いる
- LLMの実行にはBedrockを利用する
- アプリケーション全体はLambda上で動かす
- インフラはCDKで管理する
まずはローカルでBedrockとシングルターン会話してみる
依存関係インストール
poetry -C server add langchain
ハンドラ関数
multi_turn_conversation_handler.py
from typing import Any
from langchain_community.chat_models import BedrockChat
from langchain_core.messages import HumanMessage
chat = BedrockChat(
model_id="anthropic.claude-v2:1",
region_name="us-east-1",
model_kwargs={
"temperature": 0,
},
verbose=True,
)
def handler(event: dict[str, Any], context: Any) -> dict[str, Any]:
question = event["question"]
print(f"Question: {question}")
messages = [HumanMessage(content=question)]
response = chat.invoke(messages)
print(f"Response: {response}")
エントリポイント
main.py
from multi_turn_conversation_handler import handler
def main():
handler({"question": "あなたは何ができますか?"}, None)
if __name__ == "__main__":
main()
実行結果
❯ poetry -C server run python server/main.py
Question: あなたは何ができますか?
Response: content=' ご質問ありがとうございます。\n\n私はAnthropicが開発した対話型AIアシスタントClaudeです。\n\n以下のことができます:\n\n- 基本的な会話\n- 簡単な質問への回答\n- ユーザーの要求に基づいた情報の検索\n\n私の能力には限界がありますが、会話を通じてユーザーの役に立つことを目指しています。\nご不明な点があれば遠慮なくお問い合わせください。'
CDKでLambda関数を作成して実行してみる
CDKコード
import { PythonFunction } from "@aws-cdk/aws-lambda-python-alpha";
import { Duration, Stack, StackProps } from "aws-cdk-lib";
import { Effect, PolicyStatement } from "aws-cdk-lib/aws-iam";
import { Architecture, Runtime } from "aws-cdk-lib/aws-lambda";
import { Construct } from "constructs";
export class MainStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
const multiTurnConversationFn = new PythonFunction(
this,
"MultiTurnConversationFn",
{
entry: "../server",
index: "multi_turn_conversation_handler.py",
runtime: Runtime.PYTHON_3_12,
architecture: Architecture.ARM_64,
bundling: {
assetExcludes: [".venv", "__pycache__", "tests", "README.md"],
},
timeout: Duration.minutes(15),
},
);
multiTurnConversationFn.addToRolePolicy(
new PolicyStatement({
effect: Effect.ALLOW,
actions: ["bedrock:InvokeModel"],
resources: ["*"],
}),
);
}
}
デプロイしてマネコンから実行
イベントJSON
{
"question": "あなたは何ができますか?"
}
実行結果ログ
START RequestId: 0d94cf6f-c5bf-4716-87e2-e0abadc43ea8 Version: $LATEST
Question: あなたは何ができますか?
Response: content=' ご質問ありがとうございます。\n\n私はAnthropicが開発した対話型AIアシスタントClaudeです。\n\n以下のことができます:\n\n- 基本的な会話\n- 簡単な質問への回答\n- ユーザーの要求に基づいた情報の検索\n\n私の能力には限界がありますが、会話を通じてユーザーの役に立つことを目指しています。\nご不明な点があれば遠慮なくお問い合わせください。'
END RequestId: 0d94cf6f-c5bf-4716-87e2-e0abadc43ea8
REPORT RequestId: 0d94cf6f-c5bf-4716-87e2-e0abadc43ea8 Duration: 5566.85 ms Billed Duration: 5567 ms Memory Size: 128 MB Max Memory Used: 122 MB
ローカルでインメモリに会話履歴を保持してマルチターン会話をやってみる
ハンドラ関数
multi_turn_conversation_handler.py
import pprint
from typing import Any
from langchain_community.chat_models import BedrockChat
from langchain.memory import ConversationBufferMemory
from langchain.chains import LLMChain
from langchain.prompts import (
ChatPromptTemplate,
MessagesPlaceholder,
SystemMessagePromptTemplate,
HumanMessagePromptTemplate,
)
chat = BedrockChat(
model_id="anthropic.claude-v2:1",
region_name="us-east-1",
model_kwargs={
"temperature": 0,
},
verbose=True,
)
prompt = ChatPromptTemplate(
messages=[
SystemMessagePromptTemplate.from_template(
"You are a nice chatbot having a conversation with a human."
),
# The `variable_name` here is what must align with memory
MessagesPlaceholder(variable_name="chat_history"),
HumanMessagePromptTemplate.from_template("{question}")
]
)
memory = ConversationBufferMemory(memory_key="chat_history",return_messages=True)
conversation = LLMChain(
llm=chat,
prompt=prompt,
memory=memory,
verbose=True,
)
def handler(event: dict[str, Any], context: Any) -> dict[str, Any]:
question = event["question"]
print(f"Question: {question}")
response = conversation.invoke({"question": question})
pprint.pprint(response)
エントリポイント
main.py
from multi_turn_conversation_handler import handler
def main():
handler({"question": "おすすめのお肉料理の候補を3つ挙げてください"}, None)
handler({"question": "1のレシピを教えてください"}, None)
if __name__ == "__main__":
main()
実行結果
poetry -C server run python server/main.py
Question: おすすめのお肉料理の候補を3つ挙げてください
> Entering new LLMChain chain...
Prompt after formatting:
System: You are a nice chatbot having a conversation with a human.
Human: おすすめのお肉料理の候補を3つ挙げてください
> Finished chain.
{'chat_history': [HumanMessage(content='おすすめのお肉料理の候補を3つ挙げてください'),
AIMessage(content=' はい、おすすめのお肉料理の候補を3つ挙げさせていただきます。\n\n1. ステーキ\nジューシーな肉の旨味を味わえる定番のお肉料理です。上質な肉を
使い、火加減がポイントになります。\n\n2. ハンバーグ\n具だくさんのハンバーグは子供から大人まで人気のメニュー。自分好みの具材を入れてオリジナルのハンバーグが作れます。\n\n3. 焼 き鳥\n骨付き鳥肉を使った焼き鳥は、肉の旨味とジューシーさが楽しめます。タレや塩レモンなど自分の好みの味付けがおすすめ。\n\nいかがでし')], 'question': 'おすすめのお肉料理の候補を3つ挙げてください',
'text': ' はい、おすすめのお肉料理の候補を3つ挙げさせていただきます。\n'
'\n'
'1. ステーキ\n'
'ジューシーな肉の旨味を味わえる定番のお肉料理です。上質な肉を使い、火加減がポイントになります。\n'
'\n'
'2. ハンバーグ\n'
'具だくさんのハンバーグは子供から大人まで人気のメニュー。自分好みの具材を入れてオリジナルのハンバーグが作れます。\n'
'\n'
'3. 焼き鳥\n'
'骨付き鳥肉を使った焼き鳥は、肉の旨味とジューシーさが楽しめます。タレや塩レモンなど自分の好みの味付けがおすすめ。\n'
'\n'
'いかがでし'}
Question: 1のレシピを教えてください
> Entering new LLMChain chain...
Prompt after formatting:
System: You are a nice chatbot having a conversation with a human.
Human: おすすめのお肉料理の候補を3つ挙げてください
AI: はい、おすすめのお肉料理の候補を3つ挙げさせていただきます。
1. ステーキ
ジューシーな肉の旨味を味わえる定番のお肉料理です。上質な肉を使い、火加減がポイントになります。
2. ハンバーグ
具だくさんのハンバーグは子供から大人まで人気のメニュー。自分好みの具材を入れてオリジナルのハンバーグが作れます。
3. 焼き鳥
骨付き鳥肉を使った焼き鳥は、肉の旨味とジューシーさが楽しめます。タレや塩レモンなど自分の好みの味付けがおすすめ。
いかがでし
Human: 1のレシピを教えてください
> Finished chain.
{'chat_history': [HumanMessage(content='おすすめのお肉料理の候補を3つ挙げてください'),
AIMessage(content=' はい、おすすめのお肉料理の候補を3つ挙げさせていただきます。\n\n1. ステーキ\nジューシーな肉の旨味を味わえる定番のお肉料理です。上質な肉を
使い、火加減がポイントになります。\n\n2. ハンバーグ\n具だくさんのハンバーグは子供から大人まで人気のメニュー。自分好みの具材を入れてオリジナルのハンバーグが作れます。\n\n3. 焼 き鳥\n骨付き鳥肉を使った焼き鳥は、肉の旨味とジューシーさが楽しめます。タレや塩レモンなど自分の好みの味付けがおすすめ。\n\nいかがでし'), HumanMessage(content='1のレシピを教えてください'),
AIMessage(content=' ステーキのおすすめの調理法として、以下のように提案させていただきます。\n\n材料(2人分)\n・ステーキ用肉(サーロインやリブアイなど) 400g\n・
塩 少々\n・こしょう 適量 \n\n作り方\n1. 肉から余分な脂身をそぎ落とす。\n2. 室温に30分ほど戻しておく。\n3. フライパンを強火で熱し、肉の脂身の部分を下にして置く。\n4. 30秒ほど したら肉を裏返し、塩とこしょうをふる。\n5. 焼き加減を見ながら裏表2~3分ずつ調理する。\n6. 皿に移し、アルミホイルで包んで5~10分程')], 'question': '1のレシピを教えてください',
'text': ' ステーキのおすすめの調理法として、以下のように提案させていただきます。\n'
'\n'
'材料(2人分)\n'
'・ステーキ用肉(サーロインやリブアイなど) 400g\n'
'・塩 少々\n'
'・こしょう 適量 \n'
'\n'
'作り方\n'
'1. 肉から余分な脂身をそぎ落とす。\n'
'2. 室温に30分ほど戻しておく。\n'
'3. フライパンを強火で熱し、肉の脂身の部分を下にして置く。\n'
'4. 30秒ほどしたら肉を裏返し、塩とこしょうをふる。\n'
'5. 焼き加減を見ながら裏表2~3分ずつ調理する。\n'
'6. 皿に移し、アルミホイルで包んで5~10分程'}
参考
DynamoDBChatMessageHistoryを使ってみる
以下を参考に会話履歴を保存してみて、どんなデータが作成されるのかを確認する
CDKでテーブルを作成する
MainStack
に以下を追加
new aws_dynamodb.Table(this, "SessionsTable", {
partitionKey: {
name: "id",
type: aws_dynamodb.AttributeType.STRING,
},
billingMode: aws_dynamodb.BillingMode.PAY_PER_REQUEST,
});
DynamoDBChatMessageHistoryを使ってメッセージを保存する
依存関係をインストール
poetry add langchain-community -C server
main
関数を変更
main.py
from langchain_community.chat_message_histories.dynamodb import DynamoDBChatMessageHistory
def main():
history = DynamoDBChatMessageHistory(
table_name="MainStack-SessionsTablexxxxxxxx-xxxxxxxxxxxx",
primary_key_name="id",
session_id="dummy-session-id",
)
history.add_user_message("こんにちは。私は人間です。")
history.add_ai_message("こんにちは。私はAIです。")
if __name__ == "__main__":
main()
作成されたデータを確認
{
"id": "dummy-session-id",
"History": [
{
"data": {
"id": null,
"additional_kwargs": {},
"content": "こんにちは。私は人間です。",
"example": false,
"name": null,
"type": "human"
},
"type": "human"
},
{
"data": {
"id": null,
"additional_kwargs": {},
"content": "こんにちは。私はAIです。",
"example": false,
"name": null,
"type": "ai"
},
"type": "ai"
}
]
}
DynamoDBに会話履歴を保持してマルチターン会話をやってみる
ハンドラ関数を更新
- ドキュメント を参考にChainにDynamoDBChatMessageHistoryを組み込む
- 会話履歴保存用のテーブル名は環境変数から読み込む
- セッションIDはLambdaのイベントから指定できるようにする
multi_turn_conversation_handler.py
import pprint
from typing import Any
from langchain_community.chat_models import BedrockChat
from langchain_community.chat_message_histories.dynamodb import DynamoDBChatMessageHistory
from langchain.chains import LLMChain
from langchain.prompts import (
ChatPromptTemplate,
MessagesPlaceholder,
)
from langchain_core.runnables.history import RunnableWithMessageHistory
import os
chat = BedrockChat(
model_id="anthropic.claude-v2:1",
region_name="us-east-1",
model_kwargs={
"temperature": 0,
},
verbose=True,
)
prompt = ChatPromptTemplate.from_messages(
[
("system", "You are a helpful assistant."),
MessagesPlaceholder(variable_name="history"),
("human", "{question}"),
]
)
chain = prompt | chat
table_name = os.environ.get("SESSIONS_TABLE_NAME")
chain_with_history = RunnableWithMessageHistory(
chain,
lambda session_id: DynamoDBChatMessageHistory(
table_name=table_name,
primary_key_name="id",
session_id=session_id,
),
input_messages_key="question",
history_messages_key="history",
)
def handler(event: dict[str, Any], context: Any) -> dict[str, Any]:
session_id = event["sessionId"]
question = event["question"]
print(f"Session ID: {session_id}")
print(f"Question: {question}")
config = {"configurable": {"session_id": session_id}}
response = chain_with_history.invoke({"question": question}, config=config)
pprint.pprint(response)
CDKコードを更新
- SessionsTableのテーブル名をLambdaの環境変数に設定
- LambdaにSessionsTableの読み書き権限を付与
import { PythonFunction } from "@aws-cdk/aws-lambda-python-alpha";
import { Duration, Stack, StackProps, aws_dynamodb } from "aws-cdk-lib";
import { Effect, PolicyStatement } from "aws-cdk-lib/aws-iam";
import { Architecture, Runtime } from "aws-cdk-lib/aws-lambda";
import { Construct } from "constructs";
export class MainStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
const sessionsTable = new aws_dynamodb.Table(this, "SessionsTable", {
partitionKey: {
name: "id",
type: aws_dynamodb.AttributeType.STRING,
},
billingMode: aws_dynamodb.BillingMode.PAY_PER_REQUEST,
});
const multiTurnConversationFn = new PythonFunction(
this,
"MultiTurnConversationFn",
{
entry: "../server",
index: "multi_turn_conversation_handler.py",
runtime: Runtime.PYTHON_3_12,
architecture: Architecture.ARM_64,
environment: {
SESSIONS_TABLE_NAME: sessionsTable.tableName,
},
bundling: {
assetExcludes: [".venv", "__pycache__", "tests", "README.md"],
},
timeout: Duration.minutes(15),
},
);
multiTurnConversationFn.addToRolePolicy(
new PolicyStatement({
effect: Effect.ALLOW,
actions: ["bedrock:InvokeModel"],
resources: ["*"],
}),
);
sessionsTable.grantReadWriteData(multiTurnConversationFn);
}
}
実行結果
1ターン目の会話
イベントJSON
{
"sessionId": "dummy-session-id-2",
"question": "おすすめのお肉料理の候補を3つ挙げてください"
}
ログ
START RequestId: ff9ac05a-cf74-49aa-b777-0f29aae5a668 Version: $LATEST
Session ID: dummy-session-id-2
Question: おすすめのお肉料理の候補を3つ挙げてください
AIMessage(content='おすすめのお肉料理の候補として、以下の3つを挙げます。\n\n1. ステーキ\n肉のうまみが存分に味わえる定番のお肉料理です。レア~ミディアムレアがおすすめです。\n\n2. ハンバーグ\n具だくさんのハンバーグは子供から大人まで人気のメニュー。卵とパン粉でとじてジューシーな味わいが楽しめます。\n\n3. 焼き鳥\n骨付き鳥肉を炭火で焼いた焼き鳥は味も香りも最高。塩・タレがついてとってもおいしい一品です。')
END RequestId: ff9ac05a-cf74-49aa-b777-0f29aae5a668
REPORT RequestId: ff9ac05a-cf74-49aa-b777-0f29aae5a668 Duration: 12820.07 ms Billed Duration: 12821 ms Memory Size: 128 MB Max Memory Used: 128 MB Init Duration: 5422.57 ms
2ターン目の会話
イベントJSON
{
"sessionId": "dummy-session-id-2",
"question": "1のレシピを教えてください"
}
ログ
START RequestId: 49aaefa0-bb34-441c-8e0b-65292a4b033f Version: $LATEST
Session ID: dummy-session-id-2
Question: 1のレシピを教えてください
AIMessage(content='ステーキのレシピをご紹介します。\n\n【材料】(2人分)\n・牛肉のロース肉 400g\n・塩 少々\n・こしょう 少々\n・オリーブオイル 大さじ2\n・バター 20g\n\n【作り方】\n1. 肉から筋や余分な脂身をそぎ落とし、表面の水分をふき取る。\n2. 塩・こしょうを振りかける。\n3. フライパンを強火で熱し、オリーブオイルを入れる。\n4. 2cm厚のステーキに切って、脂がきつね色になるまで片面を焼く。\n5. 焼けた面にバターをのせて裏返し、お好みの焼き加減に焼き上げる。\n\nご家庭の味で楽しんでください。レア~ミディアムレアがおすすめです。')
END RequestId: 49aaefa0-bb34-441c-8e0b-65292a4b033f
REPORT RequestId: 49aaefa0-bb34-441c-8e0b-65292a4b033f Duration: 11322.72 ms Billed Duration: 11323 ms Memory Size: 128 MB Max Memory Used: 128 MB
DynamoDBテーブルに保存された会話履歴
{
"id": "dummy-session-id-2",
"History": [
{
"data": {
"id": null,
"additional_kwargs": {},
"content": "おすすめのお肉料理の候補を3つ挙げてください",
"example": false,
"name": null,
"type": "human"
},
"type": "human"
},
{
"data": {
"id": null,
"additional_kwargs": {},
"content": "おすすめのお肉料理の候補として、以下の3つを挙げます。\n\n1. ステーキ\n肉のうまみが存分に味わえる定番のお肉料理です。レア~ミディアムレアがおすすめです。\n\n2. ハンバーグ\n具だくさんのハンバーグは子供から大人まで人気のメニュー。卵とパン粉でとじてジューシーな味わいが楽しめます。\n\n3. 焼き鳥\n骨付き鳥肉を炭火で焼いた焼き鳥は味も香りも最高。塩・タレがついてとってもおいしい一品です。",
"example": false,
"name": null,
"type": "ai"
},
"type": "ai"
},
{
"data": {
"id": null,
"additional_kwargs": {},
"content": "1のレシピを教えてください",
"example": false,
"name": null,
"type": "human"
},
"type": "human"
},
{
"data": {
"id": null,
"additional_kwargs": {},
"content": "ステーキのレシピをご紹介します。\n\n【材料】(2人分)\n・牛肉のロース肉 400g\n・塩 少々\n・こしょう 少々\n・オリーブオイル 大さじ2\n・バター 20g\n\n【作り方】\n1. 肉から筋や余分な脂身をそぎ落とし、表面の水分をふき取る。\n2. 塩・こしょうを振りかける。\n3. フライパンを強火で熱し、オリーブオイルを入れる。\n4. 2cm厚のステーキに切って、脂がきつね色になるまで片面を焼く。\n5. 焼けた面にバターをのせて裏返し、お好みの焼き加減に焼き上げる。\n\nご家庭の味で楽しんでください。レア~ミディアムレアがおすすめです。",
"example": false,
"name": null,
"type": "ai"
},
"type": "ai"
}
]
}
ついでにLangSmith Tracingと連携してみる
環境変数を設定するだけでLangSmith Tracingを利用できるようになる
アプリケーション側のコードを変更する必要はない
LangSmith有効化のためにLambda関数に環境変数を設定
- SSM Parameter StoreにAPIキーを保存しておく
- CDKでSSM Parameter StoreからAPIキーを読み込んでLambda関数の環境変数に設定する
main-stack.ts
+ const langChainApiKey = aws_ssm.StringParameter.valueForStringParameter(
+ this,
+ "LangChainApiKey",
+ );
const multiTurnConversationFn = new PythonFunction(
this,
"MultiTurnConversationFn",
{
entry: "../server",
index: "multi_turn_conversation_handler.py",
runtime: Runtime.PYTHON_3_12,
architecture: Architecture.ARM_64,
environment: {
SESSIONS_TABLE_NAME: sessionsTable.tableName,
+ LANGCHAIN_TRACING_V2: "true",
+ LANGCHAIN_API_KEY: langChainApiKey,
},
bundling: {
assetExcludes: [".venv", "__pycache__", "tests", "README.md"],
},
timeout: Duration.minutes(15),
},
);
multiTurnConversationFn.addToRolePolicy(
new PolicyStatement({
effect: Effect.ALLOW,
actions: ["bedrock:InvokeModel"],
resources: ["*"],
}),
);
Lambda関数を実行してトレースを確認する
こちら と同様の会話を行ったあとのトレースを確認
会話履歴を読み込んで出力を生成していることが分かる
このスクラップは2024/03/11にクローズされました