3046 字
约 10 分钟
21
S22-A08 · 结构化输出与工具调用:可靠性设计

"解析失败率不到 1%,可以上生产了。"错的不是数字,是口径。1% 的调用级失败率,在 12 次调用的任务里会放大成约 11% 的任务级失败率;失败那步若已写下游,损失再乘一次。

结构化输出的可靠性,不是"怎么让模型输出对",而是在哪兜、兜到什么程度、兜不住谁接手——三件都是架构决策。


1. 先换口径:调用级失败率没有决策价值

口径 算法 决策用途
调用级失败率 失败调用 / 总调用 只能看趋势
任务级成功率 一任务内全部调用成功的比例 唯一可上线的口径
失败放大系数 1-(1-p)^n 判断该不该上重试
静默错误率 解析成功但语义错的比例 决定要不要加语义校验
每任务调用数 n p=1% p=3%
3 ≈3% ≈9%
8 ≈8% ≈22%
20 ≈18% ≈46%

二项近似,实际随任务结构变化。表只说明一件事:n 大时,减少 n 或加兜底的收益远大于优化 p。


2. 三种实现的可靠性上限

实现 上限由谁决定 能保证 保证不了
提示约束 模型服从度 大致格式 字段完整、类型、嵌套
JSON mode 平台解码器 语法合法 JSON 你的 schema、字段语义
schema 校验的函数调用 平台 + 你的 schema 结构形状 值是否合理、该不该调此工具

没有一条能保证语义正确。选型判据不是"哪个最可靠",而是"哪层的失败我能自动兜住"。函数调用把失败从解析期推到校验期——这才是它的价值:位置更靠前、错误结构化、兜底好写。


3. "合法 JSON 但不合我的 schema"

这类问题全部发生在"解析成功"之后:

形态 例子 检出手段
类型漂移 数字给成 "42" 类型校验
枚举外值 "urgent " 带空格或给同义词 归一化后再校验
必填缺失 省略语义必需的字段 业务校验
语义越界 amount 合法但为负 领域规则
结构合法但意图错 调 refund 指向错单 交叉校验、对账

前三类靠校验器与领域规则;第五类只能靠外部不变量(退款额不得超原单)。当 schema 问题处理,是最贵的一处误判。


4. schema 形状就是失败率参数

嵌套、可选、枚举、联合类型各自贡献失败概率,且是乘性叠加:

形状 影响 设计判据
嵌套 > 3 层 高,内层最易漏 压平成并列字段,或拆两次调用
可选字段多 中,倾向省略 改"必填 + 显式 null"
枚举 > 10 个值 高,长尾易错 拆两级,或改自由文本 + 代码映射
联合类型 oneOf 高,判别字段常丢 加显式 type 字段,禁靠存在性推断
长数组 > 20 项 高,尾部截断或合并 分页,或只返 id

判据:每加一层结构,就在兜底预算里加一笔。 表达力与生成成功率是一对取舍,决策要做的是给预算标价,不是追求最精确 schema。


5. 校验失败的三级处理与责任归属

级别 动作 成本 责任方 适用
L1 重试 带校验错误重问一次 一次调用 编排层 结构性违规、可定位
L2 宽松解析 修补/默认值/丢字段 低,引脏数据 代码 非关键字段、下游容忍
L3 转人工 挂起并交接 人力 + 延迟 业务方 关键字段、有副作用
# contract-policy.yaml —— 失败由哪层兜,写在配置而不是散在代码里
fields:
  order_id: {required: true,  on_invalid: L1_then_L3}
  amount:   {required: true,  on_invalid: L3}        # 涉钱,禁自动修补
  remark:   {required: false, on_invalid: L2_default_null}
  tags:     {required: false, on_invalid: L2_drop}
retry:
  max: 1
  feedback: validation_errors    # 回灌错误清单,不是原样重发

归属要写下来。散在代码里的兜底,半年后没人知道某字段为何被静默置空。


6. 重试的提示设计

原样重发等于赌同一颗骰子。有效重试至少要带三样:

你上一次的输出被校验拒绝,原因如下:
- $.items[2].price: 期望 number,实际 "12.5元"
- $.items[3]: 缺少必填字段 sku
只修正上述问题,其余字段保持不变。

错误路径、期望与实际、修正范围。 少了"其余保持不变",模型会重写整份输出,把正确字段也改坏——长输出里的常见二次故障。


7. 流式与结构化的冲突

流式只保证"增量可解析",不保证"增量可校验":字段流到一半永远不完整。

需求 可行做法
边流边展示结构 只展示已闭合字段
边流边执行工具调用 不做,参数必须完整
长输出的进度感 流式展示摘要,结构部分最后统一校验

判据:凡"读到就能用"的假设,在流式下都不成立。 所以两条通道通常要拆开。


8. 参数校验必须由代码做

模型给的参数是输入数据,不是可信调用:

层 校验什么 由谁 失败后
结构层 类型、必填、枚举、范围 schema 校验器 回灌重试一次
语义层 权限、归属、状态机前置条件 业务代码 直接拒绝,不回灌

语义层回灌等于说"换个参数再试",会把校验失败变成越权探测。结构层可重试,语义层硬拒。


9. 工具集合规模与选择准确率

工具数 典型表现 治理手段
< 10 基本稳定 描述写清即可
10–30 相近工具开始混淆 分组 + 描述写"何时不用"
30–100 明显退化,误选集中在少数几对 两级路由:先选组再选工具
> 100 不可靠 按任务裁剪工具集,只暴露子集

判据:工具集按任务上下文裁剪,而不是一次全暴露。 代价不止准确率,还有每次多付的 schema token。


10. 工具描述是接口文档

写法 影响
只写功能 与相近工具混淆
写"何时用 + 何时不用" 显著降低误选
写清返回结构与失败语义 减少二次调用
塞实现细节 噪声,降低准确率

最值钱的是否定条件("别用它做 X,用 Y"):把边界交给模型,成本最低。


11. 多工具返回格式不一致的治理

{
  "ok": true,
  "data": { "...": "工具自身结构" },
  "error": {"code": "RATE_LIMITED", "retryable": true, "message": "..."},
  "meta": {"tool": "refund.create", "latency_ms": 231, "idem_key": "..."}
}

外层统一信封(ok/data/error/meta),内层保留工具结构。编排层只处理一种壳,工具演进不影响调用方;代价是多层包装,换错误处理只写一次。


12. 幂等:模型会重复调用同一工具

重复调用是常态:重试、纠错、并行分支都会产生。按副作用代价分档:

副作用 处置 实现
只读 允许重复 无
幂等写(upsert) 允许重复 自然键
非幂等写 幂等键 + 去重窗口 会话+工具+规范化参数哈希
不可撤销(发信、转账) 二次确认 + 单次令牌 令牌一次性
tool_side_effects:
  refund.create: {class: non_idempotent, idem_key: required}
  refund.notify: {class: irreversible,   single_use_token: true}
  order.get:     {class: read_only,      retry: free}

表与工具注册表放一起,编排层调用前读到档位。事后判断"是否重复"成本高得多。


13. 可观测:三个必看指标

指标 定义 指向
任务级成功率 全链无失败占比 唯一对用户可见的指标
schema 违规分布 按字段/类型聚合 具体 schema 设计缺陷
工具选择错误率 标注或下游异常反推 描述与工具集规模
{"run":"r-0412","step":3,"tool":"refund.create","schema_ok":false,"violations":[{"path":"$.amount","type":"type_mismatch"}],"handled":"L1","retry":1,"ok":true}

handled 必须记。只统计失败次数,回答不了"兜底是否有效"。


14. 测试策略

集合 规模(按版本核对) 维护成本 抓什么
黄金回归集 50–200 条固定输入 低 改动后的行为漂移
对抗样本 长尾、超长、歧义 中 边界崩溃
历史失败回放 线上真实失败样本 低(自动沉淀) 同类复发
{"id":"c-017","input":"把订单 8821 的 300 元退款","expect":{"tool":"refund.create","args":{"amount":300}}}
{"id":"c-018","input":"订单 8821 退一半","expect":{"tool":"refund.create","args":{"amount":190}},"note":"歧义,允许提问"}

第三类最划算也最常被忽略:线上失败都进回归集,那是唯一反映真实分布的数据。


15. 排错

现象 常见原因 处理
解析一直成功,下游偶发脏数据 只校结构不校语义 补领域规则,升到 L3
重试后字段变差 提示没限定修正范围 加"其余保持不变"
某字段违规率特别高 该字段 optional 或嵌套过深 改必填、压平层级
两个工具偶发选错同一个 描述高度重合 补"何时不用"或合并
同一订单被退两次 非幂等且无幂等键 上 key + 去重窗口
p 没变但投诉变多 看的是调用级口径 改看任务级成功率

16. 边界

场景 判断
输出仅用于展示,人眼会扫 不必严格 schema,宽松解析
下游本就容忍缺失字段 只校"关键字段存在"
有稳定人工复核环节 校验可放宽,成本转给复核
单次调用、无副作用 重试一次足够,不要三级兜底
输出驱动不可逆动作 反过来:严 schema + 二次确认 + 单次令牌

这套设计有一条对称性:可靠性预算应与失败的不可逆程度成正比,而不是与"业务重要性"这种模糊说法挂钩。


自测

  1. 为什么调用级失败率不能作为上线判据?用 n=20 说明差距。
  2. "合法 JSON 但不合 schema"的五种形态里,哪种校验器永远抓不到?
  3. 为什么"函数调用更可靠"要打折扣?它把失败推到了哪一层?
  4. 嵌套从 2 层增到 4 层,兜底预算该怎么变?依据是什么?
  5. 工具数超过 30 后,为什么"按任务裁剪工具集"比"优化单个描述"更有效?
  6. 可靠性预算与"不可逆程度"对齐、与"业务重要性"对齐,会得出哪两种不同设计?
S22-A08 · 结构化输出与工具调用:可靠性设计
https://www.clxhxhhr.top/posts/6199/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。