在上一期我们聊到每次 Agent 调用模型前要根据当前的规则、任务状态、工具信息等上下文内容来动态组装本轮 Prompt。工具信息虽然只占当中很小的一部分但它决定了 Agent 能不能把思考变成行动。要让模型“动起来”得先让模型知道有哪些可用的工具这些工具又能做什么以及如何使用这些工具参数应该怎么填写。等工具执行完任务后系统会把执行结果送回上下文方便模型进行下一步的判断。而上面这套工具调用流程之所以能够顺利运行是因为背后有一套明确的工程约定模型应该以什么格式发起调用请求工具又应该以什么格式返回执行结果。这就是本文要讨论的工具调用的输入输出契约。工具调用的双向契约在传统软件开发中API 请求一般会定义好请求参数、返回字段、错误码和权限要求。调用方依据接口文档发送请求服务端再依照约定处理请求并返回结果这样就完成了一次数据交互。Agent 的工具调用逻辑和上面类似只不过调用方从程序变成了大语言模型。一轮完整的工具调用包含以下过程工具定义进入上下文 ↓ 模型选择工具并生成参数 ↓ Harness 校验并执行工具 ↓ 工具返回结果或错误 ↓ 结果进入上下文 ↓ 模型决定下一步在上面这条链路中输入契约作用于工具执行之前约定了模型应该如何调用工具包括但不限于工具名称、使用场景、参数类型、必填字段和限制条件。输出契约作用于工具执行之后约定工具应该返回什么信息。其中返回信息要有结果数据、执行状态、错误信息和必要的元数据。简单来说输入契约约束了模型该怎样去调用工具输出契约让模型知道这次调用产生了什么结果。这样输入输出契约一起决定了模型与工具之间是如何沟通的。如果输入契约不清楚模型可能会选错工具或传错参数。如果输出契约不清楚模型拿到结果后可能无法准确判断本次工具调用是否成功、当前任务是否要继续推进以及发生错误时应该采取什么恢复措施。目前主流的 Agent 架构将工具定义、调用编排、实际执行和输出处理都归入 Harness 的职责范围模型负责理解工具的结构化定义再生成调用请求而 Harness 负责执行工具调用请求并处理输出并将结果送回循环。输入契约的结构组成一个工具的输入契约最重要的是要告诉模型这个工具叫什么以及它能完成什么任务。例如一个天气查询工具可以写成{ name: get_weather, description: 查询指定城市当前的天气情况, input_schema: { type: object, properties: { city: { type: string, description: 城市名称例如 Beijing }, unit: { type: string, enum: [celsius, fahrenheit], description: 温度单位 } }, required: [city] } }这个定义当中的关键部分有name即工具名称。作为模型识别和调用工具的唯一标识名称最好采用“动作 对象”的结构让模型一眼看出这个工具要做什么、作用于什么对象。参考search_web、read_file、send_email。像process、handle这类宽泛的名称会让模型难以判断它适合什么任务。description即工具描述。主要用来说明工具的能力、适用场景和使用边界。一个工具描述如果只写“搜索内容”是远远不够的更清楚的表达是“这个工具是用于搜索公开网络上的实时信息请不要用于查询公司内部资料。”当模型选择工具时除了参考当前任务之外会结合工具名称和工具描述进行判断。input_schema即参数 Schema。它定义了参数整体的数据结构以及每个字段的类型、含义和约束。比如上面示例的properties就定义了city和unit两个参数enum将温度单位限制在celsius和fahrenheit之间。required即必填字段列表。示例中的required: [city]表示调用这个工具时必须提供属性properties中的city而unit可以不填。这里提醒一点required是input_schema的一部分。除了 JSON Schema 中的参数约束工具定义还需要说明调用这个工具可能会产生的影响。例如发送邮件工具会把内容发给真实收件人执行工具调用之前要和用户进行确认网页抓取工具可能耗时会比较久删除文件工具会产生不可逆的结果。诸如此类的使用边界可以写进工具描述也可以通过额外的工具元数据和 Harness 权限规则讲清楚。讲清楚使用边界之后还要进一步降低模型填写参数时的理解成本。可以通过工具定义加入真实的调用示例来降低理解成本。虽然 Schema 能说明字段类型却难以说明时间戳的单位是使用秒还是毫秒、电话号码是否需要国家代码、嵌套过滤条件该如何组合。给出具体示例可以把这些隐含的约定直接展示给模型减少它临时猜测参数格式的情况。一般来说工具描述还要覆盖使用时机、边界、参数示例、返回值和执行代价。一旦模型频繁选错工具我们应该先检查工具是否定义清楚再考虑模型能力。参数语义与传递保真输入契约除了要保证格式正确还要保证参数从模型传到工具的过程中保持语义的一致。在实际的工具调用链路中模型生成 JSON 参数后中间的 Agent 运行层通常还会经过多个处理步骤。例如Harness 或 SDK 可能会进行序列化、类型转换、默认值补充、编码处理等操作。如果这些处理在模型不知情的情况下改变了参数内容模型理解的状态就会和工具实际执行的状态产生偏差。举个例子一个文件替换工具要求模型提供old_string和new_string两个参数。模型读取文件时发现原文使用的是中文弯引号没做思考直接将这段文本原样传入old_string。但这里有一个隐藏机制是参数传递层会在执行前自动将中文弯引号转换成英文直引号直传的话会导致工具无法匹配文件中的原始内容。从模型视角来看它已经根据读取到的信息生成了正确参数但从工具视角来看实际收到的参数已经发生了变化因此工具会持续返回“未找到匹配”。这可苦了 Agent要不断地重试工具调用却难以察觉问题在于中间的参数转换。类似的传递偏差情况还包括将秒级时间戳自动转换为毫秒却没有明确告知模型自动清除查询字符串中的特殊字符在 Shell 命令后追加模型没有生成的参数自动修改文件编码或换行符将空字符串、null和字段缺失视为同一种情况。这些“智能”的修正可能会让单次调用更加方便但也会降低工具调用的可解释性和可调试性。如果确实要对参数进行规范化处理转换规则应该明确写入工具描述工具执行后也应该返回实际采用的参数让模型知道系统最终执行了什么。因此参数保真关注的不只是“字段有没有传过去”还包括参数的值、编码、单位、顺序以及语义是否在整个调用链路中保持一致。输出契约的结果表达输入契约负责解决“工具怎么调用”输出契约负责回答“工具做完后发生了什么”。一个工具执行完只返回“成功”或“失败”信息是无法为 Agent 的下一步决策提供足够信息进行判断的。比较合理的输出应该包含明确的状态、结构化数据和必要的执行细节。参考{ status: success, data: { city: Vancouver, temperature: 13.2, unit: celsius, conditions: clear } }当工具执行失败时也应该返回稳定的错误结构{ status: error, error: { code: RATE_LIMIT, message: 请求频率超过服务限制, retryable: true } }这样的输出结果让 Harness 和模型能够做出更明确的判断当前调用是否成功、数据放在哪里、错误属于临时故障还是永久失败、是否值得重试。输出契约还要区分工具执行成功和任务结果正确。例如write_file返回成功只能说明文件写入动作完成。至于写入的代码是否能编译通过、是否符合项目规范这是需要进一步验证的。因此文件编辑工具除了完成写入操作外还可以在执行后主动运行语法检查并返回结构化的错误信息帮助 Agent 在下一轮调用中快速定位问题并完成修复。对于文件读取、网页抓取、数据库查询和命令执行等工具返回结果可能包含大量内容。为了避免过长的输出占用上下文空间工具可以对结果进行截断或压缩但截断过程必须明确告知模型。例如已返回第 1—200 行共 5000 行。 剩余内容可通过 offset200 继续读取。静默截断会让模型误以为自己获得了完整信息从而基于不完整的结果做判断。因此工具在截断返回内容时要明确告知模型当前结果是否完整并提供继续获取剩余内容的方式。对于信息获取类和任务执行类工具合理的做法还包括保留完整结果、进行自动验证以及在结果进入上下文前完成解析、Schema 校验、长输出压缩和错误标准化。错误反馈与恢复信号工具执行失败是 Agent 运行中的常见问题。毕竟网络会超时接口会限流文件可能不存在参数也可能无法通过校验这些意外情况都会导致工具执行失败。一个良好的输出契约不会把所有失败都压缩成一句“调用失败”而是会提供足够的恢复信号这个错误发生在哪个阶段使用了哪些实际参数错误类型和错误码是什么该错误是否需要重试工具调用本次调用是否已经产生部分结果下一步可以采取什么措施。举个例子文件不存在和权限不足这两种情况都表现为读取失败但 Agent 的处理方式完全不同。如果是文件不存在Agent 可以搜索正确路径。如果是权限不足Agent 可能要请求用户授权。如果是网络超时Harness 就可以尝试先自动重试。如果是参数不合法这时候应该让模型重新生成参数。不同服务商 / 不同模型服务商返回的错误格式并不统一因此 Harness 可以负责将这些错误转换成统一结构并完成指数退避、备用工具调用和优雅降级等基础设施恢复。这样模型接收到的是稳定、可理解的错误信息就不需要在每次任务中重新推断某个底层 API 的错误含义。除了错误处理工具结果还需要和原始调用建立明确关联。当模型在同一轮中并行查询天气和时间时两个返回结果必须分别绑定对应的调用 ID。否则模型可能无法判断某个结果对应的是哪个工具调用尤其是在并行工具调用或多 Agent 协作场景中。ReAct 循环中的契约闭环工具调用的输入输出契约最终会嵌入 Agent 的 ReAct 循环。模型会先根据工具定义生成结构化调用请求等 Harness 校验完参数后会执行工具再把调用工具的执行返回结果作为新的消息追加到上下文。这样下一轮模型能看到自己此前发出的请求和工具返回的结果并根据结果判断是否继续调用工具是否要修改参数重新调用工具还是切换其他工具生成最终回答Harness 层的校验职责虽然结构化的 Schema 能帮模型生成调用工具的正确参数但它不能代替运行时校验格式。模型生成的参数仍然存在越界、缺失或包含危险内容这些情况所以在真正执行工具调用之前Harness 需要再次检查参数是否合法、安全。Harness 常见的校验工作包括检查字段类型、必填项和枚举值文件路径、SQL 和 Shell 参数的安全过滤检查工具权限和副作用请求用户确认高风险操作确定执行时长、内存和调用次数限制校验返回结果的 Schema错误归一化与自动重试工具调用参数、结果和时间戳的审计记录。Harness 的校验并不是简单检查参数格式而是在模型和真实环境之间增加一道安全边界。在真正执行工具之前Harness 和工具服务都要根据声明的 JSON Schema 对输入进行校验并防范路径穿越、SQL 注入、命令注入、内部网络请求等风险。同时工具还可以附带只读、破坏性、幂等性等属性注解帮助 Harness 判断一次调用是否可以自动执行、是否支持安全重试以及是否需要人工确认。模型负责提出行动Harness 负责保证这个行动符合工具契约。两者共同决定了一次工具调用能否从“参数格式正确”进一步变成“执行过程安全可靠”。结语工具调用的输入输出契约决定了 Agent 如何理解工具、调用工具以及根据结果继续行动。它不只约束参数格式还需要明确工具能力、使用边界、执行结果和错误反馈。对 Agent 来说工具设计最终要回答三个问题什么时候应该使用这个工具调用时需要提供什么信息执行后的结果应该如何理解。只有这些约定清晰工具才能成为 Agent 可以稳定使用的能力。
Agent 小知识|让 Agent 调对工具:输入输出契约的设计
在上一期我们聊到每次 Agent 调用模型前要根据当前的规则、任务状态、工具信息等上下文内容来动态组装本轮 Prompt。工具信息虽然只占当中很小的一部分但它决定了 Agent 能不能把思考变成行动。要让模型“动起来”得先让模型知道有哪些可用的工具这些工具又能做什么以及如何使用这些工具参数应该怎么填写。等工具执行完任务后系统会把执行结果送回上下文方便模型进行下一步的判断。而上面这套工具调用流程之所以能够顺利运行是因为背后有一套明确的工程约定模型应该以什么格式发起调用请求工具又应该以什么格式返回执行结果。这就是本文要讨论的工具调用的输入输出契约。工具调用的双向契约在传统软件开发中API 请求一般会定义好请求参数、返回字段、错误码和权限要求。调用方依据接口文档发送请求服务端再依照约定处理请求并返回结果这样就完成了一次数据交互。Agent 的工具调用逻辑和上面类似只不过调用方从程序变成了大语言模型。一轮完整的工具调用包含以下过程工具定义进入上下文 ↓ 模型选择工具并生成参数 ↓ Harness 校验并执行工具 ↓ 工具返回结果或错误 ↓ 结果进入上下文 ↓ 模型决定下一步在上面这条链路中输入契约作用于工具执行之前约定了模型应该如何调用工具包括但不限于工具名称、使用场景、参数类型、必填字段和限制条件。输出契约作用于工具执行之后约定工具应该返回什么信息。其中返回信息要有结果数据、执行状态、错误信息和必要的元数据。简单来说输入契约约束了模型该怎样去调用工具输出契约让模型知道这次调用产生了什么结果。这样输入输出契约一起决定了模型与工具之间是如何沟通的。如果输入契约不清楚模型可能会选错工具或传错参数。如果输出契约不清楚模型拿到结果后可能无法准确判断本次工具调用是否成功、当前任务是否要继续推进以及发生错误时应该采取什么恢复措施。目前主流的 Agent 架构将工具定义、调用编排、实际执行和输出处理都归入 Harness 的职责范围模型负责理解工具的结构化定义再生成调用请求而 Harness 负责执行工具调用请求并处理输出并将结果送回循环。输入契约的结构组成一个工具的输入契约最重要的是要告诉模型这个工具叫什么以及它能完成什么任务。例如一个天气查询工具可以写成{ name: get_weather, description: 查询指定城市当前的天气情况, input_schema: { type: object, properties: { city: { type: string, description: 城市名称例如 Beijing }, unit: { type: string, enum: [celsius, fahrenheit], description: 温度单位 } }, required: [city] } }这个定义当中的关键部分有name即工具名称。作为模型识别和调用工具的唯一标识名称最好采用“动作 对象”的结构让模型一眼看出这个工具要做什么、作用于什么对象。参考search_web、read_file、send_email。像process、handle这类宽泛的名称会让模型难以判断它适合什么任务。description即工具描述。主要用来说明工具的能力、适用场景和使用边界。一个工具描述如果只写“搜索内容”是远远不够的更清楚的表达是“这个工具是用于搜索公开网络上的实时信息请不要用于查询公司内部资料。”当模型选择工具时除了参考当前任务之外会结合工具名称和工具描述进行判断。input_schema即参数 Schema。它定义了参数整体的数据结构以及每个字段的类型、含义和约束。比如上面示例的properties就定义了city和unit两个参数enum将温度单位限制在celsius和fahrenheit之间。required即必填字段列表。示例中的required: [city]表示调用这个工具时必须提供属性properties中的city而unit可以不填。这里提醒一点required是input_schema的一部分。除了 JSON Schema 中的参数约束工具定义还需要说明调用这个工具可能会产生的影响。例如发送邮件工具会把内容发给真实收件人执行工具调用之前要和用户进行确认网页抓取工具可能耗时会比较久删除文件工具会产生不可逆的结果。诸如此类的使用边界可以写进工具描述也可以通过额外的工具元数据和 Harness 权限规则讲清楚。讲清楚使用边界之后还要进一步降低模型填写参数时的理解成本。可以通过工具定义加入真实的调用示例来降低理解成本。虽然 Schema 能说明字段类型却难以说明时间戳的单位是使用秒还是毫秒、电话号码是否需要国家代码、嵌套过滤条件该如何组合。给出具体示例可以把这些隐含的约定直接展示给模型减少它临时猜测参数格式的情况。一般来说工具描述还要覆盖使用时机、边界、参数示例、返回值和执行代价。一旦模型频繁选错工具我们应该先检查工具是否定义清楚再考虑模型能力。参数语义与传递保真输入契约除了要保证格式正确还要保证参数从模型传到工具的过程中保持语义的一致。在实际的工具调用链路中模型生成 JSON 参数后中间的 Agent 运行层通常还会经过多个处理步骤。例如Harness 或 SDK 可能会进行序列化、类型转换、默认值补充、编码处理等操作。如果这些处理在模型不知情的情况下改变了参数内容模型理解的状态就会和工具实际执行的状态产生偏差。举个例子一个文件替换工具要求模型提供old_string和new_string两个参数。模型读取文件时发现原文使用的是中文弯引号没做思考直接将这段文本原样传入old_string。但这里有一个隐藏机制是参数传递层会在执行前自动将中文弯引号转换成英文直引号直传的话会导致工具无法匹配文件中的原始内容。从模型视角来看它已经根据读取到的信息生成了正确参数但从工具视角来看实际收到的参数已经发生了变化因此工具会持续返回“未找到匹配”。这可苦了 Agent要不断地重试工具调用却难以察觉问题在于中间的参数转换。类似的传递偏差情况还包括将秒级时间戳自动转换为毫秒却没有明确告知模型自动清除查询字符串中的特殊字符在 Shell 命令后追加模型没有生成的参数自动修改文件编码或换行符将空字符串、null和字段缺失视为同一种情况。这些“智能”的修正可能会让单次调用更加方便但也会降低工具调用的可解释性和可调试性。如果确实要对参数进行规范化处理转换规则应该明确写入工具描述工具执行后也应该返回实际采用的参数让模型知道系统最终执行了什么。因此参数保真关注的不只是“字段有没有传过去”还包括参数的值、编码、单位、顺序以及语义是否在整个调用链路中保持一致。输出契约的结果表达输入契约负责解决“工具怎么调用”输出契约负责回答“工具做完后发生了什么”。一个工具执行完只返回“成功”或“失败”信息是无法为 Agent 的下一步决策提供足够信息进行判断的。比较合理的输出应该包含明确的状态、结构化数据和必要的执行细节。参考{ status: success, data: { city: Vancouver, temperature: 13.2, unit: celsius, conditions: clear } }当工具执行失败时也应该返回稳定的错误结构{ status: error, error: { code: RATE_LIMIT, message: 请求频率超过服务限制, retryable: true } }这样的输出结果让 Harness 和模型能够做出更明确的判断当前调用是否成功、数据放在哪里、错误属于临时故障还是永久失败、是否值得重试。输出契约还要区分工具执行成功和任务结果正确。例如write_file返回成功只能说明文件写入动作完成。至于写入的代码是否能编译通过、是否符合项目规范这是需要进一步验证的。因此文件编辑工具除了完成写入操作外还可以在执行后主动运行语法检查并返回结构化的错误信息帮助 Agent 在下一轮调用中快速定位问题并完成修复。对于文件读取、网页抓取、数据库查询和命令执行等工具返回结果可能包含大量内容。为了避免过长的输出占用上下文空间工具可以对结果进行截断或压缩但截断过程必须明确告知模型。例如已返回第 1—200 行共 5000 行。 剩余内容可通过 offset200 继续读取。静默截断会让模型误以为自己获得了完整信息从而基于不完整的结果做判断。因此工具在截断返回内容时要明确告知模型当前结果是否完整并提供继续获取剩余内容的方式。对于信息获取类和任务执行类工具合理的做法还包括保留完整结果、进行自动验证以及在结果进入上下文前完成解析、Schema 校验、长输出压缩和错误标准化。错误反馈与恢复信号工具执行失败是 Agent 运行中的常见问题。毕竟网络会超时接口会限流文件可能不存在参数也可能无法通过校验这些意外情况都会导致工具执行失败。一个良好的输出契约不会把所有失败都压缩成一句“调用失败”而是会提供足够的恢复信号这个错误发生在哪个阶段使用了哪些实际参数错误类型和错误码是什么该错误是否需要重试工具调用本次调用是否已经产生部分结果下一步可以采取什么措施。举个例子文件不存在和权限不足这两种情况都表现为读取失败但 Agent 的处理方式完全不同。如果是文件不存在Agent 可以搜索正确路径。如果是权限不足Agent 可能要请求用户授权。如果是网络超时Harness 就可以尝试先自动重试。如果是参数不合法这时候应该让模型重新生成参数。不同服务商 / 不同模型服务商返回的错误格式并不统一因此 Harness 可以负责将这些错误转换成统一结构并完成指数退避、备用工具调用和优雅降级等基础设施恢复。这样模型接收到的是稳定、可理解的错误信息就不需要在每次任务中重新推断某个底层 API 的错误含义。除了错误处理工具结果还需要和原始调用建立明确关联。当模型在同一轮中并行查询天气和时间时两个返回结果必须分别绑定对应的调用 ID。否则模型可能无法判断某个结果对应的是哪个工具调用尤其是在并行工具调用或多 Agent 协作场景中。ReAct 循环中的契约闭环工具调用的输入输出契约最终会嵌入 Agent 的 ReAct 循环。模型会先根据工具定义生成结构化调用请求等 Harness 校验完参数后会执行工具再把调用工具的执行返回结果作为新的消息追加到上下文。这样下一轮模型能看到自己此前发出的请求和工具返回的结果并根据结果判断是否继续调用工具是否要修改参数重新调用工具还是切换其他工具生成最终回答Harness 层的校验职责虽然结构化的 Schema 能帮模型生成调用工具的正确参数但它不能代替运行时校验格式。模型生成的参数仍然存在越界、缺失或包含危险内容这些情况所以在真正执行工具调用之前Harness 需要再次检查参数是否合法、安全。Harness 常见的校验工作包括检查字段类型、必填项和枚举值文件路径、SQL 和 Shell 参数的安全过滤检查工具权限和副作用请求用户确认高风险操作确定执行时长、内存和调用次数限制校验返回结果的 Schema错误归一化与自动重试工具调用参数、结果和时间戳的审计记录。Harness 的校验并不是简单检查参数格式而是在模型和真实环境之间增加一道安全边界。在真正执行工具之前Harness 和工具服务都要根据声明的 JSON Schema 对输入进行校验并防范路径穿越、SQL 注入、命令注入、内部网络请求等风险。同时工具还可以附带只读、破坏性、幂等性等属性注解帮助 Harness 判断一次调用是否可以自动执行、是否支持安全重试以及是否需要人工确认。模型负责提出行动Harness 负责保证这个行动符合工具契约。两者共同决定了一次工具调用能否从“参数格式正确”进一步变成“执行过程安全可靠”。结语工具调用的输入输出契约决定了 Agent 如何理解工具、调用工具以及根据结果继续行动。它不只约束参数格式还需要明确工具能力、使用边界、执行结果和错误反馈。对 Agent 来说工具设计最终要回答三个问题什么时候应该使用这个工具调用时需要提供什么信息执行后的结果应该如何理解。只有这些约定清晰工具才能成为 Agent 可以稳定使用的能力。