ToolNode 是 LangGraph 提供的预构建节点类,用来执行 LLM 返回的 tool_calls。
典型流程:
HumanMessage
↓
LLM
↓
<span>AIMessage</span>(tool_calls)
↓
ToolNode
↓
ToolMessage
↓
LLM
↓
最终 AIMessage
关键分工:
- LLM:决定是否需要调用工具,以及调用哪个工具、传什么参数。
- ToolNode:真正执行 Python 工具函数。
- ToolMessage:把工具执行结果放回消息历史。
- Router / conditional edge:决定下一步去 ToolNode 还是 END。
bind_tools()只是把工具说明绑定给模型,并不会自动执行 Python 函数。
- Agent Loop 的核心逻辑
经典 Tool Calling Agent:
<span>START</span>
↓
Agent
↓
有 tool_calls?
├─ Yes → ToolNode → Agent
└─ <span>No</span> → <span>END</span>
这里的 END 是:
模型已经完成这一次最终回答,工作流没有下一步了。
例如用户要求“算 123 + 456,再讲个笑话”:
- 第一次 LLM 调用
add(123, 456)。 - ToolNode 返回
579。 - 再次进入 LLM。
- LLM 读取工具结果,输出“579”,并继续完成“讲笑话”这个要求。
- 这一次没有
tool_calls,于是进入 END。
二级结论:工具是 LLM 获取外部信息或执行动作的手段,最终自然语言回答本身仍然是 LLM 的工作。
return {"messages": [response]}的意义
如果节点中:
response = model.<span>invoke</span>(...)
<span>return</span> {<span>"messages"</span>: [response]}
它的意思是:
把本次 LLM 返回的
AIMessage作为messages字段的更新结果交给 LangGraph。
response 是单条消息,而 messages 是消息序列,因此要写:
{<span>"messages"</span>: [<span>response</span>]}
而不是:
{<span>"messages"</span>: <span>response</span>}
LangGraph 的节点不一定要返回完整 State,可以只返回需要更新的字段。
如果 State 定义为:
<span>class</span> <span>AgentState</span>(TypedDict):
messages: Annotated[list[BaseMessage], add_messages]
那么 add_messages 会负责把新消息合并进旧消息历史,而不是简单覆盖。
- Annotated 与 Sequence
Sequence
Sequence<span>[BaseMessage]</span>
表示“一个按顺序排列、元素类型为 BaseMessage 的序列”。
它比 list 更抽象,可以表示 list、tuple 等序列类型。
但如果代码里明确需要列表操作,或者写:
<span>[system_prompt]</span> + state<span>[<span>"messages"</span>]</span>
那么直接使用:
list<span>[BaseMessage]</span>
会更直观,也避免 list + tuple 这样的类型问题。
Annotated
Annotated<span>[类型, 附加信息]</span>
用于给一个类型附加额外元数据。
LangGraph 中:
messages: Annotated<span>[list[BaseMessage]</span>, add_messages]
可以拆成:
list[BaseMessage]:这个字段的数据类型。add_messages:这个字段收到新值时的合并规则,也就是 reducer。
二级结论:类型告诉 LangGraph“这里装什么”,reducer 告诉 LangGraph“新旧值怎么合并”。
- BaseMessage 及常见消息类型
BaseMessage 是消息类型的共同基类。
常见类型:
HumanMessage:用户消息。AIMessage:模型消息。SystemMessage:系统提示词。ToolMessage:工具执行结果。
因此用户输入应该写成:
HumanMessage(<span>content</span>=user_input)
而不是直接实例化 BaseMessage。
- Tool 定义时最好标注参数类型
推荐:
<span>@tool</span>
<span>def</span> <span>add</span>(<span>a: <span>float</span>, b: <span>float</span></span>) -> <span>float</span>:
<span>"""计算两个数字的和。"""</span>
<span>return</span> a + b
参数类型和 docstring 都会帮助模型理解工具 schema,例如:
- 工具叫什么。
- 参数有哪些。
- 参数是什么类型。
- 什么时候应该调用这个工具。
- 一个完整的 Tool Calling Agent 骨架
<span>from</span> typing <span>import</span> TypedDict, Annotated
<span>from</span> dotenv <span>import</span> load_dotenv
<span>from</span> langchain_core.messages <span>import</span> BaseMessage, SystemMessage, HumanMessage
<span>from</span> langchain_core.tools <span>import</span> tool
<span>from</span> langgraph.graph <span>import</span> StateGraph, START, END
<span>from</span> langgraph.graph.message <span>import</span> add_messages
<span>from</span> langgraph.prebuilt <span>import</span> ToolNode
<span>from</span> langchain_mistralai <span>import</span> ChatMistralAI
load_dotenv()
<span>class</span> <span>AgentState</span>(<span>TypedDict</span>):
messages: Annotated[<span>list</span>[BaseMessage], add_messages]
<span>@tool</span>
<span>def</span> <span>add</span>(<span>a: <span>float</span>, b: <span>float</span></span>) -> <span>float</span>:
<span>"""计算两个数字的和。"""</span>
<span>return</span> a + b
<span>@tool</span>
<span>def</span> <span>subtract</span>(<span>a: <span>float</span>, b: <span>float</span></span>) -> <span>float</span>:
<span>"""计算两个数字的差。"""</span>
<span>return</span> a - b
tools = [add, subtract]
model = ChatMistralAI(
model=<span>"ministral-3b-2512"</span>
).bind_tools(tools)
<span>def</span> <span>model_call</span>(<span>state: AgentState</span>):
system_prompt = SystemMessage(
content=<span>"你是一个专业处理数学计算问题的工程师,请尽你最大的能力回答我的要求。"</span>
)
response = model.invoke(
[system_prompt] + state[<span>"messages"</span>]
)
<span>return</span> {
<span>"messages"</span>: [response]
}
<span>def</span> <span>route_tools</span>(<span>state: AgentState</span>):
last_message = state[<span>"messages"</span>][-<span>1</span>]
<span>if</span> last_message.tool_calls:
<span>return</span> <span>"tools"</span>
<span>return</span> <span>"end"</span>
tool_node = ToolNode(tools)
graph = StateGraph(AgentState)
graph.add_node(<span>"agent"</span>, model_call)
graph.add_node(<span>"tools"</span>, tool_node)
graph.add_edge(START, <span>"agent"</span>)
graph.add_conditional_edges(
<span>"agent"</span>,
route_tools,
{
<span>"tools"</span>: <span>"tools"</span>,
<span>"end"</span>: END
}
)
graph.add_edge(<span>"tools"</span>, <span>"agent"</span>)
agent = graph.<span>compile</span>()
- 如何调用 Agent
单轮调用:
<span>user_input</span> = input(<span>"Enter: "</span>)
<span>result</span> = agent.invoke({
"messages": <span>[
HumanMessage(content=user_input)
]</span>
})
print(result<span>["messages"]</span><span>[-1]</span>.content)
- 常见错误:
{HumanMessage(...)}
错误:
"messages": {HumanMessage(<span>content</span>=user_input)}
这里的 {...} 在 Python 中表示 set,不是 list。
set 中的元素必须可哈希,而 HumanMessage 不是可哈希对象,因此会报:
TypeError: unhashable <span>type</span>: <span>'HumanMessage'</span>
正确写法:
<span>"messages"</span><span>:</span> <span>[</span>HumanMessage(content=user_input)<span>]</span>
记忆:
[<span>x</span>] <span># list</span>
{x} <span># set</span>
- 最终职责图
AgentState
│
├─ messages
│ ├─ HumanMessage
│ ├─ AIMessage
│ └─ ToolMessage
│
↓
Agent <span>/</span> LLM 决策下一步
↓
route_tools 负责路由
↓
ToolNode 执行 Python 工具
↓
add_messages 合并消息历史
↓
Agent
↓
<span>END</span>
总结
LangGraph Tool Calling Agent 的核心,就是让 LLM 负责“决定”,ToolNode 负责“执行”,State 负责“保存上下文”,conditional edge 负责“控制流程”。
聚焦 LangGraph 工具调用智能体落地,讲清 ToolNode、消息合并与 set/list 常见错误。适合已上手 LangGraph、准备构建工具调用 Agent 的开发者查漏补缺。