DatabricksでLangGraphのクイックスタートを動かしてみる(その3)

こちらの続きです。

https://qiita.com/taka_yayoi/items/311048184657aa8ad53f

こちらのPart 3: Adding Memory to the Chatbotを動かします。

https://langchain-ai.github.io/langgraph/tutorials/introduction/#part-3-adding-memory-to-the-chatbot

# パート3: チャットボットにメモリを追加する

私たちのチャットボットは、ツールを使ってユーザーの質問に答えることができますが、以前のやり取りのコンテキストを覚えていません。これでは、一貫性のあるマルチターンの会話をする能力が制限されます。

LangGraphは、**永続的なチェックポイント機能**を通じてこの問題を解決します。グラフをコンパイルする際に`checkpointer`を提供し、グラフを呼び出す際に`thread_id`を指定すると、LangGraphは各ステップの後に自動的に状態を保存します。同じ`thread_id`を使用してグラフを再度呼び出すと、グラフは保存された状態をロードし、チャットボットが前回の続きから再開できるようにします。

後で説明するように、**チェックポイント機能**は単純なチャットメモリよりも*はるかに*強力です。エラー回復、人間を介したワークフロー、タイムトラベルインタラクションなど、複雑な状態をいつでも保存および再開することができます。しかし、先走る前に、マルチターンの会話を可能にするためにチェックポイント機能を追加しましょう。

## セットアップ

まず、必要なパッケージをインストールし、環境を設定します：

```py
%%capture --no-stderr
%pip install -U langgraph langsmith langchain_openai openai tavily-python langchain_community
%restart_python
```

```py
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`チェックポインタを作成します。

```py
from langgraph.checkpoint.memory import MemorySaver

memory = MemorySaver()
```

ここではインメモリチェックポインタを使用しています。これはチュートリアルには便利です（すべてをインメモリに保存します）。本番アプリケーションでは、`SqliteSaver`や`PostgresSaver`を使用して独自のDBに接続するように変更することが多いでしょう。

次にグラフを定義します。すでに独自の`BasicToolNode`を構築しているので、LangGraph組み込みの`ToolNode`と`tools_condition`に置き換えます。これらは並列API実行などの便利な機能を提供します。それ以外は、すべてパート2からコピーされています。

```py
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")
```

最後に、提供されたチェックポインタを使用してグラフをコンパイルします。

```py
graph = graph_builder.compile(checkpointer=memory)
```

グラフの接続性は[パート2](https://qiita.com/taka_yayoi/items/311048184657aa8ad53f)から変わっていないことに注意してください。私たちが行っているのは、グラフが各ノードを処理する際に`State`をチェックポイントすることだけです。

```py
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
```
![download.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/cd0be5e9-e8bd-b598-c99e-d461c7d35ed1.png)

これでボットと対話できます！まず、この会話のキーとして使用するスレッドを選択してください。

```py
config = {"configurable": {"thread_id": "1"}}
```

次にチャットbotを呼び出します。

```py
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()
```
![Screenshot 2025-01-30 at 12.20.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/75b4a8ed-341f-25fd-b28b-b27b4c20b547.png)

:::note info
**注意:**
構成はグラフを呼び出すときに**2番目の引数**として提供されました。重要なのは、グラフ入力（`{'messages': []}`）内にネストされていないことです。
:::

フォローアップを尋ねてみましょう: あなたの名前を覚えているかどうか確認してください。

```py
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()
```
![Screenshot 2025-01-30 at 12.21.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/3f46f377-38ca-43ee-43e2-679234b26e2d.png)

メモリに外部リストを使用していないことに**注意**してください。すべてチェックポインタによって処理されます！この[LangSmithトレース](https://smith.langchain.com/public/29ba22b5-6d40-4fbe-8d27-b369e3329c84/r)で完全な実行を確認して、何が起こっているかを確認できます。

信じられませんか？別の設定を使用してこれを試してみてください。

```py
# 唯一の違いは、ここで `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()
```
![Screenshot 2025-01-30 at 12.21.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/8cda0739-6009-9741-12e7-a4110aaebe39.png)

変更したのはconfigの`thread_id`を修正したこと**だけ**です。この呼び出しの[LangSmithトレース](https://smith.langchain.com/public/51a62351-2f0a-4058-91cc-9996c5561428/r)を比較のために参照してください。

現在、2つの異なるスレッドでいくつかのチェックポイントを作成しました。しかし、チェックポイントには何が含まれているのでしょうか？任意の時点で特定のconfigのグラフの`state`を検査するには、`get_state(config)`を呼び出します。

```py
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=())
```

```py
snapshot.next
```
```
()
```

上記のスナップショットには、現在の状態値、対応する設定、および次に処理するノード`next`が含まれています。私たちの場合、グラフは`END`状態に達しているため、`next`は空になっています。

**おめでとうございます！** LangGraphのチェックポイントシステムのおかげで、チャットボットはセッション間で会話の状態を維持できるようになりました。これにより、より自然で文脈に沿った対話が可能になります。LangGraphのチェックポイントは、**任意の複雑なグラフ状態**も処理でき、単純なチャットメモリよりもはるかに表現力があり強力です。

次のパートでは、ボットが進行する前にガイダンスや検証が必要な状況に対処するために、人間の監視を導入します。

こちらに続きます。

https://qiita.com/taka_yayoi/items/e2a7fcafd21acd31eeff

### はじめてのDatabricks

[はじめてのDatabricks](https://qiita.com/taka_yayoi/items/8dc72d083edb879a5e5d)

### Databricks無料トライアル

[Databricks無料トライアル](https://databricks.com/jp/try-databricks)