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