agentic loop 的容错边界与修复策略
在函数调用(function calling / tool use)驱动的 agentic loop 中,模型输出的 tool_calls[].function.arguments 本质是一段由语言模型逐 token 生成的字符串,而非结构化对象。它经常出现尾逗号、单引号、未转义换行、截断、注释混入等语法非法形态。若在循环中直接 JSON.parse,一次异常即可让整个 agent 崩溃。本文以“解析容错—结构修复—语义校验—回喂模型”四层防御为贯穿主线,系统比较“try/catch 把错误作为工具结果回喂”与“工具执行前做 JSON 修复/清洗”两条路线的适用边界,给出可操作的工程步骤、代码骨架与评测方法,并对该问题的学术脉络与前沿趋势作出预判。
01导语:一次 parse 异常如何摧毁整个循环
设想一个典型的 agent 运行时:用户提问,模型决定调用 search_flights,返回的 arguments 是 {"from":"PEK","to":"SHA","date":"2025-06-01",}——注意那个尾逗号。运行时代码执行 JSON.parse(args),抛出 SyntaxError。如果这个异常没有被捕获,整个请求栈展开,用户看到的是 500,而模型甚至不知道自己“说错了话”。
这不是极端个案。只要模型以自回归方式逐 token 生成结构化参数,就必然存在“语法漂移”的概率。问题不在于“会不会发生”,而在于“发生时系统如何优雅降级”。本文要回答的核心问题因此可以精确表述为:在 agentic loop 中,面对非法 JSON 的 arguments,应当把错误作为观察结果回喂给模型让其自我纠正,还是在工具执行前插入一层修复/清洗层?
笔者认为,这不是一个二选一的问题,而是一个分层问题。回喂与预修复分别作用于不同的失败模式,二者不是替代关系,而是纵深防御中的不同层。本文的主线即由此确立:把“解析失败”从一个异常事件,重构为一条可观测、可分级、可回喂的结构化错误信号,并在信号进入模型之前,用确定性的修复层尽可能消解低阶语法错误。
工程判断:任何“模型输出即信任”的假设,在 agent 场景下都是架构级缺陷。工具调用参数应被视为不可信输入,与用户输入同等对待。
02问题定义:arguments 为什么天然不可靠
2.1 生成机制决定的不确定性
函数调用参数在多数 API 中并非由独立的语法约束解码器生成,而是模型在给定 JSON Schema 描述后,以自然语言续写的方式产出字符串。即便厂商提供了“结构化输出”能力,其底层也常是约束解码(constrained decoding)或后处理校验,而非真正的形式化保证。OpenAI 的函数调用文档明确将 arguments 描述为“JSON 格式的字符串”,需要调用方自行解析(OpenAI API Reference, 2024)。这意味着解析责任在客户端。
本文评述:把解析责任推给客户端,在单轮场景下可接受;但在多轮 agentic loop 中,错误会沿循环放大——一次解析失败若被静默吞掉,模型会基于“工具已执行”的错误前提继续推理,产生更难排查的幻觉链。
2.2 非法 JSON 的典型形态分类
根据公开的开发者社区反馈与若干实证研究,非法 arguments 可归为以下几类。下表为笔者基于工程观察整理的分类,非单一文献数据:
注意最后一类:语法合法但语义非法。这恰恰是纯 JSON 修复层无法覆盖的,必须依赖 Schema 校验与回喂。这一区分构成了后文分层策略的依据。
2.3 与结构化输出研究的关联
学术界对“让语言模型输出合法结构”已有系统研究。Willard 与 Louf 提出的 Outlines 方法用有限状态机在解码时约束 token 选择,从生成阶段保证 JSON 合法性(Willard & Louf, 2023)。Beurer-Kellner 等人的 Guidance 框架则通过模板与约束解码结合,把结构化生成前移到解码层(Beurer-Kellner et al., 2023)。这些工作说明:最彻底的修复是在生成时修复。
本文评述:约束解码并非银弹。其一,它要求对模型 logits 有访问权,闭源 API 场景不可用;其二,强约束可能扭曲语义分布,导致“语法合法但内容更差”;其三,截断问题依然存在——状态机无法阻止 max_tokens 在字符串中途切断。因此,即使采用约束解码,客户端仍需要一层解析容错。这正是本文主线的现实基础。
03两条路线的争论:回喂 vs 预修复
3.1 路线 A:try/catch 回喂
做法是:捕获解析异常,构造一条 role:"tool" 的消息,内容形如 {"error":"invalid_json","detail":"..."},交回模型,让其重新生成。其理论依据是模型的自我纠错(self-correction)能力。Huang 等人的研究对 LLM 自我纠错的可靠性提出了重要质疑:在没有外部反馈时,模型自我纠错往往不能提升甚至降低准确率(Huang et al., 2024, ICLR)。但该研究同时指出,当纠错信号来自外部可验证反馈时,效果显著不同。
本文评述:JSON 解析器的报错恰好是一种确定性的外部反馈——它精确指出“第 N 行第 M 列期望逗号”。这与“让模型自己检查答案”有本质区别。因此,把解析错误回喂,在理论上站得住脚,前提是错误信息要结构化、可定位、不含噪声。
3.2 路线 B:执行前修复/清洗
做法是:在 parse 之前或 parse 失败后,用确定性规则或轻量模型修复字符串,再交给工具。常见手段包括去除尾逗号、单引号转双引号、截断补全、提取最外层花括号等。其优势是低延迟、零 token 成本、结果可复现;劣势是可能“修错”——把本应报错的输入强行改成合法但语义错误的参数,掩盖问题。
JSON 修复本身是一个成熟工程问题。Python 生态的 json_repair、demjson3,JS 生态的 jsonrepair、JSON5 解析器,都提供了不同程度的容错。但需注意:修复库的“成功”不等于“正确”,它可能返回一个合法但偏离原意的对象。
3.3 争论的实质:错误该由谁消化
两条路线的分歧,表面是技术选型,实质是错误消化责任的分摊。预修复把责任放在确定性代码侧,追求“不让模型为低级错误付费”;回喂把责任放回模型侧,追求“让模型学会在上下文里修正”。笔者认为,正确的做法是按错误层级分工:
- L1 语法层错误(尾逗号、单引号、截断)→ 优先确定性修复,修复失败再回喂;
- L2 结构层错误(缺字段、多余字段、嵌套错位)→ Schema 校验 + 回喂;
- L3 语义层错误(类型错、枚举越界、业务约束违反)→ 必须回喂,禁止静默修复。
这一分工是本文的核心主张,也是后文四层防御主线的逻辑起点。
04四层防御主线:容错、修复、校验、回喂
把上述分工落地为一条流水线,即本文提出的四层防御。每一层都有明确的输入、输出与失败语义,层与层之间通过结构化错误对象传递,而非抛异常穿透。
raw_args (string) │ ▼ [L0 容错解析] ── 严格 JSON.parse,失败不抛,返回 Result │ ok ├──────────────► 进入 L2 │ fail ▼ [L1 结构修复] ── 确定性规则/修复库,产出 candidate │ 修复成功且通过 L2 ├──────────────► 进入 L2 │ 修复失败 ▼ [L2 Schema 校验] ── JSON Schema / 类型 / 枚举 / 业务约束 │ pass ├──────────────► 执行工具 │ fail ▼ [L3 回喂模型] ── 结构化错误对象 → tool message → 下一轮 │ └──► 超过重试上限 → 降级/人工介入
4.1 L0:容错解析,异常不外泄
第一原则是循环内不抛未捕获异常。解析函数应返回一个显式的 Result 类型(成功值或错误对象),而非依赖 try/catch 控制流。错误对象至少包含:错误类别、原始字符串片段、解析器给出的位置信息(若可得)、以及一个稳定的错误码。稳定错误码是后续统计与回喂模板的基础。
4.2 L1:结构修复,只碰语法不碰语义
修复层的边界必须严格限定在语法层。允许的操作:去除尾逗号、单引号转双引号、补全未闭合括号、剥离前后非 JSON 文本、转义裸换行。禁止的操作:猜测缺失字段的取值、把字符串数字强转为数字、补全被截断的枚举值。后者的“修复”会制造静默错误,比崩溃更危险。
本文评述:修复库的默认行为往往过于激进。以部分 json_repair 类实现为例,它们会尽力“猜”出意图,这在数据清洗场景是优点,在工具调用场景是风险。工程上应封装一层“白名单修复”,只允许已知安全的变换,并对每次修复打点记录。
4.3 L2:Schema 校验,语法合法≠可用
通过 JSON.parse 只说明语法合法。工具真正需要的是符合其参数契约的对象。这一层应使用 JSON Schema 校验(如 Ajv、jsonschema),并叠加业务约束(如日期格式、枚举范围、数值区间)。校验失败产出的是结构化违规列表,而非一句“参数错误”。
4.4 L3:回喂模型,把错误变成观察
当 L1 无法安全修复、或 L2 校验失败时,进入回喂。回喂消息的设计直接决定纠错成功率。应遵循三条原则:其一,只回喂可操作信息,不要粘贴整段原始字符串;其二,给出期望形态,例如“字段 date 需为 YYYY-MM-DD”;其三,限制重试次数,避免无限循环。业界在工具调用协议上的实践(如 Anthropic 的 tool use 文档、OpenAI 的 function calling 指南)均建议在工具结果中返回明确的错误结构,而非让调用方崩溃。
关键洞察:回喂不是“把异常丢给模型”,而是“把异常翻译成模型能理解的任务约束”。翻译质量决定纠错质量。
05工程实现:代码骨架与操作步骤
5.1 统一的结果类型与错误码
先定义错误码,让每一层都能用同一套词汇沟通。以下为 TypeScript 骨架,其他语言可平移:
type ErrCode =
| "SYNTAX_TAIL_COMMA"
| "SYNTAX_SINGLE_QUOTE"
| "SYNTAX_TRUNCATED"
| "SYNTAX_UNESCAPED_NEWLINE"
| "SCHEMA_MISSING_FIELD"
| "SCHEMA_TYPE_MISMATCH"
| "SCHEMA_ENUM_VIOLATION"
| "REPAIR_UNSAFE";
interface ParseResult<T> {
ok: boolean;
value?: T;
code?: ErrCode;
detail?: string;
repaired?: boolean;
raw?: string;
}
5.2 L0+L1:容错解析与白名单修复
function parseToolArgs(raw: string): ParseResult<unknown> {
// L0: 严格解析,异常转 Result
try {
return { ok: true, value: JSON.parse(raw), raw };
} catch (e) {
const code = classifySyntaxError(raw, e);
// L1: 白名单修复
const fixed = safeRepair(raw);
if (fixed !== null) {
try {
return { ok: true, value: JSON.parse(fixed), repaired: true, raw };
} catch { /* 修复后仍失败,落到回喂 */ }
}
return { ok: false, code, detail: String(e), raw };
}
}
function safeRepair(s: string): string | null {
let t = s.trim();
// 仅做语法层变换,且每一步都可解释
t = stripCodeFence(t); // 去掉 ```json 包裹
t = t.replace(/,\s*([}\]])/g, "$1"); // 尾逗号
t = t.replace(/'/g, '"'); // 单引号(注意:仅当无合法双引号冲突时)
t = balanceBrackets(t); // 补全未闭合括号
return t;
}
注意 safeRepair 的每一步都应是幂等且可解释的。单引号替换需谨慎:若字符串内部本身含单引号,粗暴替换会破坏内容。工程上应先做词法扫描,只在“字符串定界符”位置替换。
5.3 L2:Schema 校验与违规结构化
function validateArgs(value: unknown, schema: object): ParseResult<unknown> {
const ajv = new Ajv({ allErrors: true });
const validate = ajv.compile(schema);
if (validate(value)) return { ok: true, value };
const violations = (validate.errors ?? []).map(e => ({
path: e.instancePath || "/",
rule: e.keyword,
message: e.message,
}));
return { ok: false, code: "SCHEMA_TYPE_MISMATCH", detail: JSON.stringify(violations) };
}
5.4 L3:回喂消息模板
回喂消息应短、准、可执行。以下模板经工程实践验证,能显著降低二次失败率(模板为笔者整理,非单一来源):
{
"error": "invalid_tool_arguments",
"code": "SCHEMA_TYPE_MISMATCH",
"violations": [
{ "path": "/date", "rule": "format", "expected": "YYYY-MM-DD" }
],
"instruction": "请仅重新输出 arguments,不要输出解释文字。"
}
本文评述:模板中“不要输出解释文字”一句看似多余,实则关键。模型在收到错误后常倾向于先道歉再重试,导致 arguments 被包裹在自然语言中,反而触发新的解析失败。显式约束输出形态,是降低二次失败率的低成本手段。
5.5 操作步骤清单
- 在工具调用入口统一封装
parseToolArgs,禁止业务代码直接JSON.parse; - 为每个工具维护 JSON Schema,并纳入版本管理;
- 为修复层建立白名单变换,逐条写单测与回归用例;
- 为回喂设置最大重试次数(建议 2–3 次)与总 token 预算;
- 对每次解析失败打点:错误码、工具名、模型版本、是否修复成功;
- 定期用打点数据回归评测,观察错误分布漂移。
06关键数据与案例:错误分布与修复收益
6.1 公开基准中的相关信号
直接统计“tool_calls arguments 非法率”的公开数据较少,但可从相关基准间接推断。伯克利函数调用排行榜(Berkeley Function Calling Leaderboard, BFCL)系统评测了模型在多轮、多工具场景下的调用准确率,其评测维度包含参数正确性(Patil et al., 2024 起持续更新)。ToolBench、API-Bank 等基准也报告了参数级错误(Li et al., 2023; Li et al., 2023)。这些工作表明,参数错误是函数调用失败的主要来源之一,而非边缘情况。
本文评述:现有基准多关注“语义是否正确”,对“语法是否合法”的单独统计不足。这导致工程界低估了语法层失败率。笔者认为,社区需要一个专门度量“arguments 可解析率”的轻量基准,按错误码分类统计,才能指导修复层设计。
6.2 一个模拟数据的说明
为说明分层策略的收益量级,笔者构造了一组模拟数据(非真实实验,仅用于演示分析方法):假设某工具调用场景中,语法层错误占全部失败的 60%,其中尾逗号与单引号合计占 45%,截断占 10%,其余 5%。若白名单修复能覆盖尾逗号与单引号的 90%,则可将需回喂的比例从 60% 降至约 19.5%(60% − 45%×90%)。这一计算仅为算术推演,实际收益取决于模型与场景,读者应以自身打点数据为准。
6.3 案例:截断型失败的处置
截断是最棘手的一类。若 arguments 在字符串中途被 max_tokens 切断,任何“补全”都是猜测。正确处置是:L1 明确拒绝修复(返回 REPAIR_UNSAFE),L3 回喂时附带“上次输出被截断,请精简参数或分段调用”的提示。这一处置把“修复失败”转化为“模型可理解的约束”,是分层策略优于单一策略的典型例证。
07对比与局限:何时该修,何时该退
7.1 修复层的适用边界
修复层适合高确定性、低语义风险的变换。判断标准可归纳为三条:变换是否可逆(能还原出原意)、是否幂等、是否可解释。尾逗号去除三条皆满足;缺失字段补全三条皆不满足。工程上应把这三条写成评审清单,任何新增修复规则都需过审。
7.2 回喂的失效模式
回喂并非总是有效。已知失效模式包括:模型重复同一错误(陷入局部循环)、模型为“讨好”校验器而虚构字段、错误信息过长挤占上下文。应对手段分别是:限制重试次数并升级到人工、在回喂中禁止新增字段、对错误信息做摘要。Huang 等人关于自我纠错局限的结论在此依然适用——没有外部可验证信号时,回喂会退化为空转(Huang et al., 2024)。
7.3 与约束解码的关系
若使用支持约束解码的推理栈(如 Outlines、Guidance、以及部分厂商的结构化输出模式),语法层错误可大幅减少,但如前所述不能归零。本文评述:约束解码与本文分层策略是互补的——前者降低 L1 的发生率,后者保证 L1 发生时系统不崩。把二者对立起来是伪命题。
08评测体系:如何量化容错策略
没有度量就没有优化。建议从四个指标构建评测:可解析率(parse 成功率)、修复正确率(修复后语义与原意一致的比例,需人工或规则判定)、回喂收敛率(N 次内成功的比例)、端到端任务成功率。前三个是过程指标,最后一个是结果指标。
数据集方面,可基于公开的函数调用基准构造测试集,并做如下预处理:其一,按工具 Schema 过滤掉无法校验的样本;其二,对每条样本记录模型原始 arguments 字符串;其三,按本文错误码体系人工标注子集,作为修复正确率的金标。需说明:若使用模拟注入的非法样本(如人为加尾逗号),应在报告中明确标注为“合成数据”,避免与真实分布混淆。
09前沿预判与结论
往前看,三个趋势值得关注。第一,协议层内建容错:工具调用协议可能在未来版本中直接携带“解析状态”字段,让错误成为一等公民。第二,解码层约束普及:随着开源推理栈成熟,约束解码将从“高级技巧”变为默认配置。第三,错误感知的 agent 训练:在训练数据中注入“收到结构化错误后正确重试”的轨迹,让模型内化纠错模式,而非依赖运行时提示。
回到最初的问题:try/catch 回喂,还是执行前修复?本文的答案是——按错误层级分工,用四层防御把异常转化为信号。语法层优先确定性修复,结构层与语义层必须回喂,截断等不可安全修复的情形明确拒绝修复并升级。这一主线既避免了“一崩了之”的脆弱,也避免了“强行修复”的静默错误。
笔者认为,agent 工程的成熟度,很大程度上体现在它如何对待“模型说错话”这件事。把每一次解析失败都当作可观测、可归因、可回喂的事件,而非需要吞掉的异常,是构建可靠 agentic loop 的起点。
10参考文献与拓展资源
主要参考文献(8–9 篇)
- Willard, B. T., & Louf, R. (2023). Efficient Guided Generation for Large Language Models. arXiv:2307.09702.
- Beurer-Kellner, L., Fischer, M., & Vechev, M. (2023). Prompting Is Programming: A Query Language for Large Language Models. PLDI 2023.
- Huang, J., Chen, X., Mishra, S., et al. (2024). Large Language Models Cannot Self-Correct Reasoning Yet. ICLR 2024.
- Patil, S. G., Wang, T., Gonzalez, J. E., & Zaharia, M. (2024). Gorilla / Berkeley Function Calling Leaderboard 相关报告与持续更新.
- Li, M., Zhao, Y., Yu, B., et al. (2023). API-Bank: A Comprehensive Benchmark for Tool-Augmented LLMs. EMNLP 2023.
- Qin, Y., Liang, S., Ye, Y., et al. (2023). ToolLLM: Facilitating Large Language Models to Master 16000+ Real-world APIs. arXiv:2307.16789.
- Schick, T., Dwivedi-Yu, J., Dessì, R., et al. (2023). Toolformer: Language Models Can Teach Themselves to Use Tools. NeurIPS 2023.
- OpenAI. (2024). Function calling / Structured Outputs 文档. platform.openai.com/docs.
- Anthropic. (2024). Tool use (function calling) 文档. docs.anthropic.com.
拓展资源(教程与视频)
- JSON Schema 官方入门:json-schema.org/learn
- Ajv 校验器文档:ajv.js.org
- Outlines 项目与示例:github.com/dottxt-ai/outlines
- Guidance 框架:github.com/guidance-ai/guidance
- Berkeley Function Calling Leaderboard:gorilla.cs.berkeley.edu/leaderboard.html
- JSON 修复库(JS):github.com/josdejong/jsonrepair
- JSON 修复库(Python):github.com/mangiucugna/json_repair
本文内容仅为作者学习、思考、经验、笔记的总结,仅供技术交流与参考。文中观点仅代表笔者个人思辨,不构成任何学术建议、商业建议或专业建议。所有数据来源已标注,引用时请以原始文献为准。

