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

1. ToolNode 是什么?

ToolNode 是 LangGraph 提供的预构建节点类,用来执行 LLM 返回的 tool_calls。

典型流程:

scss 复制代码
HumanMessage
    ↓
LLM
    ↓
AIMessage(tool_calls)
    ↓
ToolNode
    ↓
ToolMessage
    ↓
LLM
    ↓
最终 AIMessage

关键分工:

  • LLM:决定是否需要调用工具,以及调用哪个工具、传什么参数。
  • ToolNode:真正执行 Python 工具函数。
  • ToolMessage:把工具执行结果放回消息历史。
  • Router / conditional edge:决定下一步去 ToolNode 还是 END。

bind_tools() 只是把工具说明绑定给模型,并不会自动执行 Python 函数。

2. Agent Loop 的核心逻辑

经典 Tool Calling Agent:

sql 复制代码
START
  ↓
Agent
  ↓
有 tool_calls?
 ├─ Yes → ToolNode → Agent
 └─ No  → END

这里的 END 是:

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

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

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

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

3. return {"messages": [response]} 的意义

如果节点中:

arduino 复制代码
response = model.invoke(...)
return {"messages": [response]}

它的意思是:

把本次 LLM 返回的 AIMessage 作为 messages 字段的更新结果交给 LangGraph。

response 是单条消息,而 messages 是消息序列,因此要写:

vbscript 复制代码
{"messages": [response]}

而不是:

vbscript 复制代码
{"messages": response}

LangGraph 的节点不一定要返回完整 State,可以只返回需要更新的字段。

如果 State 定义为:

kotlin 复制代码
class AgentState(TypedDict):
    messages: Annotated[list[BaseMessage], add_messages]

那么 add_messages 会负责把新消息合并进旧消息历史,而不是简单覆盖。

4. Annotated 与 Sequence

Sequence

css 复制代码
Sequence[BaseMessage]

表示"一个按顺序排列、元素类型为 BaseMessage 的序列"。

它比 list 更抽象,可以表示 list、tuple 等序列类型。

但如果代码里明确需要列表操作,或者写:

css 复制代码
[system_prompt] + state["messages"]

那么直接使用:

css 复制代码
list[BaseMessage]

会更直观,也避免 list + tuple 这样的类型问题。

Annotated

css 复制代码
Annotated[类型, 附加信息]

用于给一个类型附加额外元数据。

LangGraph 中:

ini 复制代码
messages: Annotated[list[BaseMessage], add_messages]

可以拆成:

  • list[BaseMessage]:这个字段的数据类型。
  • add_messages:这个字段收到新值时的合并规则,也就是 reducer。

二级结论:类型告诉 LangGraph"这里装什么",reducer 告诉 LangGraph"新旧值怎么合并"。

5. BaseMessage 及常见消息类型

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

常见类型:

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

因此用户输入应该写成:

ini 复制代码
HumanMessage(content=user_input)

而不是直接实例化 BaseMessage。

6. Tool 定义时最好标注参数类型

推荐:

python 复制代码
@tool
def add(a: float, b: float) -> float:
    """计算两个数字的和。"""
    return a + b

参数类型和 docstring 都会帮助模型理解工具 schema,例如:

  • 工具叫什么。
  • 参数有哪些。
  • 参数是什么类型。
  • 什么时候应该调用这个工具。

7. 一个完整的 Tool Calling Agent 骨架

python 复制代码
from typing import TypedDict, Annotated
from dotenv import load_dotenv

from langchain_core.messages import BaseMessage, SystemMessage, HumanMessage
from langchain_core.tools import tool

from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode

from langchain_mistralai import ChatMistralAI

load_dotenv()

class AgentState(TypedDict):
    messages: Annotated[list[BaseMessage], add_messages]

@tool
def add(a: float, b: float) -> float:
    """计算两个数字的和。"""
    return a + b

@tool
def subtract(a: float, b: float) -> float:
    """计算两个数字的差。"""
    return a - b

tools = [add, subtract]

model = ChatMistralAI(
    model="ministral-3b-2512"
).bind_tools(tools)

def model_call(state: AgentState):
    system_prompt = SystemMessage(
        content="你是一个专业处理数学计算问题的工程师,请尽你最大的能力回答我的要求。"
    )

    response = model.invoke(
        [system_prompt] + state["messages"]
    )

    return {
        "messages": [response]
    }

def route_tools(state: AgentState):
    last_message = state["messages"][-1]

    if last_message.tool_calls:
        return "tools"

    return "end"

tool_node = ToolNode(tools)

graph = StateGraph(AgentState)

graph.add_node("agent", model_call)
graph.add_node("tools", tool_node)

graph.add_edge(START, "agent")

graph.add_conditional_edges(
    "agent",
    route_tools,
    {
        "tools": "tools",
        "end": END
    }
)

graph.add_edge("tools", "agent")

agent = graph.compile()

9. 如何调用 Agent

单轮调用:

ini 复制代码
user_input = input("Enter: ")

result = agent.invoke({
    "messages": [
        HumanMessage(content=user_input)
    ]
})

print(result["messages"][-1].content)

10. 常见错误:{HumanMessage(...)}

错误:

css 复制代码
"messages": {HumanMessage(content=user_input)}

这里的 {...} 在 Python 中表示 set,不是 list。

set 中的元素必须可哈希,而 HumanMessage 不是可哈希对象,因此会报:

bash 复制代码
TypeError: unhashable type: 'HumanMessage'

正确写法:

json 复制代码
"messages": [HumanMessage(content=user_input)]

记忆:

csharp 复制代码
[x]   # list
{x}   # set

11. 最终职责图

sql 复制代码
AgentState
   │
   ├─ messages
   │    ├─ HumanMessage
   │    ├─ AIMessage
   │    └─ ToolMessage
   │
   ↓
Agent / LLM        决策下一步
   ↓
route_tools        负责路由
   ↓
ToolNode           执行 Python 工具
   ↓
add_messages       合并消息历史
   ↓
Agent
   ↓
END

总结

LangGraph Tool Calling Agent 的核心,就是让 LLM 负责"决定",ToolNode 负责"执行",State 负责"保存上下文",conditional edge 负责"控制流程"。

相关推荐
镜象科技33 分钟前
AI情感大模型应用场景全解:从校园到医院再到企业的落地地图
人工智能
Joshua-a1 小时前
工程数学-理解卷积的含义_线性时不变系统的冲激响应与卷积
人工智能·深度学习
song5011 小时前
React Native for OpenHarmony 实战:三方库 react-native-css-transformer 的鸿蒙化适配指南
css·人工智能·深度学习·react native·transformer
老金带你玩AI6 小时前
ChatGPT dots,到底能替你操多少心?
人工智能
AliCloudROS7 小时前
计算巢 X DeepSeek Harness — 云端智能体工作台
人工智能·阿里云
深蓝AI7 小时前
GitHub的Push一年涨4.9倍:AI Agent为什么逼它重做Git存储?
人工智能·github
stereohomology7 小时前
大模型的观点谨:StoryTold 「Crafting Apps」四件套 · 深度总览
人工智能·llm
运维行者_7 小时前
网络性能监控怎么做?从自动发现到根因分析的4个环节
运维·服务器·网络·人工智能·支持向量机
Su米苏8 小时前
Spring AI 中MCP 与普通 @Tool的区别
java·人工智能·spring
美狐美颜sdk8 小时前
直播APP源码可以直接接入视频美颜sdk吗?技术方案详解
android·人工智能·音视频·美颜sdk·直播美颜sdk