LLM Agent 工具调用结果格式设计:从 JSON 到统一封装实战
在 LLM Agent 应用里工具调用是模型连接外部世界的桥梁。写工具函数本身并不难真正让很多开发者纠结的是工具执行完数据后到底应该以什么格式把这些数据“报告”给 LLM模型才能准确理解、继续推理并完成后续动作。这个问题从 Hacker News 上的讨论到实际项目中的踩坑都反复出现因为它直接决定 Agent 的稳定性。如果一个工具返回的结果只是一堆无结构字符串或者一个字段含义不清的 JSONLLM 可能看不懂数据可能把错误信息当成正常业务结果也可能因为结果太长而丢失重点。这篇文章会从工具调用链路的底层逻辑出发比较 JSON、文本、Markdown、CSV、YAML 等常见格式的适用场景然后给出一个可以落地的统一包装结构并用 Python 代码实现完整示例最后列出生产环境必须处理的格式问题、排错路径和最佳实践。1. 先拆解问题工具把结果回报给 LLM和给人看有什么不同1.1 LLM 工具调用是“模型决策、工具执行、结果回填”三段式要理解工具结果格式为什么重要先要看清楚工具调用到底是怎么工作的。一个典型的 LLM 工具调用链路由三段组成第一段是模型决策。LLM 根据用户请求、系统提示词和历史对话决定是否需要调用某个工具并生成一次工具调用请求里面包括工具名称和 JSON 格式的参数。第二段是你的代码执行工具。应用程序收到模型生成的调用请求后调用真实函数比如查询数据库、读取文件、请求外部 API。第三段是结果回填。工具执行完成后返回值被拼装成一个消息重新发送给 LLM模型再根据这个结果生成最终回答或发起下一次工具调用。这里的关键在于工具执行结果是一个“中间产物”它不是给最终用户看的而是给另一个 LLM 上下文窗口看的。普通函数返回值可以只追求机器可读最终用户界面则是另一套展示逻辑。工具结果正好在两者中间既需要程序能构造和处理又需要 LLM 能读懂语义。如果把这个链路拆开看就能明白为什么格式设计不能靠随手 return。模型下一次推理完全依赖你拼进对话的这段文本它的结构、字段名、详略程度都会直接影响模型判断。1.2 为什么格式不是随便写写就行很多开发者第一次写工具函数时习惯直接从数据库查一个 list然后 return 一个 Python list 或 dict框架会自动序列化成 JSON。但问题在于这个 JSON 是给程序看的不一定是给 LLM 看的。给程序看的数据结构只需要类型正确、字段完整。给 LLM 看的数据格式还要满足三个额外要求语义自解释模型没有看过你的数据库表结构只能靠字段名和内容猜测含义。关键信息突出模型在很长的上下文中找答案重点信息应该靠结构设计自然凸显。失败可恢复工具出错时模型需要知道发生了什么、能不能重试、下一步该怎么办。一个随手返回的 JSON例如{id: 1, name: Alice, status: 0}程序解析没有障碍但 LLM 看到status: 0时完全不知道 0 代表启用还是禁用不知道这个用户是否可用也不知道下一步应该做什么。这就是格式设计失败的典型例子。所以工具结果格式的核心任务不是“选择一种序列化方式”而是“让一次工具调用的结果在进入 LLM 上下文后能够被准确、高效地消费”。1.3 糟糕格式的三个典型失败表现实际项目里工具结果格式不当会表现出三类问题。第一类是模型读不懂。字段名太简略、值域没有说明、嵌套过深模型无法从结果中定位到需要的信息最后生成含糊回答或者追问用户。比如数据库查询返回了一个三层嵌套的产品分类 JSON模型不知道该向用户推荐哪个层级的数据。第二类是模型误读。工具返回了错误信息但错误信息被放在 data 字段里没有任何错误标记模型把报错文本当成正常数据回答给用户。例如 HTTP 请求失败工具把异常信息作为字符串返回模型对用户说“接口返回的数据是 500 Internal Server Error”。第三类是上下文失控。工具结果过大几万行日志、完整文件内容或全表数据直接进入上下文导致 Token 暴涨模型注意力被无关内容分散关键信息反而被淹没甚至直接触发上下文长度限制。这三类问题都指向同一个结论工具结果格式必须有设计而不是自然返回的数据库记录或异常对象。2. 设计格式前先确定三个边界条件任务类型、结果体量、错误策略2.1 查询型、操作型和汇总型工具对格式要求不同不是所有工具都适合同一种结果格式。先给工具分类再决定怎么写结果会更合理。查询型工具负责读取信息例如查数据库、搜文档、读文件。这类工具的结果通常是要被 LLM 直接引用的原始数据格式上要尽量结构化字段名要清晰必要时附带元信息说明来源和条数。操作型工具负责执行动作例如发送邮件、创建工单、下单、修改配置。这类工具的结果重点是“动作是否成功”而不是“返回了一堆数据”。所以结果格式要突出状态、成功标志、操作 ID 和失败原因。汇总型工具负责对数据进行计算或聚合例如统计订单总额、计算平均数、分析日志分布。这类工具的结果通常是一个较小的结论格式上适合直接给出数值和简要说明而不是把参与计算的明细全部返回。如果不做这个分类所有工具都返回同一套“数据列表”操作型工具的成功标志就会被数据淹没汇总型工具的结论也会被明细干扰。2.2 结果体量影响格式和截断方式工具结果的大小是设计格式时必须考虑的问题。小结果和大结果的处理策略完全不同。小结果比如一个用户信息、一条订单状态、一个操作确认信息可以直接完整返回格式可以保持详细。中等结果比如一个查询返回几十条记录可以完整返回但要做字段裁剪只保留 LLM 决策需要的列。大结果比如日志文件、全表扫描、接口返回的超大 JSON直接完整返回会让上下文膨胀需要先做摘要、截断或分页。这里建议给工具结果设置一个体量阈值。如果预估返回内容超过一定 Token 量工具应该主动进行预处理只返回统计信息、只返回前 N 条样本、只返回与用户查询直接相关的字段。让 LLM 自己去处理超大原始数据既低效又不可控。常见处理方式是“摘要 样本 完整数据位置”。工具可以返回一条汇总统计说明附上前几条典型记录并提示如果用户需要更多明细可以继续请求下一批。2.3 错误策略先于格式设计工具一定会失败。网络超时、数据库连接失败、权限不足、参数校验不通过这些情况在真实系统里很常见。问题在于工具失败后应该返回什么给 LLM。很多工具函数在异常分支直接返回 null、false 或只返回一段错误字符串。这些表达对企业应用来说信息量太低。LLM 拿到 null 不知道是“查询结果为空”还是“查询失败”拿到 false 不知道失败原因拿到一段英文报错不知道是否需要重试。因此在设计工具结果格式前要先确定错误策略是否需要区分业务异常和系统异常错误信息要不要包含错误码要不要告诉 LLM 这个错误是否可重试失败时是否要附带部分成功的上下文。这些决策会影响结果格式的数据结构最好在设计阶段就确定下来。推荐的错误策略是任何失败都必须返回结构化的错误信息包含错误码、人类可读的错误说明、是否可重试三个要素。同时错误信息必须与正常业务数据隔离不能让模型把错误当成数据。3. 逐项拆解常见格式JSON、纯文本、Markdown、CSV、YAML3.1 JSON机器可解析的首选但不是万能JSON 是 LLM 工具调用的事实标准。理由很直接它结构清晰类型明确能被程序精确解析同时由于 LLM 训练语料里包含大量 JSON 示例模型对 JSON 格式的理解能力很强。但 JSON 并不是无脑返回就完了。同样一段业务数据字段命名方式、嵌套层级和冗余程度会显著影响 LLM 的理解质量。一个常见问题是嵌套过深。如果数据库查询结果经过多层关联后形成一个五层嵌套的 JSON模型要推断数据关系Token 消耗大且容易遗漏深层字段。比较推荐的做法是把工具结果尽量扁平化一到两级嵌套最合适复杂关联可以通过数组或平铺字段表达。另一个问题是字段名含义不清。status: 0、flag: 1、type: A这类值需要额外映射才能理解。只要不影响体积字段名应该尽量完整明确例如order_status: pending就比status: 0好理解得多。JSON 还有一个特点需要注意它对 Token 的消耗高于纯文本因为每个键名、冒号、引号和括号都会占用上下文。对大型结果键名重复会导致 Token 膨胀此时需要权衡完整字段名和 Token 消耗。3.2 键值对和纯文本适用于简单查询但结构能力弱对于非常简单的工具比如返回一个配置项、一个计算数值直接用纯文本或键值对可能比完整 JSON 更高效。例如一个单位换算工具返回1 英里 1.609344 公里这段话简单直接LLM 一眼就能读到结论而且不需要解析结构。键值对形式同理country: CN timezone: Asia/Shanghai currency: CNY这种格式在简单场景下 Token 消耗低模型也容易理解。但它的问题在于结构表达能力弱没有数组、没有嵌套、没有类型区分遇到多条记录、复合对象或父子结构时纯文本表达会让解析变得非常痛苦。所以纯文本和键值对适合“结论型”工具结果不适合“数据型”工具结果。如果工具返回的是多条记录建议换成 JSON 或 CSV不要用长篇文本硬撑。3.3 Markdown 表格适合人读LLM 解析存在隐性成本Markdown 表格看起来很美人类阅读体验好LLM 对 Markdown 也有一定理解能力。但在工具结果场景里Markdown 表格有一个明显问题表格无法表达嵌套关系单元格里的长文本会破坏列宽特殊字符需要转义。如果工具返回的是一个扁平记录列表例如用户列表、订单列表、文件列表Markdown 表格可以接受。模型可以像人一样按行列读取表头也提供了字段语义。但一旦数据出现层级关系例如一个订单包含多个商品Markdown 表格就无法优雅表达了。硬塞进一格又会造成阅读理解困难。此外 Markdown 表格解析起来容易出错尤其是字段里包含竖线、换行或超长内容时。因此 Markdown 表格更适合面向用户的展示不适合作为工具结果回传给 LLM 的主要载体。如果要用建议只用于条目数少、字段简单的场景。3.4 CSV/TSV适合批量数据但类型和语义容易丢CSV 或 TSV 适合表示表格型批量数据尤其是在数据行数较多时Token 消耗比 JSON 低很多。每行一条记录逗号或制表符分隔字段第一行可以是字段名。例如查询用户列表CSV 形式user_id,display_name,email,status 1001,Alice,aliceexample.com,active 1002,Bob,bobexample.com,inactive这个格式比等价的 JSON 数组节省不少 Token而且对固定结构的数据表达清晰。但它的缺点也很明显类型信息丢失数字、枚举、布尔值在 CSV 里都是字符串。字段里如果包含逗号、引号或换行会破坏解析。值域语义必须靠字段名和内容猜例如status的值是active还是1模型只能靠上下文推断。空值表达不统一空字符串和 null 无法区分。所以 CSV/TSV 适合作为“大批量同构数据”的返回格式但前提是字段表示规范、数据简单、不包含复杂对象。3.5 YAML可读性好但要小心歧义YAML 在配置领域非常流行它的缩进风格人类友好LLM 对 YAML 也有基本的理解能力。作为工具结果格式YAML 能表达嵌套结构读起来比 JSON 更容易。问题在于 YAML 的解析规则比 JSON 复杂存在一些歧义点。例如yes、no、on、off在某些解析器里会被自动转成布尔值字符串中的特殊符号可能触发折叠语义缩进不一致会导致解析失败。如果工具回归队用 YAML 给人类看内部处理没问题。但作为面向 LLM 的通用工具结果格式YAML 比 JSON 的兼容性和可预测性差不建议作为主要格式。它更适合放在系统提示词或配置模板里而不是工具返回数据。3.6 一份格式选型速查表格式机器可解析性LLM 理解难度Token 消耗适合场景不适合场景JSON最好中中高结构化数据、嵌套数据、多字段记录超大结果、极简单结论纯文本/键值对中低最低简单结论、少量配置项多条记录、嵌套数据Markdown 表格中中中少条数扁平列表嵌套关系、长文本字段CSV/TSV中中低大批量同构数据复杂类型、含分隔符字段YAML中中中配置文件、模板通用工具结果核心选型逻辑可以概括为优先 JSON 作为通用格式大批量同构数据改用 CSV简单结论使用纯文本Markdown 表格用于展示而不是回传YAML 让位给 JSON。4. 推荐做法用统一 envelope 包装工具结果JSON 为主体4.1 一个最小可用的 ToolResult 结构综合上面各格式的优劣一个比较稳妥的做法是以 JSON 为主体定义一个统一的包装结构所有工具都返回相同的外层结构只有 data 部分根据工具类型变化。推荐的最小结构如下{ status: success, data: {}, error: null, meta: {} }四个字段分别承担不同职责status执行状态取值固定为 success、error、empty、partial 四类。data正常执行时的业务数据按工具类型组织。error失败时的错误信息包含错误码、错误说明、是否可重试。meta补充元信息例如查询耗时、返回条数、数据来源、分页信息。这个结构的核心价值是LLM 每次拿到工具结果先看 status 就知道接下来该走什么逻辑不需要在字段内容里猜。无论工具是查询数据库、发邮件还是读文件外层结构一致模型行为更稳定。4.2 status 四态success、error、empty、partialstatus 是 ToolResult 里最重要的字段它的取值设计直接影响 LLM 的决策路径。success 表示工具正常完成且返回了有效数据。LLM 直接阅读 data 字段结合 meta 里的说明进行回答或继续决策。error 表示工具执行失败。此时 data 应为 null 或缺失error 字段必须包含完整错误信息。LLM 看到 error 后应该向用户解释失败原因必要时根据 retryable 标记决定是否重试或者调用其他备用工具。empty 表示工具执行成功但没有找到符合条件的数据。这个状态很重要因为它区分了“查询失败”和“查询结果为空”。如果没有 empty 状态模型可能会把空数组理解成查询异常或者把正常的无结果回答成失败。partial 表示工具执行部分成功例如批量操作中前面几条成功、后面几条失败或者大规模查询只返回了截断后的样本。partial 状态告诉 LLM当前数据不完整回答时要注意说明局限性必要时提示用户继续获取更多信息。这四个状态基本覆盖了工具执行的所有可能结果也为 LLM 提供了清晰的分支判断依据。4.3 data 字段如何组织才利于 LLM 理解data 是业务数据的载体它的组织方式决定了 LLM 是否能高效提取关键信息。推荐三个原则扁平优先、明确字段名、补充必要统计。扁平优先意味着不要制造多层无用嵌套。如果查询一个用户列表返回一个数组每个元素包含用户主要字段就是合理结构。如果硬要包一层{result: {list: [...]}}只会增加模型解析的层级负担。明确字段名意味着能用order_status就不用s能用created_at就不用time。模型靠字段名字面含义理解数据字段名越明确理解越准确。补充必要统计意味着对于多条数据除了完整列表或截断样本还应该返回总数、条数、摘要等辅助信息。这些信息放在 meta 里而不是 data 里。例如查询返回 100 条订单但只回传前 10 条样本meta 里要写清total: 100、truncated: true让模型知道数据不完整。如果结果是单个对象data 直接放对象本身如果结果是列表data 放数组如果有嵌套关系控制在两级以内如果结果包含统计信息和明细考虑拆成data和meta分别承载。4.4 error 字段怎么写才利于模型恢复error 字段不是简单地塞入异常对象。推荐的结构是{ error: { code: DB_TIMEOUT, message: 数据库查询超时请稍后重试, retryable: true } }code 是一个稳定的机器可读错误码用于程序判断和日志分类。message 是一段给 LLM 和开发者都能读懂的说明最好直接说明失败原因和可能的下一步。retryable 是布尔值表示该错误是否值得重试。例如网络超时retryable 设为 trueLLM 可以选择稍后重试权限不足retryable 设为 false重试没有意义LLM 需要引导用户检查权限参数校验失败retryable 设为 falseLLM 需要根据参数要求修正输入后再调用。error 字段在实际拼装时可以整体放在 ToolResult 的 error 字段下也可以作为 data 的一个子字段。推荐前者因为外层信封的责任就是让 status 和 error 保持平级LLM 一眼就能看到状态和错误信息不用深入业务数据结构中挖。注意不要把异常堆栈完整丢给 LLM。堆栈信息 Token 消耗大且对模型决策帮助有限。错误信息应该经过提炼保留错误码、失败原因和可执行建议。5. Python 实现一个工具结果格式化器并验证完整调用链5.1 定义 ToolResult 数据结构和工厂方法下面用 Python 实现一个最小可用的工具结果格式化器。这个实现可以直接用于模拟 LLM 工具调用链路。from dataclasses import dataclass, field from typing import Any, Optional import json dataclass class ToolResult: status: str data: Any None error: Optional[dict] None meta: dict field(default_factorydict) def to_prompt(self) - str: payload { status: self.status, data: self.data, error: self.error, meta: self.meta, } return json.dumps(payload, ensure_asciiFalse, indent2) classmethod def ok(cls, data: Any, meta: dict None) - ToolResult: return cls(statussuccess, datadata, metameta or {}) classmethod def empty(cls, meta: dict None) - ToolResult: return cls(statusempty, data[], metameta or {}) classmethod def fail(cls, code: str, message: str, retryable: bool False) - ToolResult: return cls( statuserror, dataNone, error{ code: code, message: message, retryable: retryable, }, ) classmethod def partial(cls, data: Any, meta: dict) - ToolResult: return cls(statuspartial, datadata, metameta)这段代码定义了一个数据类 ToolResult包含四个字段和前面推荐的 envelope 结构一一对应。to_prompt()方法把结果序列化成 JSON 字符串这个字符串就是要拼到 LLM 对话上下文里的内容。工厂方法ok、empty、fail、partial分别对应四种状态方便工具函数直接构造结果。5.2 模拟查询用户工具和发送通知工具接下来模拟两个真实工具函数一个负责查询用户一个负责发送通知。def query_user(user_id: str) - ToolResult: # 模拟数据库查询 fake_db { 1001: {user_id: 1001, display_name: Alice, email: aliceexample.com, plan: pro}, 1002: {user_id: 1002, display_name: Bob, email: bobexample.com, plan: free}, } if user_id not in fake_db: return ToolResult.empty(meta{query: user_id, reason: user not found}) user fake_db[user_id] return ToolResult.ok(datauser, meta{source: user_service, matched: 1}) def send_notification(user_id: str, content: str) - ToolResult: if not content or len(content) 200: return ToolResult.fail( codeINVALID_CONTENT, message通知内容不能为空且长度不能超过 200 字符, retryableFalse, ) # 模拟发送成功 return ToolResult.ok( data{notification_id: n_10001, user_id: user_id}, meta{channel: email}, )query_user 演示了查询型工具的成功和 empty 两种情况。send_notification 演示了操作型工具重点返回操作 ID 和通道信息而不是业务数据。两个工具都调用同一个 ToolResult外层结构一致。5.3 组装成 LLM 上下文并模拟工具调用流程现在模拟一次完整调用LLM 先查询用户然后给用户发送通知。def simulate_llm_tool_sequence(): messages [] # LLM 第一次调用查询用户信息 result_1 query_user(1001) messages.append({ role: tool, content: result_1.to_prompt(), }) # 模拟 LLM 根据查询结果决定发送通知 result_2 send_notification( user_id1001, content您的试用期将在本周结束请及时续费。, ) messages.append({ role: tool, content: result_2.to_prompt(), }) for msg in messages: print(msg[role], msg[content]) if __name__ __main__: simulate_llm_tool_sequence()这段代码演示了工具结果回填后如何作为 tool 角色的消息进入对话。实际开发中消息会通过 OpenAI、Anthropic 或本地模型的 message API 发送给 LLM。这里直接打印便于观察格式。5.4 运行结果和预期输出运行代码后两次工具调用会输出两个结构清晰的 JSON第一个结果{ status: success, data: { user_id: 1001, display_name: Alice, email: aliceexample.com, plan: pro }, error: null, meta: { source: user_service, matched: 1 } }第二个结果{ status: success, data: { notification_id: n_10001, user_id: 1001 }, meta: { channel: email }, error: null }这个输出是一个可直接运行的格式模板。把这段 JSON 放入 LLM 上下文模型第一眼看到 status 是 success就会去 data 里找业务内容看到 meta能得知数据来源有错误时会读到 error 字段做出相应判断。6. 不同工具类型的结果组织案例6.1 数据库查询工具数据库查询工具最常用。查询结果既可能是单条记录也可能是多条记录还可能无数据。推荐把列字段名保持与业务一致并把查询行为相关信息放进 meta。{ status: success, data: [ {order_id: A1001, customer_name: Alice, total_amount: 199.00, status: paid}, {order_id: A1002, customer_name: Bob, total_amount: 59.00, status: pending} ], error: null, meta: { sql: select order_id, customer_name, total_amount, status from orders where customer_id ?, row_count: 2, elapsed_ms: 12 } }这里金额字段使用字符串避免浮点精度问题影响 LLM 理解。meta 中记录 row_count 和查询耗时方便模型判断结果是否完整、性能是否异常。6.2 HTTP API 调用工具HTTP API 工具需要特别注意不要直接把 HTTP 状态码作为唯一信息返回。500对 LLM 没有语义价值要转换成可执行信息。{ status: error, data: null, error: { code: API_RATE_LIMITED, message: 目标接口触发限流建议等待 30 秒后重试, retryable: true }, meta: { url: https://api.example.com/v1/orders, http_status: 429 } }HTTP 状态码可以放到 meta 里留作审计而 status 和 error 承载模型决策需要的信息。推荐做法是把各类可能的 HTTP 错误映射成稳定的错误码避免模型每次面对不同的原始文本。6.3 文件系统工具文件读取工具返回内容时要考虑文件过大和二进制内容两种风险。文本文件读取结果适合放入 data但超过阈值时要截断或摘要。路径和文件信息放入 meta。{ status: partial, data: 第一行项目初始化日志\n第二行依赖安装完成\n第三行数据库连接失败截断完整文件共 120 行, error: null, meta: { file_path: /var/log/app.log, total_lines: 120, returned_lines: 3, truncated: true } }文件类工具还应该关注权限错误和文件不存在的情况这两类错误要明确区分并让 LLM 知道下一步是检查路径还是检查权限。6.4 代码执行工具和内置计算代码执行工具返回时应区分 stdout、stderr 和退出码。让 LLM 直接看原始 stderr 效果不好建议合并成结构化结果。{ status: error, data: { stdout: , stderr: NameError: name foo is not defined }, error: { code: EXECUTION_ERROR, message: Python 代码执行失败变量 foo 未定义请检查代码后重试, retryable: true }, meta: { exit_code: 1, duration_ms: 8 } }如果代码执行成功status 设为 success执行结果放入 dataexit_code 为 0 时无需在 data 里重复说明。6.5 记忆检索工具Agent 内部常会有检索历史记忆或向量数据库的工具。这类工具的结果组织很关键因为模型需要判断检索结果与当前问题是否相关。{ status: success, data: [ {score: 0.92, content: 用户偏好使用短信通知}, {score: 0.84, content: 用户上次投诉渠道是邮件} ], error: null, meta: { query: 用户通知偏好, top_k: 2, total_hits: 15 } }这里的关键是让模型知道检索的相关性分数。LLM 可以选择高分段信息作为依据低分段信息宁可忽略。meta 里的 total_hits 则提示检索范围是否有限。7. 常见坑与排查路径结果格式正确但 LLM 仍然答错7.1 现象格式合法但 LLM 理解偏差有时工具结果已经是 JSON结构也完整但 LLM 仍然回答错误。常见的根因有三个。第一个是字段名与值域语义不明确。例如返回plan: pro模型能猜到这是套餐类型但不知道 pro 和 free 的区别。建议在系统提示词里补充字段说明或者在工具描述里写入值域映射。第二个是结果缺少与用户请求的关联。工具返回了大量字段但用户只关心某个字段模型花了很多 Token 去阅读无用信息最后却忽略了关键字段。应对方式是在工具结果里主动突出与查询相关的字段例如用摘要文本先给出直接答案再附上完整细节。第三个是模型推理能力不足以从复杂数据中提取结论。这时问题不在格式而在于数据组织过于复杂。可以考虑把数据预处理成结论形式例如直接返回统计结果而不是返回原始明细让模型自己计算。排查顺序如下检查 status 是否准确是否出现把 empty 当 error 的情况。检查字段名是否为模型可读的自描述名称。检查结果是否包含与判断直接相关的关键字段。检查是否在大段数据中埋没了结论尝试在 data 前部增加一行摘要。7.2 现象结果过大导致上下文溢出或被截断工具结果进入 LLM 上下文后Token 消耗比预想大得多。用户查询数据库返回几百行记录每条记录都包含几十个字段瞬间上下文就被塞满。模型只能看到被截断的前半段后半段数据完全丢失。处理方式工具层做字段裁剪只返回 LLM 决策必需字段不返回数据库原始所有列。设置单次工具返回的最大数据量超过阈值时启用截断或摘要。将大数据分页让 LLM 在需要更多数据时再次调用工具。meta 中标记 truncated 和 total让模型明确知道数据不完整。注意不要在工具结果里返回完整文件内容、完整日志或完整数据库表。任何超过几百行的原始数据都应该先经过摘要、抽样或聚合再进入上下文。7.3 现象错误信息被当成业务数据一个很隐蔽的问题工具内部捕获了异常但没有设置 status直接把异常字符串放在 data 里返回。LLM 分不清这是正常数据还是报错内容会把它当作业务信息回答给用户。典型的错误场景{ status: success, data: Error: java.sql.SQLException: Connection refused }这个结果会让模型认为 data 里的字符串就是有效内容。正确的做法是{ status: error, data: null, error: { code: DB_CONNECTION_REFUSED, message: 数据库连接失败请检查数据库服务与网络配置, retryable: true } }排查时只需要看一个点工具函数是否在异常分支里正确切换了 status。建议在工具函数的入口和出口做强制校验任何异常分支都不得返回 status 为 success 的结果。7.4 工具结果排查清单针对工具结果问题整理一份可复用的排查清单检查项检查重点通过标准status 正确性异常分支是否切到 error错误场景 status 不是 success字段语义字段名与值域是否明确模型不需要额外猜测值含义数据体量结果 Token 是否超过上下文预算关键信息在截断前可被读到错误信息error 是否包含 code、message、retryable模型能判断下一步动作部分结果是否标记 truncated、total模型知道数据不完整摘要突出关键结论是否在结果前部模型优先看到核心信息敏感信息data 是否包含密码、密钥、隐私字段返回前完成过滤脱敏版本一致新老工具结果格式是否兼容旧 prompt 不会因为字段变化失效这张清单可以在每次接入新工具或调整工具结果格式后逐项核对。8. 生产环境落地还要处理的五件事8.1 Token 预算与结果摘要策略学习环境里工具结果可以直接完整返回。生产环境必须为每个工具分配明确的 Token 预算。推荐的策略是为每个工具的进程内返回设置最大 Token 阈值例如 800 Token 或 1500 Token超过阈值就执行摘要。摘要可以只返回统计数字、关键字段和少量样本同时把完整结果写入本地缓存或日志用户或模型需要时再按 ID 查询更完整内容。一个可行做法是在 meta 中增加result_id和truncated字段。模型看到截断标记后可以调用专门工具获取更多数据而不会断言不完整的信息。8.2 结果中的敏感信息过滤工具结果进入 LLM 上下文前必须进行敏感信息过滤。数据库查询很容易返回包含密码哈希、身份证号、手机号、内部 token 等字段的数据这些信息一旦进入第三方模型上下文就会产生安全风险。建议在工具结果组装前统一做一次脱敏处理字段名允许层级但值需要替换例如手机号只显示前后三位密码字段直接置为***内部 token 删除不返回。脱敏逻辑应放在工具函数和 LLM 调用之间统一的拦截层而不是让每个工具各自处理。8.3 格式版本化与兼容工具结果格式升级时最容易出现的问题是旧流程还在用旧格式新 prompt 已经按新格式解析。字段名变化、status 取值变化、嵌套层级变化都会导致模型行为异常。处理方式是在 meta 中加入格式版本号例如format_version: v1。升级格式时保留旧版本兼容期或者通过 prompt 模板按版本解析。任何格式变更都要回归测试确认模型在新旧格式下都不会产生严重错误。8.4 日志与可观测性工具结果对调试 Agent 非常关键。生产环境需要记录每个工具调用的输入参数、结果 status、结果摘要、Token 消耗、耗时和最终模型输出。日志中不要保存完整敏感数据但可以保留脱敏后的结果和错误码。当用户反馈 Agent 回答错误时排查路径通常是先看工具是否被调用再看工具返回了什么最后看模型基于什么信息做出回答。没有工具结果日志这个问题基本无法排查。8.5 针对工具结果的自动化测试工具结果格式不能依赖人工检查应写成自动化测试。每个工具都需要测试成功、失败、空结果、部分结果四条路径并校验返回值是否符合 ToolResult 结构。成功路径status 为 successdata 包含预期字段。失败路径status 为 errorerror 包含 code、message、retryable。空结果status 为 emptydata 为空数组或 null。部分结果status 为 partialmeta 包含截断标记和总数。敏感信息断言返回的 data 不包含脱敏字段。这些测试可以防止工具函数在后续迭代中悄悄改变返回结构破坏 LLM 消费逻辑。工具结果格式要像接口契约一样被测试保护起来。最终回到最初的问题工具向 LLM 报告数据用什么格式好。答案不是一个单一格式而是一套统一的信封结构加上每个工具内部合理的 data 组织。把状态、数据、错误和元信息分开让模型每次都从 status 开始判断再按需读取 data 和 error这是目前项目实践中比较稳妥的路线。如果你刚接入工具调用可以先把文章里的 ToolResult 结构复制到自己的工具层跑通一条链路后再逐步做字段裁剪、Token 预算和自动化测试避免一上来就在所有工具里同时铺开。