LangGraphを試した②　~LLM利用編~

# はじめに
最近 LangGraph を触っています。なんとなくサンプルのコードを書き換えたりして使っていますが理解できていないまま使っている部分も多く感じているので、理解を深めるために自分なりに整理して書いていきたいと思います。コードなどは[公式サイト](https://langchain-ai.github.io/langgraph/)を参考にしています。
今回は LLM の利用についてです。

①はこちら

https://qiita.com/ssc-ymuramatsu/items/af4bf120b8be2076b66d

# 本記事の動作環境
本記事のコードは次の環境で動作確認をしています。
```
langchain==0.2.6
langchain-community==0.2.6
langchain-core==0.2.41
langgraph==0.2.26
```
LangChain や LangGraph は更新が続いているパッケージなので、バージョンアップすると動かなくなる恐れがあります。筆者の環境でもパッケージのバージョンを上げた結果 pydantic の v1 と v2 の互換性でエラーが出ました。

# 前提
OpenAI の API を利用しますので、環境変数`OPENAI_API_KEY`にAPIキーを設定してください。
また今回のコードは以下の`State`を利用したグラフを前提としています。
```py
from typing_extensions import Annotated, TypedDict
from langgraph.graph.message import AnyMessage, add_messages

class State(TypedDict):
    messages: Annotated[list[AnyMessage], add_messages]
```
ノードの戻り値の dict の`messages`が`State`に追記されます。

# ノードでの利用

## 文章生成
LLM を利用して、文章を生成してもらいます。

```py
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder

def food_node(state):
    llm = ChatOpenAI(model="gpt-4o")
    prompt = ChatPromptTemplate.from_messages(
        [
            (
                "system",
                "あなたはおすすめの食事に関するAIエージェントです。ユーザーの入力に対して適切な回答を返してください。"
            ),
            MessagesPlaceholder(variable_name="messages")
        ]
    )
    agent = prompt | llm
    result = agent.invoke(state)
    return {"messages": result}
```
`MessagesPlaceholder(variable_name="messages")`により`state`から過去の`messages`を取り出して LLM に送り、回答を`state`の`messages`に追記しています。

## Tool 利用
LLM に関数を呼び出させることができます。利用する関数の選択、引数の生成を自動で行ってくれます。今回は公式がすでに用意している、Wikipedia の情報を取得する Tool を利用します。
利用には`wikipedia`パッケージをインストールする必要があります。
```sh
pip install wikipedia
```
Tool の list と Tool を実行するノードを定義します。
```py
from langchain_community.tools.wikipedia.tool import WikipediaQueryRun
from langchain_community.utilities import WikipediaAPIWrapper
from langgraph.prebuilt import ToolNode

wikipedia = WikipediaQueryRun(api_wrapper=WikipediaAPIWrapper())
tools = [wikipedia]
tool_node = ToolNode(tools)
```
Tool を呼び出すエージェントノードを作成します。
```py
def manga_node(state):
    llm = ChatOpenAI(model="gpt-4o")
    prompt = ChatPromptTemplate.from_messages(
        [
            (
                "system",
                "あなたは漫画に関する情報を回答するAIエージェントです。Toolを利用してユーザーの入力に対して適切な回答を返してください。"
            ),
            MessagesPlaceholder(variable_name="messages")
        ]
    )
    agent = prompt | llm.bind_tools(tools)
    result = agent.invoke(state)
    return {"messages": result}
```
`ChatModel.bind_tool()`を利用することによって Tool を紐づけることができます。Tool 呼び出しが必要なときは呼び出す Tool やその引数の情報を返し、呼び出しが不要なときは普通に回答を生成します。

# エッジでの利用
LLM をエッジで利用することで、次のノードを LLM に決定してもらうことができます。
```py
from pydantic import BaseModel
from typing import Literal
def food_or_manga_router(state):
    class routeResponse(BaseModel):
        next: Literal["food", "manga", "end"]

    prompt = ChatPromptTemplate.from_messages(
        [
            (
                "system",
                "あなたは次のノードを決定するAIエージェントです。ユーザーの入力から適切な次のノードを選択してください。"
                "次のノードには: food, manga"
                "があります。まだどれにも当てはまらない場合はendを選択してください。"
            ),
            MessagesPlaceholder(variable_name="messages")
        ]
    )
    llm = ChatOpenAI(model="gpt-4o")
    router_chain = (
        prompt
        | llm.with_structured_output(routeResponse)
    )
    result = router_chain.invoke(state)
    print("next: ", result["next"])
    return result["next"]
```
ここでは次のノードを`"food"`, `"manga"`, `"end"`から選ばせています。`ChatModel.with_structured_output()`を利用することによって出力をスキーマに従って構造化することができます。今回のような、余計な出力はせずに選択肢から選んで欲しいときは`Literal`型でスキーマを定義するのが便利です。

# 実行
これまでのコードを実行してみましょう。グラフを定義していきます。
```py
from typing_extensions import Annotated, TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import AnyMessage, add_messages
from langgraph.prebuilt import tools_condition

class State(TypedDict):
    messages: Annotated[list[AnyMessage], add_messages]

graph = StateGraph(State)

graph.add_node("food", food_node)
graph.add_node("manga", manga_node)
graph.add_node("tools", tool_node)

graph.add_conditional_edges(
    START,
    food_or_manga_router,
    {
        "food": "food",
        "manga": "manga",
        "end": END
    }
)

graph.add_edge("food", END)
graph.add_conditional_edges(
    "manga",
    tools_condition,
    {
        "tools": "tools",
        "__end__": END
    }
)
graph.add_edge("tools", "manga")

app = graph.compile()
```
`tools_condition`は LangGraph が提供するルーティング関数で、`tool_calls`があるときは`"tools"`を返し、ない場合は`"__end__"`を返す関数です。Tool 呼び出しを行うエージェントノードから Tool ノードはこの関数を使ってつなぎましょう。

以下のようなグラフができました。
![qiita2_graph.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3657697/2f17e55e-f32b-7793-dd00-254a1eb7acba.png)

- 食べ物に関する話題なら、food ノードでおすすめの食事について回答
- 漫画に関する話題なら、manga ノードでwikipediaの情報を使って回答
- その他の話題なら何もせず終了

するようなグラフが作成できました。

```py
events = app.stream({"messages": [("user", "ほうれん草を使ったものが食べたい")]})

for e in events:
    print(e)

# food
# {'food': {'messages': AIMessage(content='ほうれん草を使った料理はたくさんありますが、いくつかおすすめを挙げてみますね！\n\n1. **ほうれん草とベーコンのキッシュ**\n   - サクサクのパイ生地に、ほうれん草とベーコン、チーズをたっぷり使ったキッシュは、おもてなしにもぴったりです。\n\n2. **ほうれん草のゴマ和え**\n   - シンプルながらも風味豊かなゴマ和えは、ほうれん草をさっと茹でてゴマと醤油で和えるだけ。お弁当のおかずにもぴったりです。\n\n3. **ほうれん草とチーズのオムレツ**\n   - ほうれん草を炒めてから卵と合わせ、チーズをたっぷり入れると、とろりとした食感が楽しめるオムレツができます。\n\n4. **ほうれん草とトマトのパスタ**\n   - ほうれん草とトマト、にんにくをオリーブオイルで炒めて作るパスタは、シンプルながらも栄養満点で美味しいです。\n\n5. **ほうれん草のクリームスープ**\n   - ほうれん草と玉ねぎ、じゃがいもを煮込んでミキサーにかけ、クリームと合わせれば、濃厚で温かなスープが完成します。\n\nどれも簡単に作れるので、ぜひ試してみてくださいね！どの料理がお好みですか？', ...}}
```
```py
events = app.stream({"messages": [("user", "ワンピースの単行本の最新刊は何巻？")]})

for e in events:
    print(e)

# next:  manga
# {'manga': {'messages': ...}}
# {'tools': {'messages': ...}}
# {'manga': {'messages': AIMessage(content='「ワンピース」の最新の単行本は第109巻です（2024年7月時点）。', ...)}}
```
```py
events = app.stream({"messages": [("user", "LangGraphの魅力は？")]})

for e in events:
    print(e)

# next:  end
```
Tool の利用やルーティングが意図したとおりに動いていますね。

# おわりに
LLM を利用してルーティングしたりToolを利用したりすることができました。次回は Tool の作成について説明したいと思います。

# 参考
[LangGraph Tutorials](https://langchain-ai.github.io/langgraph/tutorials/)