watsonx.aiを使って、LangGraph でチャットボットを構築する手順の解説

# はじめに
筆者は最近、LangGraphの学習を進め、watsonx.aiを活用してツールを選定するチャットボットの構築に挑戦しました。本記事では、LangGraphに関する少し知識がある方を対象とし、LangGraphを用いたチャットボット構築の手順を解説します。
本記事が、LangGraphを使ってチャットボットを構築してみたい方や、LLMエージェントのステップをグラフ化して状態管理を行いたい方の参考になれば幸いです。
# LangGraphとは
LangGraphは、グラフベースのステートマシンを使用して、複雑でスケーラブルなAIエージェントシステムを構築するためのPythonライブラリです。エージェントのワークフローをステートマシンとしてモデル化することで、信頼性と永続性を提供し、長時間実行されるタスクやエラーが発生しやすいワークロードをサポートします。

LangGraphを理解するためには、以下の3つの主要なコンポーネントが非常に重要だと思います。

**- State**
システムやエージェントの現在の状態を保持し、ワークフローの進行状況やデータを管理します
**- Node**
ワークフロー内の個々の処理やアクションを表し、特定のタスクを実行する基本単位です
**- Edge**
ノード同士を接続し、データや状態の遷移を制御する役割を持ちます

LangGraphでは、**State**、**Node**、**Edge**といった抽象化要素を採用することで、信頼性と柔軟性のバランスを保ちながら、エージェントに制御力を持たせています。Stateは共通のメモリとして役立ち、NodeとEdgeを用いてアプリケーションのフローを表現することで、制御の流れを細かく設定可能にします。さらに、エージェントが自己判断で進行を制御できるようにすることで、動的なワークフローが実現します。

:::note info
詳細についてさらに知りたい方は、以下のリンクをご参照ください。
:::

https://langchain-ai.github.io/langgraph/

# 構築手順
さて、実際にLangGraphを使用してチャットボットを構築してみましょう。
今回構築したいチャットボットは、ユーザーのプロンプトが外部情報の検索を必要とするかどうかを判断します。もし直接回答できる場合は検索を行いませんが、回答できない場合は`tools`(Tavily Search、またはWikipedia)を使って検索を行い、その結果を基に回答します。グラフは以下のようになります
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3850641/1f84419f-d108-34f7-fd87-8462d3ccdf51.png">
## 動作環境
```
LangChain: 0.3.4
LangChain Community: 0.3.3
LangGraph: 0.2.39
langchain-ibm: 0.3.1

Python: 3.11.9
```
## watsonx.aiに関する設定
今回は、LangChain LLMs APIでwatsonx.aiモデルを使用します。
初めてwatsonx.aiを使用する際には、ライブラリをインストールする必要があります：
``` bash
%pip install -qU langchain-ibm
```
``` python
from langchain_ibm import ChatWatsonx

llm = ChatWatsonx(
    model_id="mistralai/mistral-large",
    url="https://us-south.ml.cloud.ibm.com",
    WATSONX_APIKEY = "Your_Watson_Apikey"
    project_id=project_id,
    params={
    "temperature": 0.9,
    "max_tokens": 500,
})
```

Tavily Search APIやWikipedia APIといったユーザーが用意した機能（Function）を呼び出すためには、[Function Calling](https://python.langchain.com/v0.1/docs/modules/model_io/chat/function_calling/)（Tool Callingとも呼ばれる）という仕組みが必要です。つまり、Function Callingとは、LLMが外部のツールを呼び出して情報を取得したり、特定のタスクを実行したりする機能のことです。
今回、`mistralai/mistral-large`モデルを選択した理由は、LangChainの[ドキュメント](https://python.langchain.com/docs/integrations/chat/ibm_watsonx/)に記載されている通り、ChatWatsonxにおける`.bind_tools`のサポートがまだベータ段階にあるため、Function Callingを行う際にこのモデルの使用が推奨されているからです。

:::note info
**APIキー**の作成方法および**プロジェクトID**の確認方法については、以下リンクを参照してください。
:::
https://www.ibm.com/docs/en/mas-cd/maximo-manage/continuous-delivery?topic=setup-create-watsonx-api-key
## 使用tools
使用するツール（`Tavily_Search`と`Wikipedia`）を定義した後、これらのツールをLLMに
バインドします。
Function Calling機能をサポートするLangChainのChatModelには、.bind_toolsメソッドが実装されており、必要なツールへのアクセスが可能になります。このメソッドによって、LLMが検索エンジンを使用する際に正しいJSON形式を理解できるようになります。
``` python
from langchain_community.tools import WikipediaQueryRun
from langchain_community.utilities import WikipediaAPIWrapper
from langchain_community.tools.tavily_search import TavilySearchResults

tavily_search = TavilySearchResults(max_results=2)
wiki_search = WikipediaQueryRun(api_wrapper=WikipediaAPIWrapper())

tools = [tavily_search,wiki_search]

llm_with_tools = llm.bind_tools(tools)
```
## Stateを定義
- Stateを定義
TypedDict を使って、Stateのスキーマを定義しています。Stateには、`messages` というリストが含まれています。
```PYTHON
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages

class State(TypedDict):
    messages: Annotated[list, add_messages]
    
graph_builder = StateGraph(State)
```
## Chatbotノードを定義
`graph_builder.add_node()`でノードを定義できます。
- Chatbotノードを定義
`chatbot`ノードでは、LLMを使って、受け取った`message`を処理し、返答を生成します。

``` python
def chatbot(state: State):
    return {"messages": [llm_with_tools.invoke(state["messages"])]}
    
# chatbotノード追加する
graph_builder.add_node("chatbot", chatbot)
```
## Toolsノードを定義
- Toolsノードを定義
`Tools`ノードは、利用可能なツールを把握し、それらのツールを実際に呼び出します。この2つの機能を実現するために、`BasicToolNode` クラスでこれを実装します。
- `BasicToolNode`クラス
`__init__`では、`tools` のリストを受け取り、`tools_by_name` という辞書に変換して保存します。
`__call__` メソッドでは、メッセージのリストから最新のmessageを取得し、その中のtool_callに基づいてToolを呼び出します。呼び出し結果をToolMessage形式で返します。
``` python
import json
from langchain_core.messages import ToolMessage

class BasicToolNode:
    """A node that runs the tools requested in the last AIMessage."""

    def __init__(self, tools: list) -> None:
        self.tools_by_name = {tool.name: tool for tool in tools}
    # __call__ メソッドでは、メッセージのリストから最新のmessageを取得し、
    # その中のtool_callに基づいてToolを呼び出します。呼び出し結果をToolMessage形式で返す
    def __call__(self, inputs: dict):
        if messages := inputs.get("messages", []):
            message = messages[-1]
        else:
            raise ValueError("No message found in input")
        outputs = []
        for tool_call in message.tool_calls:
            tool_result = self.tools_by_name[tool_call["name"]].invoke(
                tool_call["args"]
            )
            outputs.append(
                ToolMessage(
                    content=json.dumps(tool_result),
                    name=tool_call["name"],
                    tool_call_id=tool_call["id"],
                )
            )
        # どのツールを使ったかをユーザーに知らせる
        print(f"---{tool_call['name']} was called---")
        return {"messages": outputs}
# tool_node追加する
tool_node = BasicToolNode(tools = [tavily_search,wiki_search])
graph_builder.add_node("tools", tool_node)
```
## Edgeでノード同士を接続
Edgeを定義する方法は2つあります。それぞれ、
`graph_builder.add_edge()`と
`graph_builder.add_conditional_edges()`です。
簡単に言うと、前者は一対一の接続に使用され、後者は分岐がある場合の接続に使用されます。
今回のケースでは、`tools` から `chatbot` への接続、および `start` から `chatbot` への接続は一対一の接続です。しかし、`chatbot` から `tools` や `ends` への接続は一対多の接続です。そのため、`route_tools`という関数を使って、どのルートを使うかを判断する必要があります。
- route_tools
route_tools 関数を使って、`State`の中でメッセージがツールを呼び出しているかどうかを確認します。この関数は、`conditional_edge`で使われます。最後のメッセージにツール呼び出しがある場合は `tools` にルーティングし、そうでない場合は `end` にルーティングします。
``` python
from typing import Literal

def route_tools(
    state: State,
):
    """
    Use in the conditional_edge to route to the ToolNode if the last message
    has tool calls. Otherwise, route to the end.
    """
    if isinstance(state, list):
        ai_message = state[-1]
    elif messages := state.get("messages", []):
        ai_message = messages[-1]
    else:
        raise ValueError(f"No messages found in input state to tool_edge: {state}")
    if hasattr(ai_message, "tool_calls") and len(ai_message.tool_calls) > 0:
        return "tools"
    return END
```
`graph_builder.add_conditional_edges()`で条件付きエッジを定義します。
```python
graph_builder.add_conditional_edges(
    "chatbot",
    route_tools,
    # 呼び出している場合は tools ノードにルーティングされ、
    # 呼び出しがない場合は END にルーティングされる
    {"tools": "tools", END: END},
)
# toolsとchatbot、startとchatbotを繋がる
graph_builder.add_edge("tools", "chatbot")
graph_builder.add_edge(START, "chatbot")
# graphをコンパイル
graph = graph_builder.compile()
```

## State Graphを生成
``` python
from IPython.display import Image, display
display(Image(graph.get_graph().draw_mermaid_png()))
```
![stategraph.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3850641/1f84419f-d108-34f7-fd87-8462d3ccdf51.png)
## チャットボットを実行
``` python
def stream_graph_updates(user_input: str):
    for event in graph.stream({"messages": [("user", user_input)]}):
        for value in event.values():
            print("Assistant:", value["messages"][-1].content)

while True:
    user_input = input("User: ")
    if user_input.lower() in ["quit", "q"]:
        print("Goodbye!")
        break
    stream_graph_updates(user_input)
```
# 動作確認
実際に検索不要な簡単な質問、Tavilyが使用される質問、Wikipediaが使用される質問をそれぞれ投げてみると、結果は以下になります。
## 検索不要な簡単な質問
```
User:  1+1=?

Assistant:  Sure! The answer to 1 + 1 is 2.
```

## Tavilyが使用される質問
```
User:  What's the current temperature in Tokyo?

Assistant: 
---tavily_search_results_json was called---
Assistant: [{"url": "https://www.timeanddate.com/weather/japan/tokyo/ext", "content": "(Weather station: Tokyo, Japan). See more current weather. ... See weather overview. 2 Week Extended Forecast in Tokyo, Japan. Scroll right to see more Conditions Comfort Precipitation Sun; Day Temperature Weather Feels Like Wind Humidity Chance Amount UV Sunrise Sunset; Wed Oct 23: 81 / 62 \u00b0F: Thundershowers. Overcast."}, {"url": "https://www.accuweather.com/en/jp/tokyo/226396/current-weather/226396", "content": "Current weather in Tokyo, Tokyo, Japan. Check current conditions in Tokyo, Tokyo, Japan with radar, hourly, and more."}]
Assistant:  The current temperature in Tokyo is 81°F (27°C).
```
## Wikipediaが使用される質問
```
User:  Search for Tokyo on Wikipedia and provide a brief introduction in one sentence.

Assistant: 
---wikipedia was called---
Assistant: "Page: Tokyo\nSummary: Tokyo (; Japanese: \u6771\u4eac, T\u014dky\u014d, [to\u02d0k\u02b2o\u02d0] ), officially the Tokyo Metropolis (\u6771\u4eac\u90fd, T\u014dky\u014d-to), is the capital of Japan and one of the most populous cities in the world, with a population of over 14 million residents as of 2023 and the second-most-populated capital in the world. The Greater Tokyo Area, which includes Tokyo and parts of six neighboring prefectures, is the most-populous metropolitan area in the world, with 41 million residents as of 2024.\nLocated at the head of Tokyo Bay, Tokyo is part of the Kant\u014d region on the central coast of Honshu, Japan's largest island. Tokyo serves as Japan's economic center and the seat of both the Japanese government and the Emperor of Japan. The Tokyo Metropolitan Government administers Tokyo's central 23 special wards (which formerly made up Tokyo City), various commuter towns and suburbs in its western area, and two outlying island chains known as the Tokyo Islands..."
Assistant:  Tokyo is the capital of Japan and one of the most populous cities in the world, known for its rich history, economic importance, and cultural significance.
```
期待通り、前に生成したState Graphのようにユーザーのプロンプトに基づいて次のアクションを決定できるチャットボットを実現しました。今回作成したチャットボットは、どの検索ツールを使用するか、またはツールを使用せずに応答できるかを自ら判断して、回答を生成することができます。
# 今後について
今回は、LangGraphを使用して、ツールを活用し質問に回答できるチャットボットを構築しました。また、このLangGraphで構築したチャットボットには、1. 過去の会話の文脈を保持し、連続した会話を実現すること、2. エージェントが不安定な場合に人間が手動で入力を補助できること、などの機能も実現可能です。今後も学習を重ね、これらの機能の実現に挑戦していきたいと考えています。

将来、LangGraphを活用したチャットボットには、様々な使用シーンが期待されます。
- 多くのツールを統合し、旅行の計画や各種予約を支援するチャットボット
- コード生成を支援し、自己検証で信頼性の高い出力を行うチャットボット 

など
 
:::note info
これらのユースケースに興味がある方は、以下のリンクをご参照ください。
:::
https://langchain-ai.github.io/langgraph/tutorials/customer-support/customer-support/

# 終わりに
今回は、watsonx.aiとLangGraphを用いて、Web検索ができるかつ状態を持つチャットボットを作成しました。筆者は主にLangGraphの公式ドキュメントを参考にしてコードを作成しました。不足している点がありましたら、ご指摘ください。

この記事が、LangGraphを使用し、チャットボットを構築したいと考えている方に少しでも役立てば幸いです。

### 参考文献
- [LangGraph公式](https://langchain-ai.github.io/langgraph/)
- [Why LangGraph?](https://langchain-ai.github.io/langgraph/concepts/high_level/)
- [LangChain Tavily search](https://python.langchain.com/docs/integrations/tools/tavily_search/)
- [LangChain Wikipedia](https://python.langchain.com/docs/integrations/tools/wikipedia/)