上一讲的 Chain 只会把工具调用请求"带回来",这一讲让工具真正被执行。
LLM 扮演路由器,决定是直接回答还是调用工具。
回顾上一讲(Chain)的图:START → tool_calling_llm → END。当你问"Multiply 2 and 3",LLM 返回了一个带 tool_calls 的 AIMessage,但图直接走到 END 了——工具请求就躺在 State 里没人理。
Chain 图只有一个节点。LLM 说"我要调用工具",但图没有地方去执行这个调用。用户得到的是一个"工具调用请求",而不是最终答案。
Router 要解决的就是这个问题,需要新增两个东西:
Router 这个名字来自网络路由器的比喻——根据条件决定数据包走哪条路。在这里,LLM 就是路由器:
Router 是最简单的 Agent 形态:LLM 根据用户意图,自主决定是否调用工具。"LLM 控制执行流程"——这就是 Agent 的定义。注意此时工具执行完毕后直接结束,不会把结果再传回给 LLM。下一讲(Agent)才会实现完整的"思考→工具→思考"循环。
ToolNode 是 LangGraph 内置的预构建节点,专门用来执行 LLM 请求的工具调用。你不需要自己写执行逻辑:
from langgraph.prebuilt import ToolNode
# 定义工具函数
def multiply(a: int, b: int) -> int:
"""Multiply a and b."""
return a * b
# ToolNode 接收工具列表,自动处理执行逻辑
tool_node = ToolNode([multiply])
# 注册为图的节点
builder.add_node("tools", tool_node)
ToolNode 从 State 里取出最新的 AIMessage,读取其中的 tool_calls,然后:
multiply)multiply(a=2, b=3))ToolMessage 追加到 messages# ToolNode 执行后,State 里的 messages 变成:
# [
# HumanMessage("Multiply 2 and 3"),
# AIMessage(tool_calls=[{name:"multiply", args:{a:2,b:3}}]),
# ToolMessage(content="6", name="multiply", tool_call_id="call_xxx") ← 新增
# ]
如果 LLM 在一次回复里请求了多个工具调用(parallel tool calling),ToolNode 会并行执行所有工具,提高效率。
tools_condition 是 LangGraph 内置的路由函数,专门用于判断"LLM 有没有请求工具":
from langgraph.prebuilt import tools_condition
# tools_condition 的等效手写版本(帮助你理解它做了什么):
def tools_condition(state: MessagesState):
last_message = state["messages"][-1] # 取最新的消息
if last_message.tool_calls: # 有工具调用请求?
return "tools" # → 去 tools 节点
return END # → 结束
# 用法:作为条件边的路由函数
builder.add_conditional_edges(
"tool_calling_llm", # 从这个节点出发
tools_condition, # 用这个函数决定去哪
# 可选:明确指定可能的路由目标(提高可读性)
# {"tools": "tools", END: END}
)
tools_condition 就是我们在 1-1 里学到的"路由函数(decide_mood)"的生产级版本。它做的事情完全一样:读 State,返回下一个节点名。
| LLM 回复类型 | tools_condition 返回 | 下一步 |
|---|---|---|
| AIMessage(纯文字) | "__end__" | 直接结束,返回给用户 |
| AIMessage(含 tool_calls) | "tools" | 去 ToolNode 执行工具 |
from langgraph.graph import StateGraph, MessagesState, START, END
from langgraph.prebuilt import ToolNode, tools_condition
# ─── 工具定义 ───
def multiply(a: int, b: int) -> int:
"""Multiply a and b."""
return a * b
# ─── LLM 绑定工具 ───
llm_with_tools = llm.bind_tools([multiply])
# ─── LLM Node ───
def tool_calling_llm(state: MessagesState):
return {"messages": [llm_with_tools.invoke(state["messages"])]}
# ─── 构建图 ───
builder = StateGraph(MessagesState)
builder.add_node("tool_calling_llm", tool_calling_llm) # LLM 节点
builder.add_node("tools", ToolNode([multiply])) # 工具执行节点
builder.add_edge(START, "tool_calling_llm") # 入口
builder.add_conditional_edges( # 条件分支
"tool_calling_llm",
tools_condition, # 有工具调用→tools,没有→END
)
builder.add_edge("tools", END) # 工具执行完→结束
graph = builder.compile()
messages = [HumanMessage(content="Hello, how are you?")]
result = graph.invoke({"messages": messages})
# 执行路径:
# START → tool_calling_llm(LLM 直接回复,无 tool_calls)
# → tools_condition 返回 END
# → 结束
#
# 最终 messages:
# [HumanMessage("Hello, how are you?"), AIMessage("Hi! I'm fine...")]
messages = [HumanMessage(content="What is 2 multiplied by 3?")]
result = graph.invoke({"messages": messages})
# 执行路径:
# START → tool_calling_llm(LLM 请求工具)
# → tools_condition 返回 "tools"
# → ToolNode 执行 multiply(2, 3) = 6
# → END
#
# 最终 messages:
# [
# HumanMessage("What is 2 multiplied by 3?"),
# AIMessage(tool_calls=[{name:"multiply",args:{a:2,b:3}}]),
# ToolMessage(content="6", name="multiply") ← 工具执行结果
# ]
Router 执行了工具,但把 ToolMessage("6") 直接返回给用户,而不是再让 LLM 解读这个结果然后用自然语言回答。"6" 只是工具的原始输出。
真正的用户友好体验是:LLM 看到工具结果后,用自然语言说"2 乘以 3 等于 6"。这就是下一讲 Agent 要做的事——把 ToolMessage 再传回给 LLM。
| 组件 | 类型 | 作用 | 等价于 |
|---|---|---|---|
ToolNode | 预构建 Node | 自动执行 LLM 请求的工具调用 | 自己写一个解析 tool_calls 并执行的函数 |
tools_condition | 预构建路由函数 | 判断是否有工具调用,返回目标节点名 | 自己写 decide_mood 风格的条件函数 |
ToolNode 和 tools_condition 是 LangGraph 提供的"标准零件",帮你少写样板代码。但你完全可以不用它们,自己实现等价功能——理解了 1-1 的图构建模式,就能看透这两个组件在做什么。