
最近面试了几轮做 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 MessageAgent 运行时将工具的真实执行结果反馈给模型必须对应某个 tool_call_idcontent 是工具结果关键区别在于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(defaultNone, description结构化结果数据) suggestion: str | None Field( defaultNone, 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_asciiFalse)每个工具内部只需要把自己的业务结果填进这个 payload然后调用 to_content_string 生成 content。这样无论工具内部逻辑多复杂最终进入上下文的格式都是受控的。这里特别强调 description 不要省。Pydantic 的 Field 描述不仅能帮你生成文档还能和后续的 JSON Schema 结合用来校验工具输出。我在服务端代码里就经常通过 payload 的 schema 做自动化测试排查哪个工具改了字段类型导致模型读不懂。4.3 把 Tool Message 和 LangGraph 的节点函数串起来如果项目用的是 LangGraphTool 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( nametool_call[name], tool_call_idtool_call[id], **result ) new_messages.append( ToolMessage( namepayload.name, contentpayload.to_content_string(), tool_call_idpayload.tool_call_id, ) ) return {messages: new_messages}这个节点的关键是将每条 tool_call 的 id 原封不动传给 ToolMessage。只要这里不错位模型在多工具并发的时候就能准确知道哪个结果对应哪个调用。当时我们排查过很多“模型重复调用同一个工具”的问题最后定位基本都是 tool_call_id 在中间被框架重新生成或丢失。4.4 与大模型交互时的格式对齐不同模型的服务商对 Tool Message 的字段要求并不完全一致。OpenAI 兼容协议一般要求 role 为 tool并携带 tool_call_idClaude 的 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 八股文有用得多。这个设计能力也不是一两天能练出来的多从失败的线上案例里复盘多看一眼线上真实的消息日志你会很快建立起自己的判断力。