AI Agent 落地实践 · 第 11 篇

2026-11-04 · 阿良 · 苏州畅达软件

Schema 写在代码里,模型却看不见:一次 Agent 结构化输出契约的三轮返工

AI Agent结构化输出Schema契约设计故障复盘

我们在做一套多项目软件工厂,Agent 不只执行代码任务,还要回报候选方案、缺口、审查结论和任务状态。 这些结果会进入下一阶段,甚至成为人工审批依据,所以不能只返回一段“看起来像答案”的散文。 真正难的不是定义 JSON,而是让模型知道要返回什么、让程序验证它真的符合,并且保证两边用的是同一份契约。


一、现象:连续三次 output_invalid,最后不是重试,而是升级

第一次真实运行研究工作流时,活动连续三次返回 output_invalid,随后达到重试上限并升级。

日志给出的结论很直接:

活动输出解析失败
class = output_invalid

最初很容易把它理解成模型不会按要求输出。但检查代码后发现,workflow 里确实定义了输出 schema:

候选方案 candidates[]
  - name
  - verdict
  - rejection_reason?
  - known_costs?
recommendation

问题是:这个 schema 只存在 workflow 定义里。执行器派发活动时没有把它放进 activity.input,模型根本看不到。

测试通过,是因为测试替身按照“正确输入”构造了结果;第一次接真实运行时,缺的字段才暴露。

schema 存在,不等于 schema 参与了这次调用。


二、第一次修复:把 schema 交给模型,仍然会出现格式漂移

把 schema 注入活动后,模型终于知道字段名和层级。但新的问题出现了:

  • 契约里写的是自然语言;
  • output_schema 只是松散类型字符串;
  • 模型可能把合法 JSON 包在解释文字和 Markdown 围栏里;
  • 程序再用 JSON.parse 直接解析整段回复,就会失败。

当时的兜底是 extractJSON:从文本里找一段像 JSON 的内容,再尝试解析。

这能救回一部分结果,却带来三个风险:

  1. 使用什么格式,模型要靠猜;
  2. 解析器能捞什么,程序要靠猜;
  3. “解析成功”与“类型正确”是两件事。

一次实测中,某个模型在确认结果前先写了总结,再附上合法 JSON。提取器能找回对象,但真正的结构约束仍然没有进入输出生成过程。

这不是靠再加一层正则能解决的问题。


三、第二轮返工:不要把“模型定义格式”和“模型输出格式”混为一谈

最初的设计想用同一套 S-expression 既定义 schema,又让模型直接返回 S-expression。

这个想法在程序侧很优雅,但实际测试连续失败:

  • 模型把 schema 风格、键值对风格和自定义标签混在一起;
  • S-expression 里面再嵌 JSON 字符串,需要双重转义;
  • 漏一层转义,整个输出就变成非法字符;
  • 同一个阶段又出现 3 次 output_invalid。

最后的分界定成:

Schema 定义:S-expression,程序编译,模型不生成
Prompt:由 schema 投影生成,模型可见
模型输出:JSON 信封
校验:JSON → 类型校验后转 typed object

这是很关键的一次降复杂度。

Schema 继续用适合机器编写的语法;模型只输出最稳定的 JSON 结构。两边共享的是类型与字段约束,不是强迫模型学习另一门表示法。


四、最终契约:同一份 schema,同时驱动 prompt 和 reader

现在每个非确定性 stage 都带一个 contract:

contract
  prompt
    system
    task
    rules
  schema

执行前:

  1. readSchema 把 schema 编译成内部类型树 Ty;
  2. 非法或未知类型在加载期直接失败;
  3. renderPrompt(stage) 用同一份契约生成模型 prompt;
  4. 禁止用字符串数组手工拼 prompt。

执行后:

  1. 模型返回纯 JSON 信封;
  2. reader 检查信封字段;
  3. 按同一个 Ty 校验每个必填字段、数组元素、枚举和对象字段;
  4. 校验通过后才成为 typed artifact;
  5. 校验失败返回 output_invalid,附带具体路径或 offset 给下一轮。

这条链路的关键不是“用了 JSON”,而是:

模型看到的约束,和执行器校验的约束,必须来自同一个源。

否则会形成两份 schema:prompt 里一份,代码里另一份。改了一边忘了另一边,模型与校验器就会永久互相误解。


五、宽容包裹层,不宽容结构

后来的实测说明,模型偶尔会先总结一句“所有测试都已转绿”,再附上合法 JSON 信封。

我们没有继续使用任意 extractJSON 兜底,但保留了“找到第一个平衡 JSON 对象”的能力。

区别是:

  • 可以容忍 JSON 外面的自然语言包裹;
  • 不允许 JSON 本体非法;
  • 不允许缺 workflow / stage / output;
  • 不允许 output 的类型与 schema 不符;
  • 纯散文、无信封、结构错误仍然硬失败。

这比“整段字符串必须一字不差是 JSON”更贴近真实模型,也比“只要能捞出 JSON 就算成功”更安全。

宽容的是模型表达习惯,不是数据契约。


六、验证:验证的不只是能解析,而是错误会被抓住

本次重新运行了 reader、运行时接线和负向验证:

check-reader                    18 项通过
check-runtime-output             3 项通过
negative-verify-runtime-output   2 项通过

reader 覆盖了:

  • 合法 schema 编译;
  • 未知类型加载期失败;
  • 缺字段、未知字段、类型不符;
  • 纯散文无信封硬失败;
  • 散文包裹合法 JSON 时可提取;
  • 包围层合法但信封缺字段时仍失败;
  • prompt 必须投影任务卡与路径范围。

负向注入则把旧 extractJSON 恢复、把解析路径退回旧实现,确认校验器会变红,再恢复为绿。

这类测试的意义在于:验证错误不会被“解析器很努力”掩盖。


七、沉淀成四条

一、只写在代码里的 schema,不参与 Agent 契约。

模型必须看见字段、层级、枚举和示例;只在执行后才检查,只能告诉模型“你错了”,无法在生成前告诉它“应该怎样”。

二、prompt 和校验器必须同源生成。

手写 prompt 加手写 schema,等于维护两份真相。长期一定会漂移,而且漂移时通常只有线上才暴露。

三、让定义格式服务于机器,让输出格式服务于模型。

Schema 可以用程序友好的 DSL 表达;模型输出应选它最稳定、解析器最确定的格式。没有必要为了“统一”强迫模型生成同一种语言。

四、容错应作用在包裹层,严格性应作用在结构层。

允许模型多说一句解释,但字段、类型和枚举不能放过。反过来,优先恢复解析成功率而放松结构,只是在制造假成功。


一句话总结

Agent 输出的可靠性,不取决于 schema 写得多漂亮,而取决于模型看到的契约、程序校验的契约、错误回灌的路径是不是同一份、同一版、同一语义。


作者:阿良 · 苏州畅达软件 —— 这是「AI Agent 落地实践」系列第 11 篇。前十篇分别讲权限隔离、多租户数据隔离、操作审计、LLM 分层、AI 人设护栏、工作流标识隔离、输入运行时契约、中文检索、AI 只读工具边界和 L2 改动集放行。有类似的结构化输出或 Agent 编排场景,欢迎交流。

准备开始您的数字化项目?

留下需求,我们会在一个工作日内与您联系。

联系我们