fbpx

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とのやりとりの履歴をメモリに保持し、会話がつながるようにしてみました。また一歩、実用的なチャットボットに近づいたことになります。

ただ先に述べた通り、一旦スクリプトを再起動すると会話がつながらなかったり、コンソールアプリであるため使い勝手がよくなかったりと、まだまだ実用には遠いです。

引き続き、実用的なチャットボットに向けて段階的に強化・改良を進めていきます。

Author

Chef・Docker・Mirantis製品などの技術要素に加えて、会議の進め方・文章の書き方などの業務改善にも取り組んでいます。「Chef活用ガイド」共著のほか、Debian Official Developerもやっています。

Daisuke Higuchiの記事一覧

新規CTA