函数调用与结构化输出——可靠性工程实践
函数调用与结构化输出的可靠性工程。从训练、提示、解码三层说明实现机制,归纳六类失败模式,给出运行时校验、重试与兜底方案及评测指标体系。
一 问题定义
让模型"调用正确的函数、填正确的参数、输出合法的 JSON",是 Agent 工程里最基础的一颗螺丝。业界对它的关注从"能不能"转向"够不够稳":工具选择准确率、参数 schema 合规率、结构化输出的可解析率,三者合起来决定一条 Agent 链能否无人值守。本文梳理从工具描述设计、约束解码到评测闭环的工程实践。
二 函数调用的三种实现层
| 层级 | 机制 | 保障强度 | 典型形态 |
|---|---|---|---|
| 训练层 | RLHF / 指令微调让模型学会工具调用分布 | 概率性(最常见失败仍在这里) | GPT / Claude / Gemini 的原生 function calling |
| 提示层 | 工具 schema 以 JSON Schema 注入系统提示,引导输出格式 | 较弱(长上下文下易漂移) | prompt 内嵌 tools 定义 + 示例(few-shot) |
| 解码层 | 结构化输出 / 约束解码(grammar-constrained decoding),在采样阶段屏蔽不合法 token | 强(格式 100% 合法,语义仍需校验) | OpenAI structured outputs、Outlines、guidance、SGLang 结构化 |
生产系统普遍是三层叠加:训练层保证"懂",提示层保证"知道当前用哪个",解码层保证"格式一定对"。其中解码层的价值被低估——它把"格式错误"这类本可工程消除的问题从模型概率域挪到确定性域,是成本最低的一层保险。
三 失败模式分类
| 失败类型 | 现象 | 根因 |
|---|---|---|
| 选错工具 | 调用了语义相近但错误的函数(search_user 调成 search_order) | 工具描述语义重叠、缺少区分性 affordance |
| 参数幻觉 | 参数名 / 枚举值凭空捏造 | schema 约束未入解码层,或描述过于简略 |
| 参数忠实度不足 | 参数值与上下文事实不符(订单号抄错、时间算错) | 模型"填表"而非"取数",无交叉验证 |
| 格式漂移 | 长对话后 JSON 不闭合、混入散文 | 上下文稀释 + 未开约束解码 |
| 过度调用 | 不需要调工具也调了,或重复调用 | 缺少"不调用"的负例训练与提示约束 |
| 嵌套 / 多工具编排失败 | 工具链中间步参数传递错 | 链式 error 累积、中间状态未显式约束 |
四 工具描述工程
- description 写"做什么 + 何时用",不写"怎么做":模型需要的是边界判断(哪个工具适用),不是内部实现。"查询用户的未读订单列表(status=pending)"比"调用 order 服务"信息密度高。
- 参数描述写取值来源:每个参数的 description 里指明值从哪来("从用户消息中的订单号提取"),显著降低参数幻觉。
- 枚举必给、闭集必写全:开放集合用正则 + 说明;枚举值全列,禁止"等"。
- 相似工具加区分字段:search_user / search_order 这类易混对,在 description 中显式写"仅当目标是用户档案时使用;订单相关用 search_order"。
- 参数数量控制:经验上单工具参数超过 10 个,漏填率开始上升。优先把低频参数收进 object 子结构,或用 "params" 分片。
- 默认值即契约:可选参数一律给默认值并在描述中说明,避免模型省略后下游崩溃。
五 结构化输出工程
除"调用工具"外,让模型输出严格 JSON(分类结果、抽取字段、UI 数据)是同一问题的另一面。实践要点:
- schema 即接口文档:字段名用名词单数、平铺优先少嵌套;每个字段 description 一句话说清语义,必填用 required 显式声明,可选的给 default。
- 用联合类型显式建模"结果分支":如 "match" / "no_match" / "ambiguous" 三态,比让模型自由输出"没找到"稳健得多;歧义态单独建模,下游可走人工或追问。
- 枚举 + 正则双重约束:状态字段用 enum,日期 / 编号字段用 pattern,解码层直接拦截非法值。
- 禁止字符串内嵌 JSON:把 JSON 作为字段值再序列化是解析失败的主要来源;下游需要就拆字段。
- 长输出用流式 + 增量 parse:大 schema 分阶段输出(先骨架后详情)可降低中途截断损失。
解码层选型上,约束解码在无温度(temperature=0)任务下应默认开启;开启后"格式类失败"趋近于零,剩余失败转为"语义类",评测关注点也随之转移。
六 运行时防护:校验、重试与兜底
- 服务端再次校验:不信任模型侧的"已 schema 校验"。接收 JSON 后再过一次 JSON Schema 校验(jsonschema 库),失败直接走重试,不进入业务逻辑。
- 重试策略分级:格式错误 → 原样重试(可附错误信息提示);参数非法 → 带错误信息的定向重试("字段 X 应匹配枚举 [...]");语义可疑(值与上下文不符)→ 换模型或降级人工。重试上限 2-3 次,防止死循环与成本失控。
- 修复而非重跑:轻微格式错误(缺引号、尾逗号)用规则 / 小模型修复,比重跑整条链便宜一个数量级。
- 兜底态显式建模:所有链路都有"无法调用 / 信息不足"的出口,且这个出口是业务设计的一部分(追问用户、取默认值、人工队列),不是 try-except 里的 pass。
- 幂等与可重入:写操作带 idempotency key;同一意图重复调用时靠 key 去重,防止"过度调用"造成重复副作用。
七 评测体系:三个核心指标
| 指标 | 定义 | 测量方法 |
|---|---|---|
| 工具选择准确率 | 给定 query,选中 ground-truth 工具的比例 | 构造工具选择测试集(含"不该调用"负例),逐条比对 |
| 参数正确率 | 选中正确工具前提下,参数与标注值等价的比例 | 参数级比对:枚举精确匹配、数值容差、字符串归一化后匹配 |
| 可解析率 / schema 合规率 | 输出能被解析且通过 schema 校验的比例 | 全量输出过 jsonschema,统计通过率(约束解码下应 ≈100%,否则查配置) |
测试集设计三原则:覆盖所有工具(含易混对)、覆盖边界参数(空值 / 极值 / 多值)、覆盖负例(模糊 query、无关 query、多工具并列)。评测集随工具演进版本化,每次 schema 变更都回归跑一遍——工具调用是"契约",契约变更没有回归测试就是事故。
八 安全交叉点:注入与工具调用
工具调用是提示注入的主要"爆炸出口"(详见本站《Agent 提示注入攻击》一文)。工程上要做的:高危工具默认人工确认;工具参数里的自由文本字段(如邮件正文、搜索词)过注入检测;禁止把用户原文整体透传进工具参数(先抽取结构化字段再填入);参数白名单校验(URL 域名、路径前缀、命令动词)。这条线与本章的可靠性工程是同一套管道——契约即安全边界。
九 选型与落地清单
- 模型原生 function calling + 服务端 schema 双校验(基线,任何栈都该有)
- 严格 JSON 输出任务开约束解码(structured outputs / outlines / guidance 任选其一)
- 工具按阶段暴露,单轮可见工具 ≤10 个
- 写操作幂等键 + 高危工具人工确认队列
- 三指标评测集 + schema 变更必回归
- 失败分类日志(选错 / 参数 / 格式 / 过度调用),月度看分布,反哺工具描述改进
成熟度判据:可解析率长期 ≥99.5% 时,工程重心应从"修格式"转向"语义正确率"——那一层没有银弹,只能靠测试集迭代 + 失败样本复盘。