在上一期我们聊到每次 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 行。
剩余内容可通过 offset=200 继续读取。
静默截断会让模型误以为自己获得了完整信息,从而基于不完整的结果做判断。因此,工具在截断返回内容时,要明确告知模型当前结果是否完整,并提供继续获取剩余内容的方式。对于信息获取类和任务执行类工具,合理的做法还包括保留完整结果、进行自动验证,以及在结果进入上下文前完成解析、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 可以稳定使用的能力。