跳转到内容

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

先把一个看似简单的动作放到桌面上:模型说“给订单退款 199 元”。

这句话听起来很完整,程序却还不能动账。订单是谁的,199 是不是实付金额,当前身份能不能写入,超时后该不该重试,退款记录是否真的落库,这些都还没有答案。

Tool Calling 最容易让人放松警惕的地方也在这里: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 是否拥有写权限?

函数签名只是入口。一个能落地的工具契约至少要经过五层检查:语法和 Schema 先保证“能读”,业务规则、授权和终态验证才真正决定“该不该做、做完算不算成功”。

只做前两层,系统仍然可能格式完全正确地做错事。这是工具调用里最昂贵的一类错误。

模型靠什么判断该不该点这个工具

Section titled “模型靠什么判断该不该点这个工具”

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

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

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

第二个描述明确了数据范围、输出结构和非目标。模型没读过你的源码,它只能据此判断:现在该搜什么,以及什么事不该由它来做。

工具设计要回答什么时候应该调用、什么时候不该调用、参数怎样消除歧义、返回结果包含哪些事实、哪些失败可以重试、是否有副作用、是否需要批准–但它不是营销文案,也不必把内部实现全塞进去。它更像一张写给执行者的工作卡:动作边界、输入含义、可观察结果和禁区都要清楚。

PydanticAI、Instructor 等项目把模型输出映射到类型模型,并利用验证错误触发修正。[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 连接起来;执行边界仍由应用负责。来源见文末。

别把执行结果重新写回一句人话

Section titled “别把执行结果重新写回一句人话”

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

退款好像成功了。

这会把刚建立的确定性边界重新变成语言猜测。调用方至少要能区分成功、失败和未知,并能拿着 operation_id 回查终态:

{
"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 解决的是连接和能力交换。MCP Server 可以暴露 Tools、Resources 和 Prompts。当前规范将 Tools 描述为 model-controlled,但同时要求应用在敏感操作上保留人类确认和安全控制。[4]

MCP 稳定规范页面

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

一个 MCP Tool 有清晰 Schema,只能说明客户端和服务器能交换请求。它不自动提供当前用户的业务身份、最小权限、多租户隔离、工具内容是否可信、幂等和事务,或者结果是否满足上层目标。

协议让工具更容易接入,也让供应链更长。一个 Tool 能连通,不等于它值得信任;来源、身份和权限仍要由接入方逐项确认。

看到“可并行”前,先问它们会不会碰同一份状态

Section titled “看到“可并行”前,先问它们会不会碰同一份状态”

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

如果它们共享状态,并行就可能产生竞态:两个 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 只是完整动作链的第一步。

关于工具定义,要检查工具名称是否表达具体动作、描述是否写清适用和非适用场景、参数是否使用明确类型/枚举/范围、返回是否结构化、错误是否区分可重试与不可重试。

关于权限,要明确工具是只读还是写入、当前身份来自哪里、是否采用最小权限、高风险动作是否需要批准、Secret 是否与模型上下文隔离。

关于副作用,要确认是否有幂等键、超时后怎样查询真实状态、部分成功怎样补偿、并发冲突怎样处理、能否撤销或回滚。

关于证据,要想清楚工具返回什么终态、上层怎样确认目标完成、Trace 是否记录动作而不泄露敏感数据、证据是否绑定当前请求和资源版本。

收尾:把 Tool Call 当成一份待证实的提案

Section titled “收尾:把 Tool Call 当成一份待证实的提案”

工具调用不是给模型发一张函数名清单,而是建立一条从候选决策到真实副作用的受控通道:Schema 让数据可解析,业务规则判断动作是否合理,授权决定能不能做,幂等和事务限制后果,终态验证才证明有没有做成。

这里有个边界要留在脑中:再严格的 Schema 也替代不了身份、账务、合规和人工审批。它们需要落在应用和环境里,而不是交给模型在文字里承诺。

下一讲会把这些工具放进更长的任务里。动作有了,接下来要解决的是:面对依赖、变化和预算,Agent 怎样决定先做哪一步,又何时放弃旧计划。