Tool Calling Agent:ToolNode、消息状态与常见踩坑

文章来源声明: 原文作者:Nolla; 来源站点:掘金; 原文链接:https://juejin.cn/post/7687148425100427291; 本文基于上述来源整理/加工,觅优补充点评,仅供技术学习交流。版权归原作者所有。
觅优短评

聚焦 LangGraph 工具调用智能体落地,讲清 ToolNode、消息合并与 set/list 常见错误。适合已上手 LangGraph、准备构建工具调用 Agent 的开发者查漏补缺。

本文目录
1. ToolNode 是什么? ----------------

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 函数。

  1. Agent Loop 的核心逻辑

经典 Tool Calling Agent:

<span>START</span>
  ↓
Agent
  ↓
有 tool_calls?
 ├─ Yes → ToolNode → Agent
 └─ <span>No</span>  → <span>END</span>

这里的 END 是:

模型已经完成这一次最终回答,工作流没有下一步了。

例如用户要求“算 123 + 456,再讲个笑话”:

  1. 第一次 LLM 调用 add(123, 456)
  2. ToolNode 返回 579
  3. 再次进入 LLM。
  4. LLM 读取工具结果,输出“579”,并继续完成“讲笑话”这个要求。
  5. 这一次没有 tool_calls,于是进入 END。

二级结论:工具是 LLM 获取外部信息或执行动作的手段,最终自然语言回答本身仍然是 LLM 的工作。

  1. 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 会负责把新消息合并进旧消息历史,而不是简单覆盖。

  1. 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“新旧值怎么合并”。

  1. BaseMessage 及常见消息类型

BaseMessage 是消息类型的共同基类。

常见类型:

  • HumanMessage:用户消息。
  • AIMessage:模型消息。
  • SystemMessage:系统提示词。
  • ToolMessage:工具执行结果。

因此用户输入应该写成:

HumanMessage(<span>content</span>=user_input)

而不是直接实例化 BaseMessage

  1. 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,例如:

  • 工具叫什么。
  • 参数有哪些。
  • 参数是什么类型。
  • 什么时候应该调用这个工具。
  1. 一个完整的 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>()

  1. 如何调用 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)

  1. 常见错误:{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>

  1. 最终职责图

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 负责“控制流程”。