结构化输出与工具调用:把语言变成可执行契约
模型可以说:“我准备给订单退款 199 元。”
程序不能凭这句话就动账。它需要知道订单编号、金额类型、币种、原因、调用身份和授权范围;还要判断这是不是重复请求,以及退款后数据库是否真的改变。
工具调用最容易制造一种错觉:模型已经输出了合法 JSON,所以系统已经安全。
合法 JSON 只解决了解析问题。真正的工具契约至少还包括结构校验、业务校验、授权、副作用控制和结果验证。
这一讲讨论的核心不是“怎样让模型吐 JSON”,而是怎样把概率生成的候选动作,逐层变成程序敢执行的动作。
一、从文本约定到结构化调用
Section titled “一、从文本约定到结构化调用”早期 LLM 应用常在 Prompt 里要求:
请严格输出 JSON,不要添加任何解释。然后用字符串解析模型回复。
这种方式的问题很直接:模型可能多写一段说明、漏一个引号、换一个字段名,或者生成解析合法但语义错误的数据。
Function Calling 和 Structured Outputs 改变了接口形式。开发者提供工具名称、描述和参数 Schema,模型返回结构化候选调用。支持受约束解码的模型还可以在生成阶段保证输出符合给定 Schema。[1]
这大幅降低了解析成本,但没有消除应用责任。
下面三个对象必须分开:
Model Output:模型提出的候选参数Validated Request:通过结构与业务检查的请求Executed Action:经过授权并实际执行的动作从第一项到第三项,中间隔着真正的控制面。
二、工具定义不是函数签名那么简单
Section titled “二、工具定义不是函数签名那么简单”一个退款工具可能这样声明:
{ "name": "refund_order", "description": "为符合退款政策的订单发起退款", "parameters": { "type": "object", "properties": { "order_id": {"type": "string"}, "amount": {"type": "number", "exclusiveMinimum": 0}, "currency": {"enum": ["CNY", "USD"]}, "reason": {"type": "string"} }, "required": ["order_id", "amount", "currency", "reason"], "additionalProperties": false }}Schema 能保证字段存在、类型正确、币种属于允许集合,却无法回答:
- 订单是否属于当前用户;
- 实付金额是不是 199 元;
- 当前政策是否允许退款;
- 退款金额是否超过人工审批阈值;
- 这笔订单是否已经退过;
- 当前 Agent 是否拥有写权限。
所以工具契约至少分五层。
第一层:Syntax
Section titled “第一层:Syntax”JSON 能否解析,字段名是否正确。
第二层:Schema
Section titled “第二层:Schema”类型、必填、枚举、长度、范围是否符合声明。
第三层:Business Rules
Section titled “第三层:Business Rules”请求是否符合当前订单、政策、账户和业务状态。
第四层:Authorization
Section titled “第四层:Authorization”当前身份是否有权执行,是否需要人工批准。
第五层:Effect Verification
Section titled “第五层:Effect Verification”动作执行后,环境是否真的达到预期终态。
只做前两层,系统仍然可能“格式完全正确地做错事”。
三、工具描述会直接改变模型行为
Section titled “三、工具描述会直接改变模型行为”模型不是通过阅读源码理解工具,而是通过名称、描述、参数和少量上下文判断何时调用。
下面两个描述都能运行,效果却可能完全不同:
search(query): 搜索信息search_current_policy(query):仅搜索当前生效的官方政策文档;返回标题、生效日期、来源 URL 和摘录。不要用于搜索用户订单或执行写操作。第二个描述明确了数据范围、输出结构和非目标,模型更容易做出正确选择。
工具设计要回答:
- 什么时候应该调用;
- 什么时候不该调用;
- 参数怎样消除歧义;
- 返回结果包含哪些事实;
- 哪些失败可以重试;
- 是否有副作用;
- 是否需要批准。
工具描述不是营销文案,也不是把内部实现全写进去。它是一份面向模型的接口契约。
四、结构化输出解决什么,不解决什么
Section titled “四、结构化输出解决什么,不解决什么”PydanticAI、Instructor 等项目把模型输出映射到类型模型,并利用验证错误触发修正。[2][3]
这条路线有三个明显收益:
- 应用拿到稳定的数据结构;
- 错误可以定位到具体字段;
- 类型模型能进入测试、IDE 和常规代码审查。
但类型系统不能证明事实。
class RefundRequest: order_id: str amount: floatamount=199 符合类型,不代表用户真的支付了 199 元。order_id="order_1024" 是字符串,不代表订单存在或属于当前账户。
结构化输出把语言变成程序可以检查的对象。它没有替程序完成检查。

图 1:现代 Agent SDK 会把普通函数、Schema 和模型 Tool Call 连接起来;执行边界仍由应用负责。来源见文末。
五、工具返回也需要契约
Section titled “五、工具返回也需要契约”很多系统认真设计输入,却让工具随意返回文本:
退款好像成功了。这会把确定性系统重新变成语言猜测。更可靠的结果应该区分:
{ "status": "success", "operation_id": "refund_demo_88", "order_state": "refunded", "amount": 199, "currency": "CNY", "verified_at": "2026-07-20T10:00:00Z"}失败也需要明确错误类型、是否可重试、有没有产生部分副作用。
尤其要警惕 unknown:请求超时并不代表动作没有发生。对于付款、退款、发信、建单等操作,系统应使用幂等键,再查询终态,而不是直接重试。
Timeout is not failure.超时只说明结果未知,不说明动作没有发生。六、MCP 把工具接口标准化以后,责任有没有减少
Section titled “六、MCP 把工具接口标准化以后,责任有没有减少”没有。MCP 解决的是连接和能力交换。
MCP Server 可以暴露 Tools、Resources 和 Prompts。当前规范将 Tools 描述为 model-controlled,但同时要求应用在敏感操作上保留人类确认和安全控制。[4]

图 2:MCP 标准化 Host、Client 与 Server 的连接,不替应用决定业务授权。来源见文末。
一个 MCP Tool 有清晰 Schema,只能说明客户端和服务器能交换请求。它不自动提供:
- 当前用户的业务身份;
- 最小权限;
- 多租户隔离;
- 工具内容是否可信;
- 幂等和事务;
- 结果是否满足上层目标。
协议让工具更容易接入,也让供应链更长。工具越容易安装,来源和权限越要认真检查。
七、并行工具调用为什么更难
Section titled “七、并行工具调用为什么更难”如果两个调用互不影响,并行可以降低延迟。例如同时查询天气和交通。
如果它们共享状态,并行就可能产生竞态:
- 两个 Agent 同时修改同一工单;
- 一个调用读取旧余额,另一个已经完成扣款;
- 两个退款动作使用不同请求 ID;
- 汇总节点先收到后执行的结果。
工具是否可以并行,不应只由模型判断。契约需要声明读取/写入性质、依赖、资源键和冲突规则。
read-only + independent -> usually parallelizablewrite + shared resource -> serialize or lock八、真实实验:合法结构不等于合法业务
Section titled “八、真实实验:合法结构不等于合法业务”本讲的 tool_contract.py 用三个退款对象演示三类结果。
第一个对象结构和业务阈值都通过:
{"valid": true, "errors": []}第二个对象可以写成 JSON,却同时违反订单编号、金额和币种规则:
{ "valid": false, "errors": ["invalid_order_id", "invalid_amount", "unsupported_currency"]}第三个对象字段和类型都正确,但金额超过 500 元,需要人工批准:
{"valid": false, "errors": ["approval_required"]}实验结果见 experiment-result.json。它没有调用模型,因为要验证的正是模型之外的契约。
读者可以继续加三项:订单归属查询、幂等键和执行后终态检查。加完以后,你会发现“Tool Calling”只是完整动作链的第一步。
九、一份可直接使用的工具检查表
Section titled “九、一份可直接使用的工具检查表”- 工具名称是否表达具体动作;
- 描述是否写清适用和非适用场景;
- 参数是否使用明确类型、枚举和范围;
- 返回是否结构化;
- 错误是否区分可重试与不可重试。
- 工具是只读还是写入;
- 当前身份来自哪里;
- 是否采用最小权限;
- 高风险动作是否需要批准;
- Secret 是否与模型上下文隔离。
- 是否有幂等键;
- 超时后怎样查询真实状态;
- 部分成功怎样补偿;
- 并发冲突怎样处理;
- 能否撤销或回滚。
- 工具返回什么终态;
- 上层怎样确认目标完成;
- Trace 是否记录动作而不泄露敏感数据;
- 证据是否绑定当前请求和资源版本。
十、这一讲的结论
Section titled “十、这一讲的结论”工具调用不是让模型拥有一个函数名,而是建立一条从候选决策到真实副作用的受控通道。
Schema 负责让数据可解析,业务规则负责判断动作是否合理,授权决定能不能做,幂等和事务控制后果,终态验证证明有没有做成。
下一讲进入 Planning。工具让 Agent 有了动作空间,规划要解决的是:面对复杂目标,应该怎样选择动作顺序,又怎样在环境变化时放弃旧计划。
[1] OpenAI, Function Calling and Structured Outputs. https://platform.openai.com/docs/guides/function-calling
[2] PydanticAI, Output. https://ai.pydantic.dev/output/
[3] Instructor, Structured Outputs. https://github.com/567-labs/instructor
[4] Model Context Protocol, Tools. https://modelcontextprotocol.io/specification/2025-11-25/server/tools
[5] JSON Schema, Specification. https://json-schema.org/specification
[6] OpenAI Agents SDK, Tools. https://openai.github.io/openai-agents-python/tools/