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

相关推荐
hrrrrxeeeee1 小时前
文件读取→比对→风险标记,拆解采购 AI 完整工作链路
大数据·人工智能·机器学习·prompt
AI闲人1 小时前
Agent 平台的两种哲学:从 WeKnora 和 Molio 聊起
人工智能·知识库·企业ai落地
林伽一2 小时前
从开源基础设施到垂直封装,AI 产业的技术主轴正在位移
人工智能·科技·ai·语言模型
lie..2 小时前
30天从零开始学AI应用开发(Day 3):30 分钟搭好开发环境,Python + VSCode 一次配齐
人工智能·python·大模型
β添砖java2 小时前
机器学习2 KNN算法、距离度量、特征预处理、超参数选择、手写数字、鸢尾花
人工智能·算法·机器学习
cc_瀚海知行2 小时前
模型调用 3|怎样让程序可靠地接收模型输出?
人工智能·ai
科创致远2 小时前
科创致远 ESOP 系统深度评测:从参数解析到 ROI 实战验证
大数据·人工智能·制造·精益工程
CAIE研习社2 小时前
从“会聊天”到能落地:光伏、风电运维里的AI怎么用?
大数据·人工智能
TAN-90°-2 小时前
Deep Learning for Computer Vision——Generative Models 2
人工智能·深度学习·神经网络·算法·目标检测·机器学习·计算机视觉