こちらの続きです。
こちらのPart 3: Adding Memory to the Chatbotを動かします。
パート3: チャットボットにメモリを追加する
私たちのチャットボットは、ツールを使ってユーザーの質問に答えることができますが、以前のやり取りのコンテキストを覚えていません。これでは、一貫性のあるマルチターンの会話をする能力が制限されます。
LangGraphは、永続的なチェックポイント機能を通じてこの問題を解決します。グラフをコンパイルする際にcheckpointer
を提供し、グラフを呼び出す際にthread_id
を指定すると、LangGraphは各ステップの後に自動的に状態を保存します。同じthread_id
を使用してグラフを再度呼び出すと、グラフは保存された状態をロードし、チャットボットが前回の続きから再開できるようにします。
後で説明するように、チェックポイント機能は単純なチャットメモリよりもはるかに強力です。エラー回復、人間を介したワークフロー、タイムトラベルインタラクションなど、複雑な状態をいつでも保存および再開することができます。しかし、先走る前に、マルチターンの会話を可能にするためにチェックポイント機能を追加しましょう。
セットアップ
まず、必要なパッケージをインストールし、環境を設定します:
%%capture --no-stderr
%pip install -U langgraph langsmith langchain_openai openai tavily-python langchain_community
%restart_python
import os
os.environ["OPENAI_API_KEY"] = dbutils.secrets.get(scope="demo-token-takaaki.yayoi", key="openai_api_key")
# TavilyのAPIキー
os.environ["TAVILY_API_KEY"] = "<TavilyのAPIキー>"
まず、MemorySaver
チェックポインタを作成します。
from langgraph.checkpoint.memory import MemorySaver
memory = MemorySaver()
ここではインメモリチェックポインタを使用しています。これはチュートリアルには便利です(すべてをインメモリに保存します)。本番アプリケーションでは、SqliteSaver
やPostgresSaver
を使用して独自のDBに接続するように変更することが多いでしょう。
次にグラフを定義します。すでに独自のBasicToolNode
を構築しているので、LangGraph組み込みのToolNode
とtools_condition
に置き換えます。これらは並列API実行などの便利な機能を提供します。それ以外は、すべてパート2からコピーされています。
from typing import Annotated
from langchain_openai import ChatOpenAI
from langchain_community.tools.tavily_search import TavilySearchResults
from langchain_core.messages import BaseMessage
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode, tools_condition
class State(TypedDict):
messages: Annotated[list, add_messages]
graph_builder = StateGraph(State)
tool = TavilySearchResults(max_results=2)
tools = [tool]
llm = ChatOpenAI(model="gpt-4o-mini")
llm_with_tools = llm.bind_tools(tools)
def chatbot(state: State):
return {"messages": [llm_with_tools.invoke(state["messages"])]}
graph_builder.add_node("chatbot", chatbot)
tool_node = ToolNode(tools=[tool])
graph_builder.add_node("tools", tool_node)
# 条件付きエッジを追加
graph_builder.add_conditional_edges(
"chatbot",
tools_condition,
)
# ツールが呼び出されるたびに、次のステップを決定するためにチャットボットに戻る
graph_builder.add_edge("tools", "chatbot")
graph_builder.add_edge(START, "chatbot")
最後に、提供されたチェックポインタを使用してグラフをコンパイルします。
graph = graph_builder.compile(checkpointer=memory)
グラフの接続性はパート2から変わっていないことに注意してください。私たちが行っているのは、グラフが各ノードを処理する際にState
をチェックポイントすることだけです。
from IPython.display import Image, display
try:
display(Image(graph.get_graph().draw_mermaid_png()))
except Exception:
# This requires some extra dependencies and is optional
pass
これでボットと対話できます!まず、この会話のキーとして使用するスレッドを選択してください。
config = {"configurable": {"thread_id": "1"}}
次にチャットbotを呼び出します。
user_input = "こんにちは!私の名前はTakaです。"
# configはstream()またはinvoke()の**2番目の引数**です!
events = graph.stream(
{"messages": [{"role": "user", "content": user_input}]},
config,
stream_mode="values",
)
for event in events:
event["messages"][-1].pretty_print()
注意:
構成はグラフを呼び出すときに2番目の引数として提供されました。重要なのは、グラフ入力({'messages': []}
)内にネストされていないことです。
フォローアップを尋ねてみましょう: あなたの名前を覚えているかどうか確認してください。
user_input = "私の名前を覚えていますか?"
# configはstream()またはinvoke()の**2番目の引数**です!
events = graph.stream(
{"messages": [{"role": "user", "content": user_input}]},
config,
stream_mode="values",
)
for event in events:
event["messages"][-1].pretty_print()
メモリに外部リストを使用していないことに注意してください。すべてチェックポインタによって処理されます!このLangSmithトレースで完全な実行を確認して、何が起こっているかを確認できます。
信じられませんか?別の設定を使用してこれを試してみてください。
# 唯一の違いは、ここで `thread_id` を "1" ではなく "2" に変更することです
events = graph.stream(
{"messages": [{"role": "user", "content": user_input}]},
{"configurable": {"thread_id": "2"}},
stream_mode="values",
)
for event in events:
event["messages"][-1].pretty_print()
変更したのはconfigのthread_id
を修正したことだけです。この呼び出しのLangSmithトレースを比較のために参照してください。
現在、2つの異なるスレッドでいくつかのチェックポイントを作成しました。しかし、チェックポイントには何が含まれているのでしょうか?任意の時点で特定のconfigのグラフのstate
を検査するには、get_state(config)
を呼び出します。
snapshot = graph.get_state(config)
snapshot
StateSnapshot(values={'messages': [HumanMessage(content='こんにちは!私の名前はTakaです。', additional_kwargs={}, response_metadata={}, id='c646da36-f0ba-442e-9b59-22dba8b8e657'), AIMessage(content='こんにちは、Takaさん!どのようにお手伝いできますか?', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 19, 'prompt_tokens': 89, 'total_tokens': 108, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_72ed7ab54c', 'finish_reason': 'stop', 'logprobs': None}, id='run-db9cd933-bc92-4945-a961-e1cdd4bbe391-0', usage_metadata={'input_tokens': 89, 'output_tokens': 19, 'total_tokens': 108, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}), HumanMessage(content='私の名前を覚えていますか?', additional_kwargs={}, response_metadata={}, id='8d20da02-230a-4bea-aa14-a824df5f33f2'), AIMessage(content='はい、Takaさんの名前を覚えています!他に何かお手伝いできることはありますか?', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 29, 'prompt_tokens': 123, 'total_tokens': 152, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_bd83329f63', 'finish_reason': 'stop', 'logprobs': None}, id='run-febb8f96-99ad-4781-80d1-9158cb42441b-0', usage_metadata={'input_tokens': 123, 'output_tokens': 29, 'total_tokens': 152, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]}, next=(), config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1efdeac8-3f45-62d8-8004-7bd258a57fa7'}}, metadata={'source': 'loop', 'writes': {'chatbot': {'messages': [AIMessage(content='はい、Takaさんの名前を覚えています!他に何かお手伝いできることはありますか?', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 29, 'prompt_tokens': 123, 'total_tokens': 152, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_bd83329f63', 'finish_reason': 'stop', 'logprobs': None}, id='run-febb8f96-99ad-4781-80d1-9158cb42441b-0', usage_metadata={'input_tokens': 123, 'output_tokens': 29, 'total_tokens': 152, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]}}, 'thread_id': '1', 'step': 4, 'parents': {}}, created_at='2025-01-30T01:50:00.134296+00:00', parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1efdeac8-27a5-6e7d-8003-28bb0f0f974a'}}, tasks=())
snapshot.next
()
上記のスナップショットには、現在の状態値、対応する設定、および次に処理するノードnext
が含まれています。私たちの場合、グラフはEND
状態に達しているため、next
は空になっています。
おめでとうございます! LangGraphのチェックポイントシステムのおかげで、チャットボットはセッション間で会話の状態を維持できるようになりました。これにより、より自然で文脈に沿った対話が可能になります。LangGraphのチェックポイントは、任意の複雑なグラフ状態も処理でき、単純なチャットメモリよりもはるかに表現力があり強力です。
次のパートでは、ボットが進行する前にガイダンスや検証が必要な状況に対処するために、人間の監視を導入します。
こちらに続きます。