LangGraph マルチエージェントのチュートリアルを動かしてみる
エージェントツールの開発がLangChain からLangGraph に移行するので、LangGraph の理解も重要になっている。
単にLangGraph を使いたいだけなら、LangGraph のチュートリアルを動かしてみると良い。が、LangGraph のメリットはやはり、複雑なエージェント間の制御を可能にすることだと思う。
今回はPython 版のMultiagent Workflow のチュートリアルのうち、multi-agent-collaborationを動かしてみたので、その備忘録。これを理解していれば残りもまあ問題ないと思われる。(JavaScript 版は触ったことないが...)
チュートリアルの解説
Multi-agent network
「divide-and-conquer」は「分割統治法」。大学の授業とかで耳にしてたら以下イメージしやすいかも。
Create Agents
MessagesPlaceholder
プロンプトの一部(コンテクストや質問文など)はRAG でよく変数化するが、プロンプトごと変数化するのがMessagesPlaceholder 。
今回の場合、変数messages の中身が全てLLM に入力されることになる。
参考) LangChain API Reference:MessagesPlaceholder
prompt.partial()
prompt の引数の一部を設定してしまう処理。以下のコードを読んだらイメージしやすいかも。
(※ 内部のコード見るとfunctools.partial は使って無さそうだったので、あくまでイメージまで)
from functools import partial
def echo(a: str, b: str):
print(f"{a}, {b}!")
echo2 = partial(echo, a="Hello")
echo2(b="World") #Hello, World!
llm.bind_tools()
LLM にツール群を与えて、どのツールを使い & どんな引数を入力すべきかを答えさせる(→ エージェント化する)ことが出来る。
実際にそのツールを実行するためには、 そのための処理が別途必要 なのに注意。
参考) LangChain 公式ドキュメント:tool calling
Define tools
デコレータ@tool により、定義した関数をツールに出来る。LLM がツールの詳細を把握するため、description やannotaion が必要。
from langchain_core.tools import tool
from typing import Annotated
@tool
def double(a: Annotated[int, "2倍したい整数"] ):
"""入力された整数を2倍した値を返します。"""
return 2 * a
print(double.name) #double
print(double.description) #入力された整数を2倍した値を返します。
print(double.args) #{'a': {'description': '2倍したい整数', 'title': 'A', 'type': 'integer'}}
Create graph
Define State
LangGraph の肝は「状態, State」であると思う。本記事では以下、「State」 はLangGraph 一般における"状態"を指すものとする。
AgentState はTypedDict 型を継承しているので、{"messages" : ..., "sender" : ...} のような辞書と考えれば良い。
class AgentState(TypedDict):
messages: Annotated[Sequence[BaseMessage], operator.add]
sender: str
ただしState の annotation には特別な意味がある のに注意。
-
AgentState.messages(BaseMessageのシーケンス型)は、operator.addがアノテーションされているため、State 更新時、 新規メッセージがシーケンスの末尾に追加される 挙動になる。 -
AgentState.senderにはアノテーションが無いため、State の更新時に置換される挙動になる。
Define Agent Nodes
LangGraph において、頂点(ノード)はState のみを引数とし、新たなState を返す関数である。ノードに到達したとき、その関数の処理が行われる。
(agent_node 関数はstate 、agent 、 name の3つの引数が必要だが、実際にノードとして設定する際はagent とname をfunctools.partial で設定してしまうので問題ない)
正確には、ノードは
RunnableLikeであれば良い。すなわちエージェントでも、LangGraph そのものでも良い。
hierarchical_agent_teams でやってるのがそれ。
agent_node()
agent_node 関数の処理は以下の通り。
-
agentにstateをinvoke した結果を得る。 -
- で得られた結果に対して、出力結果が
ToolMessage型ならpass 。そうでない場合、typeを削除、nameを書き換えてAIMessage型を再作成する。
- で得られた結果に対して、出力結果が
from langchain_core.messages import HumanMessage, AIMessage
result = HumanMessage(content='', name='test', type='human')
print(AIMessage(**result.dict(exclude={'name', 'type'}))) #content='' additional_kwargs={} response_metadata={}
※ 今回は
research_nodeとchart_nodeしか作成しない都合上、1. の結果はAIMessage型以外有り得ない。
そのためToolMessage型の分岐・typeの削除処理は不要だが、汎用性を考慮してこのような分岐を設定していると思われる。
- "messages"=[2. の結果]、 "sender"=エージェント名 とした辞書を返す。
State のannotation から、返された辞書の"messages" の中身がAgentState.messages の末尾に追加されることに注意。AgentState.sender は返された辞書の"sender" の中身に置き換わる。
Define Tool Node
llm.bind_tools() だとツールを選ぶだけなので、そのツールを実行するためのノードを作成している。ToolNode 便利
Define Edge Logic
LangGraph の複雑な分岐処理を可能にするのがRouter である。
Router はState を引数として文字列を返す関数で、後述するadd_conditional_edges と合わせて分岐が決定する。
今回のrouter は以下のような処理になる。
-
stateのmessagesシーケンスのうち、最後尾(=直近のノードで行われた処理の結果)を見る。 - それに
tool_callsが含まれる場合、LLM がツールを使うべきと判断したわけなので、ツールを実行するノードの名前"call_tool"を返す。 - それの出力に"FINAL ANSWER" が含まれる場合、LLM が全ての処理を終了したと判断したわけなので、処理終了ノードを返す。
- どちらでもない場合、処理続行として、
"continue"を返す。
Define the Graph
いよいよグラフを定義する。
add_node()
これはグラフにノード(ノード名と関数)を追加するだけ。
add_edge()
グラフに辺(エッジ)を追加する。
有向グラフであるのに注意。add_edge(A, B) だとA → B のようなエッジになる。
add_conditional_edges()
グラフの分岐を定義する。引数は、(ノード名、router、辞書) のような形式になる。
- そのノードに到達し、ノードの関数を実行してState が更新される。
- router にState を入力した結果を得る。
- 辞書において、2. の結果をキーとしたときのvalue が、次のノードになる。
ノードcall_tool の遷移先は、必ず その直前のノードになる ことに注意。State の"sender" の使い道はここ。
workflow.add_conditional_edges(
"call_tool",
lambda x: x["sender"],
{
"Researcher": "Researcher",
"chart_generator": "chart_generator",
},
)
実行例
理解は難しいと思うので、チュートリアルを実際に実行した結果を貼っておく。
ただしチュートリアルどおりだと動作しない(イギリスのGDP の正確なデータが入手できずにResearcher が途中で投げてしまい、chart_generator の手にも負えないため、エージェント間の盥回しが無限ループする)ことがあったので、適当に質問文を変えています。あと3期面白いし
また途中経過がよく分からないので、pretty_print() 使って確認するのがおすすめ。
for event in graph.stream(
{
"messages": [
HumanMessage(
content="<グラフへの入力>"
)
],
},
# Maximum number of steps to take in the gra
{"recursion_limit": 150},
stream_mode="values"
):
if "messages" in event:
event["messages"][-1].pretty_print()
グラフへの入力
Liella! のメンバー人数を縦軸、2020年,2021年,2022年,2023年を横軸としたmatplotlib グラフを出力してください。ただし、グラフのタイトルと軸は英語で記述すること。
Researcher(1回目)
call_tool(tavily_tool) へ遷移。Args はLiella! メンバー人数 2020 2021 2022 2023
call_tool(1回目):tavily_tool
[{"url": "https://www.lovelive-anime.jp/yuigaoka/cast/", "content": "Liella!(リエラ)キャスト紹介 | ラブライブ!スーパースター!! Liella!リエラ 音楽CDのリリース、アニメーション、書籍、スマートフォン向けアプリなど、オールメディアで展開をおこなうプロジェクト・ラブライブ!シリーズ。その第4作となる「ラブライブ!スーパースター!!」から生まれた11人組のスクールアイドルグループです。...(以下略)}]
→ 直前のノードResearcher に戻る。
Researcher(2回目)
Liella! のメンバー人数について以下の情報が得られました:
- 2020年: 5人
- 2021年: 5人(2021年4月にデビュー)
- 2022年: 9人(2期生4人を追加)
- 2023年: 11人(3期生2人を追加)
これらの情報を基に、matplotlibを使ってグラフを生成します。
→ call_tool(tavily_tool) へ遷移。Args はmatplotlib グラフ作成方法
call_tool(2回目):tavily_tool
[{"url": "https://www.yutaka-note.com/entry/matplotlib_pyplot", "content": "mat-plotlib.pyplotモジュールを使用してグラフを作成する方法をまとめています。 pyplotでは手軽に手早く簡単にグラフ作成することが可能です! グラフを作成、 グラフのスタイル変更、 タイトルや軸の設定など基本事項について徹底解説しています!"}, {"url": "https://www.tech-teacher.jp/blog/python-library19-pyplot/", "content": "Matplotlib でグラフを作成するには主に『pyplotインターフェース』を用いる方法、『オブジェクト指向インターフェース』を用いる方法の2つがあります。. 本記事では『pyplotインターフェース』の使い方と関数について解説します。. もう一つのオブジェクト"}]
→ 直前のノードResearcher に戻る。
Researcher(3回目)
2回目のtavily_tool では有益な情報が得られなかったが、LLM の前提知識としてコードを書いてしまったらしい。
Matplotlib の pyplot モジュールを使用して、Liella! のメンバー人数の推移を示すグラフを作成します。以下はそのコード例です。
'''python
import matplotlib.pyplot as plt
# データ
years = [2020, 2021, 2022, 2023]
members = [5, 5, 9, 11]
# グラフの作成
plt.figure(figsize=(8, 6))
plt.plot(years, members, marker='o')
# タイトルとラベルの設定
plt.title('Number of Liella! Members Over Years')
plt.xlabel('Year')
plt.ylabel('Number of Members')
# グリッドの表示
plt.grid(True)
# グラフの表示
plt.show()
'''
このコードを実行すると、2020年から2023年にかけてのLiella! のメンバー人数の変化を示す折れ線グラフが表示されます。
→ tool_calls も"FINAL ANSWER" も出力されなかったため、chart_generator へ遷移。
chart_generator(1回目)
call_tool(python_repl) へ遷移。Args は上記のコードの中身
call_tool(3回目):python_repl
Successfully executed:
'''python
import matplotlib.pyplot as plt
# Data
years = [2020, 2021, 2022, 2023]
members = [5, 5, 9, 11]
# Create the plot
plt.figure(figsize=(8, 6))
plt.plot(years, members, marker='o')
# Set title and labels
plt.title('Number of Liella! Members Over Years')
plt.xlabel('Year')
plt.ylabel('Number of Members')
# Show grid
plt.grid(True)
# Display the plot
plt.show()
'''
Stdout:
If you have completed all tasks, respond with FINAL ANSWER.

→ 直前のノードchart_generator に戻る。
chart_generator(2回目)
Here is the matplotlib graph showing the number of Liella! members from 2020 to 2023:
- **Title**: Number of Liella! Members Over Years
- **X-axis**: Year (2020, 2021, 2022, 2023)
- **Y-axis**: Number of Members
The graph clearly illustrates the increase in the number of Liella! members over these years.
FINAL ANSWER
→ "FINAL ANSWER" が出力されたため、終了ノードEND へ遷移。
Discussion