LangGraphの会話履歴をメモリ保持しよう #ai #langgraph #azure #openai #llm #python
はじめに
当ブログではここまで
にて、LangGraphを使ってAzure OpenAIとやりとりする方式を見てきました。
これらでの「やりとり」は一問一答、1回きりです。質疑応答の内容は直前であっても一切覚えていません。そのため本家 ChatGPT のように会話を続けて情報を徐々に、あるいは次々引き出したり、または方向性を軌道修正したり、ということができません。
そこで本稿では、LangGraphの機能を使ってやりとりの履歴をメモリに保持し、会話ができるようにしてみます。
会話履歴を保持できないチャットボット
次はやりとりの履歴を保持できないチャットボットの例です。
from typing import Annotated from typing_extensions import TypedDict from langgraph.graph import StateGraph, START, END from langgraph.graph.message import add_messages from langchain_core.messages import BaseMessage from langchain_openai import AzureChatOpenAI from yaspin import yaspin import os from dotenv import load_dotenv load_dotenv() # os.getenv("AZURE_OPENAI_API_KEY") # os.getenv("AZURE_OPENAI_ENDPOINT") llm = AzureChatOpenAI( azure_deployment = os.getenv("AZURE_OPENAI_MODEL_NAME"), api_version = os.getenv("AZURE_OPENAI_API_VERSION"), temperature = 0.95, max_tokens = 1024, ) class State(TypedDict): messages: Annotated[list, add_messages] def chatbot(state: State): with yaspin(text="Processing", color="yellow") as spinner: res = llm.invoke(state["messages"]) spinner.ok("✅ ") return {"messages": res} graph_builder = StateGraph(State) graph_builder.add_node("chatbot", chatbot) graph_builder.set_entry_point("chatbot") graph_builder.set_finish_point("chatbot") graph = graph_builder.compile() def stream_graph_updates(user_input: str): events = graph.stream( {"messages": [("user", user_input)]}, stream_mode="values" ) for event in events: print(event["messages"][-1].content) while True: try: user_input = input("User: ") if user_input.lower() in ["quit", "exit", "q"]: print("Goodbye!") break stream_graph_updates(user_input) except Exception as e: print(f"error: {e}") break
これを実行すると User:
と入力待ちになるので、自己紹介してみましょう。
User: 私の名前はボブです。 私の名前はボブです。 ✅ Processing こんにちはボブさん!どうぞよろしくお願いします。何かお手伝いできることがあれば、教えてください。 User:
きちんと名前を呼び返してくれました。続けて User:
と入力待ちになるので、今教えた名前を聞き返してみましょう。
User: 私の名前はボブです。 私の名前はボブです。 ✅ Processing こんにちはボブさん!どうぞよろしくお願いします。何かお手伝いできることがあれば、教えてください。 User: 私の名前は何ですか? 私の名前は何ですか? ✅ Processing 私の知識ベースには、あなたの個人情報が含まれていませんので、あなたの名前が何であるかを知ることはできません。あなた自身によって提供されない限り、あなたの名前を知る方法はありません。 User:
直前に自己紹介していたにも関わらず、「知らない」となってしまいました。これはつまり、直前のやりとりを一切覚えていない、参照していないことを示しています。これではチャットボットとして使えません。
会話履歴をメモリ保持するチャットボット
LangGraphには状態を永続化する仕組みがありますので、これを利用して会話履歴を保持できます。公式ドキュメントでAdding Memory to the Chatbotとして触れられている方法です。
ここでは会話履歴をメモリに保持する形式を取るので、MemorySaverを使います。
from langgraph.checkpoint.memory import MemorySaver
グラフをコンパイルする際に、メモリに状態を保存するように指定します。LangGraphの用語で、保存する状態のスナップショットを Checkpoint
と呼び、実際に保存する仕組みを Checkpointer
と呼びます。
memory = MemorySaver() graph = graph_builder.compile(checkpointer=memory)
Checkpointer によって保存された各 Checkpoint には、一意のIDを割り当てます。これをLangGraphの用語で Thread
と呼びます。ここでは Thread のIDを 1
として固定しています。
events = graph.stream( {"messages": [("user", user_input)]}, {"configurable": {"thread_id": "1"}}, stream_mode="values" )
前項の「会話履歴を保持できないチャットボット」との差分は次のようになります。
--- 23_chatbot_nomem.py 2024-11-08 18:16:32.580373788 +0900 +++ 23_chatbot_simple.py 2024-11-08 18:25:42.232364204 +0900 @@ -6,6 +6,7 @@ from langgraph.graph.message import add_messages from langchain_core.messages import BaseMessage from langchain_openai import AzureChatOpenAI +from langgraph.checkpoint.memory import MemorySaver from yaspin import yaspin import os @@ -35,11 +36,13 @@ graph_builder.set_entry_point("chatbot") graph_builder.set_finish_point("chatbot") -graph = graph_builder.compile() +memory = MemorySaver() +graph = graph_builder.compile(checkpointer=memory) def stream_graph_updates(user_input: str): events = graph.stream( {"messages": [("user", user_input)]}, + {"configurable": {"thread_id": "1"}}, stream_mode="values" ) for event in events:
全体のコードは次の通りです。
from typing import Annotated from typing_extensions import TypedDict from langgraph.graph import StateGraph, START, END from langgraph.graph.message import add_messages from langchain_core.messages import BaseMessage from langchain_openai import AzureChatOpenAI from langgraph.checkpoint.memory import MemorySaver from yaspin import yaspin import os from dotenv import load_dotenv load_dotenv() # os.getenv("AZURE_OPENAI_API_KEY") # os.getenv("AZURE_OPENAI_ENDPOINT") llm = AzureChatOpenAI( azure_deployment = os.getenv("AZURE_OPENAI_MODEL_NAME"), api_version = os.getenv("AZURE_OPENAI_API_VERSION"), temperature = 0.95, max_tokens = 1024, ) class State(TypedDict): messages: Annotated[list, add_messages] def chatbot(state: State): with yaspin(text="Processing", color="yellow") as spinner: res = llm.invoke(state["messages"]) spinner.ok("✅ ") return {"messages": res} graph_builder = StateGraph(State) graph_builder.add_node("chatbot", chatbot) graph_builder.set_entry_point("chatbot") graph_builder.set_finish_point("chatbot") memory = MemorySaver() graph = graph_builder.compile(checkpointer=memory) def stream_graph_updates(user_input: str): events = graph.stream( {"messages": [("user", user_input)]}, {"configurable": {"thread_id": "1"}}, stream_mode="values" ) for event in events: print(event["messages"][-1].content) while True: try: user_input = input("User: ") if user_input.lower() in ["quit", "exit", "q"]: print("Goodbye!") break stream_graph_updates(user_input) except Exception as e: print(f"error: {e}") break
では、実際に動かしてみましょう。「会話履歴を保持できないチャットボット」と同じく、自己紹介から始めます。
User: 私の名前はボブです。 私の名前はボブです。 ✅ Processing こんにちは、ボブさん。どうぞよろしくお願いします。何かお手伝いできることがあれば、お知らせください。 User:
ここで聞き返してみましょう。
User: 私の名前はボブです。 私の名前はボブです。 ✅ Processing こんにちは、ボブさん。どうぞよろしくお願いします。何かお手伝いできることがあれば、お知らせください。 User: 私の名前は何ですか? 私の名前は何ですか? ✅ Processing あなたの名前はボブだとおっしゃいました。 User:
名前を覚えていました! さらに身長・体重を伝えてみましょう。
User: 私の名前は何ですか? 私の名前は何ですか? ✅ Processing あなたの名前はボブだとおっしゃいました。 User: 私は身長193cm、体重95kgです。 私は身長193cm、体重95kgです。 ✅ Processing 了解しました。あなたは身長193cm、体重95kgですね。何か特定の情報や質問があれば、お気軽にどうぞ。 User:
覚えてくれたようですね。プロフィールを聞いてみましょう。
User: 私は身長193cm、体重95kgです。 私は身長193cm、体重95kgです。 ✅ Processing 了解しました。あなたは身長193cm、体重95kgですね。何か特定の情報や質問があれば、お気軽にどうぞ。 User: 私のプロフィールを教えてください。 私のプロフィールを教えてください。 ✅ Processing あなたが提供した情報に基づいて、以下のようなプロフィールをまとめることができます: - 名前: ボブ - 身長: 193cm - 体重: 95kg この情報以外にも、あなたの趣味、興味、職業など、さらに詳しいプロフィールを共有したい場合は、追加情報を教えてください。プロフィールは個人をよりよく理解するための基本的なデータや興味、活動などに基づいて作成されます。 User:
直前の身長・体重だけでなく、最初に教えた名前もきちんと返してくれました。
一旦終了してみましょう。このスクリプトでは quit
と入力すると終了します。
この情報以外にも、あなたの趣味、興味、職業など、さらに詳しいプロフィールを共有したい場合は、追加情報を教えてください。プロフィールは個人をよりよく理解するための基本的なデータや興味、活動などに基づいて作成されます。 User: quit Goodbye!
再度スクリプトを実行し、プロフィールを聞いてみます。
User: 私のプロフィールを教えてください。 私のプロフィールを教えてください。 ✅ Processing すみませんが、私はあなたのプロフィールに関する情報を持っていません。私はOpenAIの人工知能アシスタントで、個人情報やユーザープロフィールにアクセスする能力がありません。私の目的は、質問に答えたり、一般的な情報を提供したりすることです。プライバシーとセキュリティを保つため、このようなシステムが設計されています。何か質問があれば、それに関する情報を提供する手助けをすることはできますが、個人的な情報についてはお手伝いできません。 User:
今回はメモリに記憶しているので、スクリプトを一旦終了したことによりメモリがクリアされ、先程の会話の内容をすべて忘れてしまったことになります。
まとめ
本稿では、LangGraphの機能を使って、生成AIとのやりとりの履歴をメモリに保持し、会話がつながるようにしてみました。また一歩、実用的なチャットボットに近づいたことになります。
ただ先に述べた通り、一旦スクリプトを再起動すると会話がつながらなかったり、コンソールアプリであるため使い勝手がよくなかったりと、まだまだ実用には遠いです。
引き続き、実用的なチャットボットに向けて段階的に強化・改良を進めていきます。