跳转到内容

结构化输出与工具调用:把语言变成可执行契约

模型可以说:“我准备给订单退款 199 元。”

程序不能凭这句话就动账。它需要知道订单编号、金额类型、币种、原因、调用身份和授权范围;还要判断这是不是重复请求,以及退款后数据库是否真的改变。

工具调用最容易制造一种错觉:模型已经输出了合法 JSON,所以系统已经安全。

合法 JSON 只解决了解析问题。真正的工具契约至少还包括结构校验、业务校验、授权、副作用控制和结果验证。

这一讲讨论的核心不是“怎样让模型吐 JSON”,而是怎样把概率生成的候选动作,逐层变成程序敢执行的动作。

早期 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 是否拥有写权限。

所以工具契约至少分五层。

JSON 能否解析,字段名是否正确。

类型、必填、枚举、长度、范围是否符合声明。

请求是否符合当前订单、政策、账户和业务状态。

当前身份是否有权执行,是否需要人工批准。

动作执行后,环境是否真的达到预期终态。

只做前两层,系统仍然可能“格式完全正确地做错事”。

三、工具描述会直接改变模型行为

Section titled “三、工具描述会直接改变模型行为”

模型不是通过阅读源码理解工具,而是通过名称、描述、参数和少量上下文判断何时调用。

下面两个描述都能运行,效果却可能完全不同:

search(query): 搜索信息
search_current_policy(query):
仅搜索当前生效的官方政策文档;返回标题、生效日期、来源 URL 和摘录。
不要用于搜索用户订单或执行写操作。

第二个描述明确了数据范围、输出结构和非目标,模型更容易做出正确选择。

工具设计要回答:

  • 什么时候应该调用;
  • 什么时候不该调用;
  • 参数怎样消除歧义;
  • 返回结果包含哪些事实;
  • 哪些失败可以重试;
  • 是否有副作用;
  • 是否需要批准。

工具描述不是营销文案,也不是把内部实现全写进去。它是一份面向模型的接口契约。

四、结构化输出解决什么,不解决什么

Section titled “四、结构化输出解决什么,不解决什么”

PydanticAI、Instructor 等项目把模型输出映射到类型模型,并利用验证错误触发修正。[2][3]

这条路线有三个明显收益:

  1. 应用拿到稳定的数据结构;
  2. 错误可以定位到具体字段;
  3. 类型模型能进入测试、IDE 和常规代码审查。

但类型系统不能证明事实。

class RefundRequest:
order_id: str
amount: float

amount=199 符合类型,不代表用户真的支付了 199 元。order_id="order_1024" 是字符串,不代表订单存在或属于当前账户。

结构化输出把语言变成程序可以检查的对象。它没有替程序完成检查。

OpenAI Agents SDK 仓库

图 1:现代 Agent SDK 会把普通函数、Schema 和模型 Tool Call 连接起来;执行边界仍由应用负责。来源见文末。

很多系统认真设计输入,却让工具随意返回文本:

退款好像成功了。

这会把确定性系统重新变成语言猜测。更可靠的结果应该区分:

{
"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]

MCP 稳定规范页面

图 2:MCP 标准化 Host、Client 与 Server 的连接,不替应用决定业务授权。来源见文末。

一个 MCP Tool 有清晰 Schema,只能说明客户端和服务器能交换请求。它不自动提供:

  • 当前用户的业务身份;
  • 最小权限;
  • 多租户隔离;
  • 工具内容是否可信;
  • 幂等和事务;
  • 结果是否满足上层目标。

协议让工具更容易接入,也让供应链更长。工具越容易安装,来源和权限越要认真检查。

如果两个调用互不影响,并行可以降低延迟。例如同时查询天气和交通。

如果它们共享状态,并行就可能产生竞态:

  • 两个 Agent 同时修改同一工单;
  • 一个调用读取旧余额,另一个已经完成扣款;
  • 两个退款动作使用不同请求 ID;
  • 汇总节点先收到后执行的结果。

工具是否可以并行,不应只由模型判断。契约需要声明读取/写入性质、依赖、资源键和冲突规则。

read-only + independent -> usually parallelizable
write + 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 是否记录动作而不泄露敏感数据;
  • 证据是否绑定当前请求和资源版本。

工具调用不是让模型拥有一个函数名,而是建立一条从候选决策到真实副作用的受控通道。

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/