最近面试了几轮做 AI Agent 的候选人,几乎人人简历上都有“熟悉 LangChain / LangGraph / 各类 Agent 框架”,但一聊到 Tool Message,十个里能答到点子上的不超过三个。这个细节恰恰是 Agent 能否稳定运行的命门,也是实际开发和面试里最容易暴露深度的位置。
Tool Message 听起来就是工具返回结果的消息体,但它直接决定了模型下一步怎么决策、子任务怎么调度、失败怎么恢复,还牵涉上下文长度、并发一致性、多工具协同这些硬问题。这篇文章我打算把 Tool Message 设计这件事彻底讲透,从它在完整调用链里的定位,到字段级拆解,再到可落地的协议实现和踩坑实录。适合正在准备 AI Agent 工程师面试的朋友,也适合已经在写 Agent 但总感觉“模型行为不稳定、工具结果一复杂就出问题”的同学。
1. Tool Message 在 Agent 里的定位:先搞清楚它在哪,才知道怎么设计
1.1 一次 Agent 任务完整的消息流转
要理解 Tool Message,先要把一次 Agent 任务从头到尾的消息流看明白。我用一个最常见的场景来说:用户问“帮我查一下北京今天适合穿什么衣服”。
第一步,系统把用户消息丢给模型,模型发现这个问题需要调用天气 API,于是返回一个 Assistant Message,里面带着 tool_calls,内容是“调用 get_weather,参数是 city=北京”。这时的 Assistant Message 并没有对用户说话,它在“表达意图”。
第二步,Agent 运行时解析这个 tool_calls,真正去请求天气服务。服务返回“晴转多云,最高 28 度,最低 18 度,微风”。运行时代码将这个结果包装成一条 Tool Message。
第三步,把这条 Tool Message 连同之前的用户消息、Assistant Message 一起再发给模型。模型看到工具返回之后,才能组织最终语言:“北京今天晴转多云,早晚偏凉建议加件薄外套。”
这个过程中,Tool Message 处于“模型输出意图”和“模型组织最终答案”的中间地带,是整个回路里的反馈信号。没有它,模型永远不知道工具究竟执行成功没有、结果长什么样,只能靠猜。
1.2 它和 System / User / Assistant Message 的本质区别
很多候选人能背出四个消息角色,但说不清 Tool Message 和其他角色到底差在哪里。我把它们的本质差异整理成一张表:
| 消息类型 | 消息来源 | 核心作用 | 关键约束 |
|---|---|---|---|
| System Message | 开发者 | 设定全局规则、人格、输出格式 | 位置固定,通常在对话最前面 |
| User Message | 终端用户 | 表达请求,携带用户目标 | 用户输入可能携带噪音,需要清洗 |
| Assistant Message | 模型 | 表达回复,或携带调用工具的意图 | 同一个 Assistant 角色会交替出现多轮 |
| Tool Message | Agent 运行时 | 将工具的真实执行结果反馈给模型 | 必须对应某个 tool_call_id,content 是工具结果 |
关键区别在于:System、User、Assistant 都是“对话层面”的消息,而 Tool Message 是“执行层面”的消息。它是 Agent 运行时自己生成并注入对话历史的,不是模型产出的。所以设计的自由度其实在框架和业务方手里,模型只是消费者。
这个区别带来一个很实际的结论:Tool Message 的设计质量,直接决定模型基于什么样的上下文做下一步决策。如果你把一堆格式混乱、信息缺失、甚至只有“success”的 Tool Message 塞进上下文,模型根本没法工作。
1.3 用生活类比理解 Tool Message 的职责
我经常用“请人办事”来类比。你派助手去买咖啡,助手回来之后,你需要知道的不是“买好了”这三个字,而是“买了什么杯子、几分糖、多少钱、哪家店买的、有没有遇到排队”。如果助手只说“搞定了”,你接下来完全没法判断要不要让他再带份早餐。
Tool Message 就是这个“助手回来后递交给你的执行回报单”。回报单写得好,决策层(模型)才能准确判断下一步行动是继续追问、直接收尾还是切换方案。这也是为什么很多 Agent 实际效果不稳定,问题不在模型能力,而在工具结果的表达方式。
2. 为什么 Tool Message 的设计会直接决定 Agent 的“智商”
2.1 模型并不“看到”真实世界,它只“看到”消息
这是整篇文章最重要的原则:大模型从来不直接接触你的数据库、第三方 API 或者文件系统。它看到的一切外部世界,都是通过消息文本映射出来的。Tool Message 就是这一层映射的载体。
假设你的工具返回的是个 JSON 字符串,里面有关键字段 stock_quantity。模型不是通过数据库 schema 理解库存的,它只能从 Tool Message 里那个字段名、字段值和周围说明文字推断含义。如果字段名是 qty,值是一个数字,没有任何单位说明,模型很可能把这个值理解成“件数”之外的东西,甚至干脆忽略。
所以设计 Tool Message 有一个隐含任务:把原始工具输出翻译成模型“容易消化”的语言。这跟给人看的接口文档不是一回事,模型是没有耐心的读者,它会在上下文里快速扫描信息,你要做的是降低它的理解成本。
2.2 一个失败案例复盘:Tool Message 里只有“success”
我有一次维护一个内部 Agent,工具是对接上游 CRM 的创建订单接口。当时的返回处理很简单,业务代码把第三方返回包成一个字符串塞进 Tool Message,内容包括 HTTP 状态码和“success”字样。
结果模型在一个月内反复出现同一个问题:用户说“帮我下一个订单”,工具明明创建成功了,模型却回复“抱歉,下单失败,请稍后再试”。根因就是 Tool Message 只写了 success,没有包含订单号、金额、预计送达时间任何有效信息。模型看到这个空泛结果,结合企业服务类的谨慎心态,自然倾向于保守表达。
后来我把 Tool Message 改成结构化内容,包含 order_id、total_amount、estimated_delivery 这些字段,并加了人类可读的摘要:“订单 20240601 已创建成功,金额 299.00 元,预计明日送达”。同样的模型、同样的工具,回复立刻正常了。工具结果是好的,问题出在消息设计,这种案例在我实际接触的项目里非常多。
2.3 设计 Tool Message 的三个核心目标
经过这些教训,我把 Tool Message 的设计目标总结成三条:
第一,信息完整。模型需要的决策要素必须都在。查询类工具要返回查询条件对应的结果集,操作类工具要返回操作是否成功、影响对象和后续状态。这一点不能省,但也不是字段越多越好。
第二,意图明确。让模型一眼看出“这是成功、失败、还是部分成功”。尤其在多工具并行调用的场景里,模型需要同时处理五六个工具结果,如果每个结果的成功/失败标志不清晰,决策质量立刻掉下去。
第三,链路可对账。每一条 Tool Message 都能精确追溯到是哪一次工具调用产生的。这也是 tool_call_id 存在的意义。并发场景下面临的顺序和配对问题,都依赖这一条。
这三条目标会贯穿后面所有具体设计。
3. 核心细节拆解:生产级 Tool Message 应该长什么样
3.1 基础字段:名字、内容和调用 ID 是铁三角
不管用的是 OpenAI 原生接口、LangChain 还是自研框架,Tool Message 最稳定的三个字段就是 name、content、tool_call_id。其中 name 是工具名,content 是执行结果主体,tool_call_id 关联具体的工具调用请求。
这三个字段缺一不可。name 决定模型知道“这个结果来自哪个能力模块”,content 提供决策依据,tool_call_id 解决消息对账。面试时我常问一个问题,如果让你自己设计一个 Agent 框架,Tool Message 最少要几个字段?能答出这三个字段并解释清楚的候选人,基本是有实战经验的。
这里有个容易忽略的点:tool_call_id 的生成方和消费方必须是同一个上下文。有些框架会在多轮调用里重新生成 ID,导致第二轮模型发出的 tool_calls 引用不到第一轮的 Tool Message,整个对话就会“失忆”。我习惯用调用序号加时间戳的组合生成 ID,例如 tool_call_ _ ,既保证唯一性,又方便人工排查。
3.2 工具名与调用映射:多工具场景下怎么避免串台
当 Agent 同时注册了十个甚至更多工具时,Tool Message 的 name 字段就变得格外重要。模型收到工具结果时,往往需要结合“我调了哪个工具、参数是什么”来判断结果是否合理。
举个例子,一个 Agent 同时接了 get_stock_price 和 get_exchange_rate,如果某个工具实现时内部调错了 API,返回内容张冠李戴,而 Tool Message 里的 name 又是错的,模型就完全没有发现异常的依据。所以 name 字段应该与工具定义中的名称严格一致,不要用缩写、不要用中文别名,保持一致能减少模型的理解偏差。
此外我还建议在 content 里附带工具调用时的关键入参,尤其是查询类工具。比如 get_stock_price 返回的 content 建议是“股票代码 AAPL 的最新收盘价为 192.53 美元,查询时间为 ...”。模型看到这个内容就知道结果是针对哪个标的的,不需要靠猜,也不容易出现多工具交叉时的信息混淆。
3.3 content 字段的表达方式:什么时候用纯文本,什么时候用 JSON
这是实际开发中争议最多的部分。有的人喜欢返回 JSON 字符串,有的人喜欢返回自然语言,我在不同项目里都试过,最终形成的规则是分层处理。
如果工具结果最终要直接展示给用户,比如淘宝商品信息、天气信息、订单状态,我建议 content 里写人话,同时把结构化字段放在一个叫 structured 的附加字段里。如果工具结果主要是给模型做推理用的,比如知识库检索、代码执行结果、计算过程,用 JSON 格式更合适,因为字段分明的 JSON 在上下文里比一长串文字更容易被模型定位关键值。
我这里说的“附加字段”是指内容自描述,不是丢弃协议。例如 content 可以写成:
{ "summary": "检索到 5 条相关文档", "structured": [ {"doc_id": "d001", "title": "Agent Memory 论文", "score": 0.92}, {"doc_id": "d002", "title": "Tool Use 综述", "score": 0.87} ] }这里 content 整体是一个 JSON,但包含 summary 字段给模型一个快速概览,再通过 structured 承载明细。模型既能一眼看到结论,又能在需要时深入细节。
3.4 错误返回绝不能只丢一个“error”
Tool Message 的错误设计是最能体现工程师水平的地方。很多初版实现会把异常直接堆进 content,例如:
return "Error: 'NoneType' object has no attribute 'id'"这种消息对模型完全没用。模型不知道这个错误是参数错误、网络超时、权限不足还是数据不存在,更不知道下一步该重试、该换工具参数、还是直接告诉用户不行。
我设计的错误 Tool Message 通常包含四个部分:错误类型、错误摘要、可能原因、建议动作。例如:
{ "status": "error", "error_type": "api_rate_limit", "summary": "天气服务接口触发限流,每分钟最多 10 次调用", "suggestion": "等待 6 秒后重试同一参数,或改用备用天气源" }这种设计让模型有据可循。它看到 rate_limit 和重试建议,大概率会采取“等待后重试”或“换备用源”的策略,而不是机械地告诉用户系统失败。
4. 实操过程:从 0 架设一套可落地的 Tool Message 规范
4.1 协议先行的设计思路
我落地过好几套 Agent 项目,总结出一个经验:第一步不是写代码,而是先把 Tool Message 协议写出来,包括字段定义、成功和失败格式、统一示例。这份协议要同时被工具开发者、Agent Runtime、和 Prompt 维护者看到。因为工具类很多项目里不是一个人写的,就算是一个人写的,过两周也会忘。
我把这套协议放在项目目录下的 docs/tool-message-spec.md 里,作为所有工具实现的基本约定。例如定义统一格式:
ToolMessage 字段约定 - name: 工具名,必须与 tools 列表一致 - tool_call_id: 调用 ID,由 Agent Runtime 生成 - content: 可被模型直接消费的结果内容 - status: success / error / partial其中 status 是顶层字段,content 里用结构化字符串承载。这个约定很简单,但能让所有工具的输出行为统一,模型看到的上下文风格一致,决策稳定性会好很多。
4.2 用 Pydantic 类约束 Tool Message 的生成
我平时用 Python 做 Agent 开发,Pydantic 是最好用的工具之一。定义 Tool Message 的数据类能强制所有工具函数返回合法结构,同时自动做字段校验。
一个典型的实现如下:
from typing import Any, Literal from pydantic import BaseModel, Field class ToolMessagePayload(BaseModel): name: str = Field(..., description="工具名,必须与工具注册名一致") tool_call_id: str = Field(..., description="对应的调用 ID") status: Literal["success", "error", "partial"] = "success" summary: str = Field("", description="一句话结果摘要") data: Any = Field(default=None, description="结构化结果数据") suggestion: str | None = Field( default=None, description="对模型的动作建议,主要用在错误或部分成功场景" ) def to_content_string(self) -> str: import json return json.dumps({ "status": self.status, "summary": self.summary, "data": self.data, "suggestion": self.suggestion, }, ensure_ascii=False)每个工具内部只需要把自己的业务结果填进这个 payload,然后调用 to_content_string 生成 content。这样无论工具内部逻辑多复杂,最终进入上下文的格式都是受控的。
这里特别强调 description 不要省。Pydantic 的 Field 描述不仅能帮你生成文档,还能和后续的 JSON Schema 结合,用来校验工具输出。我在服务端代码里就经常通过 payload 的 schema 做自动化测试,排查哪个工具改了字段类型导致模型读不懂。
4.3 把 Tool Message 和 LangGraph 的节点函数串起来
如果项目用的是 LangGraph,Tool Message 通常不是手动构造,而是通过工具节点自动生成的。很多同学在这里会踩坑:直接让工具函数返回一个字符串,以为 LangGraph 会自动做包装。其实默认工具节点确实会包装,但包装逻辑非常简单,基本就是“工具返回啥,content 就是啥”。你的工具函数如果返回半结构化文本,模型消费的效率就会受影响。
所以我更推荐在工具函数内部直接返回 ToolMessagePayload 实例,或者在工具函数返回后、交给节点之前做一层转换。以 LangGraph 风格为例,工具节点可以这样实现包装逻辑:
from typing import Annotated, TypedDict from langgraph.graph import StateGraph, END from langchain_core.messages import ToolMessage class AgentState(TypedDict): messages: Annotated[list, lambda x, y: x + y] def tool_node(state: AgentState): last_message = state["messages"][-1] new_messages = [] for tool_call in last_message.tool_calls: result = run_tool(tool_call["name"], tool_call["args"]) payload = ToolMessagePayload( name=tool_call["name"], tool_call_id=tool_call["id"], **result ) new_messages.append( ToolMessage( name=payload.name, content=payload.to_content_string(), tool_call_id=payload.tool_call_id, ) ) return {"messages": new_messages}这个节点的关键是:将每条 tool_call 的 id 原封不动传给 ToolMessage。只要这里不错位,模型在多工具并发的时候就能准确知道哪个结果对应哪个调用。当时我们排查过很多“模型重复调用同一个工具”的问题,最后定位基本都是 tool_call_id 在中间被框架重新生成或丢失。
4.4 与大模型交互时的格式对齐
不同模型的服务商对 Tool Message 的字段要求并不完全一致。OpenAI 兼容协议一般要求 role 为 tool,并携带 tool_call_id;Claude 的 tool_result 格式则更强调 is_error 标志。团队里多个模型并存的时候,我的做法是统一内部协议,然后在适配层做转换。
比如在 LangChain 里,直接使用原生 ToolMessage 对象,LangChain 的 BaseChatModel 适配器会帮你转换到底层 API 格式。但如果你用的是自研调用层,就要小心:把内容拼成对话数组时,位置必须在对应的 Assistant Message 之后。顺序错了,模型会报错或者行为异常。
对齐这件事还有个细节:模型上下文里的 Tool Message 一般要预留一定的 token 预算。一个执行结果动辄几千 token 的工具,如果原样塞回去,几轮下来上下文就爆了。所以我在工具节点后面经常加一个“结果瘦身”步骤,只保留模型决策需要的关键信息,详细数据放到外部存储里,需要时再让模型调工具取。
5. 高频坑位与面试追问实录:这些细节才是区分度
5.1 并发调用下 Tool Message 顺序错乱怎么办
这是 Agent 工程师面试里出现频率极高的问题。一个模型在一次回复里可能同时发出多个 tool_calls,例如同时调用“查库存”和“领优惠券”,这时候工具是并行执行的,返回顺序和调用顺序不一定一致。
答案的核心就是 tool_call_id 对账,而不是依赖数组顺序。正确实现里,每条 Tool Message 通过 tool_call_id 绑定原先的调用,模型在下一个回合看到这些消息时,会根据 ID 重建对应关系。只要绑定正确,返回顺序打乱不影响决策。我在实际代码里还会多做一步排序,把 Tool Message 按 tool_call_id 的生成序号排好再追加到消息列表,图个保险,顺手减少上下文里的随机性。
面试追问通常还会接着问:如果你的框架不维护 tool_call_id,怎么处理?我的答案是设计上不允许这种情况。任何 Agent 框架都必须把 tool_call_id 作为一等公民对待。如果团队自研框架偷懒,靠位置推断对应关系,那并发一高必然出事故。
5.2 工具返回内容太长,上下文爆掉怎么办
这个问题几乎每个做 Agent 的人都会遇到。工具返回一个 2 万字的数据库查询结果,直接塞进 Tool Message,模型不仅处理慢,还会“淹没”在冗长数据里,反而忽略真正重要的字段。
我的处理思路分三层:第一层,工具本身支持参数裁剪,比如分页、字段过滤,从源头减少返回量。第二层,在 Tool Message 生成前做摘要,用 summary 概括结果,只保留 top N 条明细。第三层,对于确实需要完整数据的任务,把完整结果存到临时存储,Tool Message 里只放一个检索用的引用 ID。模型如果发现自己需要更深的数据,再发起第二次工具调用去取。
实际项目中“摘要层”的效果最明显。比如检索类工具,Tool Message 里我经常只放前 5 条结果标题加打分,模型基于这些信息做排序、筛选和回答,命中率和全量投喂基本持平,但 token 消耗降了一个数量级。
5.3 工具返回空结果算成功还是失败
这也是一个很容易踩的语义坑。工具正常执行了,但查询结果为空,例如“查这个客户名下有没有订单”,结果确实没有订单。这时候 status 应该是什么?
我把这类情况归为 partial 而不是 error。错误的核心特征是“工具没能给出有效结果”,而空结果也是有效结果,只是数据面为空。Tool Message 设计里我会写清楚:
{ "status": "success", "summary": "该客户名下暂无订单记录", "data": [] }这样模型会自然理解为“查询成功但结果为空”,然后继续追问可能原因,并给出更贴合的后续行动。如果把空结果标成 error,有些模型反而会编造订单或者道歉,业务上完全不可接受。
5.4 模型不听话,生成非法 tool_call 怎么办
模型返回的 tool_calls 并不是 100% 合法,参数可能缺失、类型可能错误、工具名可能不存在。Agent Runtime 这时候要做的不是把这个非法调用转发到工具代码里,而是先生成一个特殊 Tool Message,向模型反馈参数校验失败原因。
我见过一个坑:工具函数内部抛异常,Runtime 又没捕获,整个对话直接中断。改进方式是引入一个 validate_and_call 层,先做 args 的 JSON Schema 校验,校验失败时生成如下 Tool Message:
{ "status": "error", "error_type": "invalid_params", "summary": "工具 get_weather 缺少必须参数 city", "suggestion": "请补充城市名称后重新调用" }这种反馈就像把球踢回给模型,让它基于具体错误做修正。模型得到了足够详细的纠错信号,往往下一轮就能给出合法调用。这比“你调用的工具不存在”这种宽泛返回要有效得多。
5.5 一个预留扩展点:让 Tool Message 携带“动作建议”
最后聊一个很多人忽视的扩展字段:suggestion。我之所以坚持在 Tool Message 协议里保留这个字段,是因为它对模型的引导作用极其明显。
模型在面对复杂决策时容易出现两种毛病:一是调工具失败后重复用同一套错误参数重试;二是明明工具结果异常却自作主张继续下一步。suggestion 字段相当于给模型一个“路标”,告诉它接下来哪条路最可能走得通。比如限流错误提示等待几秒,权限错误提示需要切换用户授权,数据不一致提示使用另一把 key 重查。
这个字段的写法也有讲究。不要写命令式语气,例如“你必须重试”,而是写“可考虑在 6 秒后重试,或转为使用备用数据源”。模型对这种带选项的建议采纳率会更高,因为它不违背模型的“决策自主性”。这背后其实就是工具结果与提示词工程相结合的设计思路,值得在面试里聊几句,能体现对模型交互特性的理解。
在实际项目中,我还遇到过工具结果正常但被模型误判的场景。加上 suggestion 字段之后,这类误判率下降非常明显。所以如果你准备面试,建议把这个字段放进自己的设计里,并且能解释清楚它是“给模型的建议,而不是给用户的回复”。
写在最后的一点个人体会
我做过不少 Agent 项目,踩过最深的一个坑就是“结果处理得太随意”。早期我也直接返回原始字符串,总觉得模型够聪明能自己理解,结果被各种诡异行为打脸。后来才想明白一件事:Agent 的稳定性不是靠模型随机应变,而是靠工程上把反馈信息做得足够规范,让模型的每一次决策都有清晰依据。
Tool Message 就是你给模型铺的路。路标清晰了,它才能按时到终点。面试里把一个细节聊透,比背十篇 Agent 八股文有用得多。这个设计能力也不是一两天能练出来的,多从失败的线上案例里复盘,多看一眼线上真实的消息日志,你会很快建立起自己的判断力。