LangGraphで AIエージェントをまなんでいく - その4 一連のプロセスを繰り返す-
前回の記事でLangGraphを使ってLLMアプリケーションでのルーターの機能を実装して見ました。
ルーターはモデル(LLM)が一度の意思決定で「1つのツールを選択して実行」し、その結果を返すシンプルな仕組みです。
今回はさらに柔軟な動作ができるように拡張する方法を学んでいきます。
これは__ReAct__というエージェントアーキテクチャで、LLMが「ツールの呼び出し」と「ツールの結果に基づいた次のアクションの選択」を組み合わせて、動的かつ汎用的な意思決定を行う仕組みのことです。
ReActがどういった動きをするのかを簡単にまとめると、
-
Act(ツールの呼び出し)
モデルは、ユーザーの入力を解析し、どのツールを呼び出すべきかを決定します。
例: ユーザーが「2つの数値を掛け算して」と入力した場合、モデルは「掛け算ツール」を選択します。
-
Observe(ツールの出力の観察)
ツールの実行結果をモデルに戻します。モデルはこの結果を「観察」し、次に何をするかを決定します。例: ツールから結果(例えば、6 x 7 = 42)を受け取ります。
-
Reason(ツール結果に基づいた推論)
モデルはツールの結果を基に、次の行動を決定します:-
別のツールを呼び出す。
-
結果をそのままユーザーに返す。
例: 次のツールを呼び出す: 「掛け算結果をさらに加算するツールを呼び出す」。
応答を返す: 「計算結果は42です」とユーザーに応答。
-
langchain-academy の講座では以下のような図も示されていました。イメージしやすいと思います。
ReActアーキテクチャでのエージェント作成
ツール
LLMを作成し、掛け算、足し算、割り算を行う関数を用意し、ツールとして利用します。
from langchain_google_genai import ChatGoogleGenerativeAI
def multiply(a: int, b: int) -> int:
"""Multiply a and b.
Args:
a: first int
b: second int
"""
return a * b
# This will be a tool
def add(a: int, b: int) -> int:
"""Adds a and b.
Args:
a: first int
b: second int
"""
return a + b
def divide(a: int, b: int) -> float:
"""Divide a and b.
Args:
a: first int
b: second int
"""
return a / b
tools = [add, multiply, divide]
llm = ChatGoogleGenerativeAI(model="gemini-1.5-flash")
llm_with_tools = llm.bind_tools(tools)
LLMを作成し、エージェントの全体的な望ましい行動をプロンプトします。
from langgraph.graph import MessagesState
from langchain_core.messages import HumanMessage, SystemMessage
# System message
sys_msg = SystemMessage(content="あなたは、一連の入力に対して算術演算を実行するタスクを与えられた親切なアシスタントでぇす。")
# Node
def assistant(state: MessagesState):
return {"messages": [llm_with_tools.invoke([sys_msg] + state["messages"])]}
LangGraphのグラフを作成します。
from langgraph.graph import START, StateGraph
from langgraph.prebuilt import tools_condition
from langgraph.prebuilt import ToolNode
from IPython.display import Image, display
# Graph
builder = StateGraph(MessagesState)
# ノードの定義。このノードが実行されます
builder.add_node("assistant", assistant)
builder.add_node("tools", ToolNode(tools))
# エッジの定義:制御フローがどのように動くかを決める。
builder.add_edge(START, "assistant")
builder.add_conditional_edges(
"assistant",
# アシスタントからの最新のメッセージ(結果)がツールコールである場合 -> tools_conditionはtoolsにルーティングする。
# アシスタントからの最新のメッセージ(結果)がツールコールでない場合 -> tools_conditionはENDにルーティングする。
tools_condition,
)
builder.add_edge("tools", "assistant")
react_graph = builder.compile()
# Show
display(Image(react_graph.get_graph(xray=True).draw_mermaid_png()))
メッセージを与えてどんな結果になるのか見て見ました。
そこそこ厳密にしたメッセージのつもりです。
messages = [HumanMessage(content="3と4を足す。その出力に2を掛ける。 さらにその出力を5で割る。")]
messages = react_graph.invoke({"messages": messages})
for m in messages['messages']:
m.pretty_print()
アシスタントがメッセージの内容を理解してAI Message でツールコールを返していますね。
その後ツールが実行されて結果が正しく返ってきました。
messages = [HumanMessage(content="大当たり確率が1/199の台で、100回転以内に当たりが引ける期待値は?")]
messages = react_graph.invoke({"messages": messages})
システムメッセージに与えた役割とは関係のない質問をすると、
このようにツールコールではなく、LLM自体が直接応答してくれました。
ReActアーキテクチャ
- act - モデルが特定のツールを呼び出す
- observe - ツールの出力をモデルに返す。
- reason - ツールの出力をモデルに推論させ、次に何をすべきかを決定させる(例えば、別のツールを呼び出すか、直接応答するか)。
の動作をLangGraphを使って実装してみました。
このアーキテクチャは動的にタスクを解析して実行する必要がある計算や操作を行う際に非常に効果的です。
LangGraphのループ構造を使うことで、柔軟かつ効率的な処理が可能になるので、実際の業務の置き換え手に使えるかもしれませんね。