第 14 章 结构化输出与契约:让 Agent 的输出能接住
输出是自由文本,下游要的是数据
第三部分讲完了"让 Agent 动手"。从这一章开始,进入第四部分"让它可靠"。前面几部分解决"能不能干",这部分解决"干得稳不稳"。
最基础也最烦人的可靠性问题,是输出。Agent 天然输出自由文本,但下游系统要的是可解析的数据:数据库要结构化记录,下一个 Agent 要能交接,自动化工作流要能判断"这是合格还是不合格"。自由文本一旦往下游走,就是一场灾难:
- 输出格式不可预测,要写一堆解析逻辑。
- 校验和错误处理无从下手。
- 接自动化工作流极其脆弱。
- 分类结果不稳定。
- 还得人工后处理,从文本里抠结构化数据。
一个 Agent 的输出要喂给另一个系统,自由文本根本接不住。
这一章讲两个模式,从"定义契约"到"让契约不破":
- Structured Output Specification(结构化输出规范):用确定性 schema 约束 Agent 输出。
- Schema Validation Retry(schema 校验重试):输出不合规就重试,还要跨步骤学习。
模式一:结构化输出规范(Structured Output Specification)
问题
如开头所说:自由文本输出没法校验、没法解析、没法接下游。更麻烦的是多阶段工作流,一个 Agent 的输出是下一个 Agent 或系统的输入,文本输出直接让整条链路崩掉。
方案
用确定性 schema 约束 Agent 的输出。不让模型自由发挥文本,而是用类型系统、JSON Schema 或框架自带的结构化输出 API,规定精确的输出格式。
三个核心动作:
1. 定义显式输出 schema:
- 用 TypeScript 接口、JSON Schema 或 Pydantic 模型。
- 规定必填字段、类型、约束。
- 为分类输出定义枚举。
- 写清字段语义和校验规则。
2. 用框架的结构化输出能力:
- OpenAI 的结构化输出(JSON schema 约束解码)。
- Anthropic 的 tool use 返回结构化结果。
- Vercel AI SDK 的
generateObject。 - LangChain 的输出解析器、LlamaIndex 的 Pydantic programs、Instructor 的重试包装。
3. 在生成时校验:
- 框架保证 LLM 遵守 schema。
- 类型错误在进入应用代码之前就被抓住。
- 输出保证可解析。
import { generateObject } from 'ai';
import { z } from 'zod';
// 定义严格的输出 schema
const LeadQualificationSchema = z.object({
qualification: z.enum(['qualified', 'unqualified', 'needs_review']),
confidence: z.number().min(0).max(1),
companySize: z.enum(['enterprise', 'mid-market', 'smb', 'unknown']),
estimatedBudget: z.string().optional(),
nextSteps: z.array(z.string()),
reasoning: z.string()
});
// Agent 返回结构化、已验证的输出
const result = await generateObject({
model: openai('gpt-4'),
schema: LeadQualificationSchema,
prompt: `分析这条线索: ${leadData}`
});
// TypeScript 知道确切结构,可以直接按字段判断
if (result.object.qualification === 'qualified') {
await sendToSalesTeam(result.object);
}另一个例子:内容审核分析。用 Pydantic 定义 AbuseAnalysis,字段全是枚举 + 置信度 + 证据列表 + 是否要人工复核。生成后直接判断:要人工就 send_to_human,否则 auto_execute。schema 把"输出"变成了"可执行的决策结构"。
证据
- 来自 Vercel AI 团队(
established,成熟),他们在构建 Agent 时用结构化分类做线索资格评估。 - JSONformer(Billings 等人,2023):约束解码能从根本上消除重试循环。
- OpenAI / Anthropic 都把结构化输出做成官方能力,已经是行业标准做法。
怎么用
什么时候用:
- 多阶段工作流,需要结构化交接。
- 分类和归类任务。
- 数据提取和转换。
- 接数据库或 API。
- 合规和审计要求。
- 质量保证和校验。
实施步骤:
- 明确输出需求:Agent 做什么决策?要提取什么数据?下游系统消费什么?
- 设计 schema:必填字段、枚举、约束都要定义清楚。
- 接框架:用框架的结构化输出能力,保证生成即合规。
- 处理校验失败:带澄清提示重试(行业惯例 3 次);失败回退人工复核;记录 schema 违规,用于改进提示词。
缓解策略: schema 别定太死。留一个 additional_context 可选字段装自由文本备注;schema 要版本化,支持平滑降级;分类在演进时用联合类型;必填/可选字段平衡好。
取舍
- 好处:输出保证可解析,解析错误归零;类型安全,编译期检查;接数据库、API、工作流顺畅;内置约束校验;schema 校验还能在执行前挡住提示注入;组件之间是显式契约;容易验证输出正确性。
- 代价:schema 一改,所有关联方要协调更新;前期设计 schema 要花功夫;可能限制有用的自由文本输出;依赖 LLM 提供商的 schema 支持;schema 太严会导致生成失败;需求变了要跟着改,有演进摩擦。
模式二:schema 校验重试(Schema Validation Retry with Cross-Step Learning)
问题
模式一假设"schema 定了输出就合规"。现实是:LLM 不总能一次生成符合 schema 的 JSON。更糟的是,多步工作流里问题会叠加:
- schema 违规:生成的 JSON 不匹配预期的 Zod/JSON Schema。
- 一次失败全盘死:单次失败就终止整个工作流。
- 不长记性:每一步都独立地重复同样的错误。
- 白烧 token:失败的响应照样消耗上下文和钱。
- 链路脆弱:不稳定的 LLM 输出让 Agent 不可靠。
方案
多步重试 + 详细错误反馈 + 跨步骤错误累积。让 Agent 在整个工作流里从自己的校验失败中学习。
三个核心机制:
1. 多次尝试重试 + 详细反馈:
行业实践用 2-3 次重试。研究(Self-Refine,ICLR 2024)显示,带反馈的迭代优化能把输出质量提升 15%-45%。
const maxAttempts = 3;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const result = await ctx.llm.invokeStructured({ schema, options }, msgs);
if (result.parsed) {
return result.parsed; // 成功,跳出重试循环
}
// 提取详细的 Zod 校验错误
const validationError = getZodError(result.rawText);
// 存下来供跨步骤学习
ctx.schemaErrors?.push({
stepIndex: currStep,
error: validationError,
rawResponse: result.rawText || "",
});
// 把错误反馈追加进消息,让模型重试时看到
msgs = [
...msgs,
{ role: "assistant", content: result.rawText },
{ role: "user", content: `校验错误:\n${validationError}\n请修正。` },
];
}
// 全失败:带着累积的错误上下文抛异常
throw new SchemaValidationError(`3 次尝试后仍失败`, {
stepIndex: currStep,
errors: ctx.schemaErrors?.slice(-3) // 最近 3 条错误
});2. 跨步骤错误累积:
Agent 维护一个最近的 schema 错误滚动窗口,在后续 LLM 调用里带上这些错误:
interface AgentContext {
schemaErrors: Array<{
stepIndex: number;
error: string; // Zod 校验错误
rawResponse: string; // LLM 实际输出的内容
timestamp: number;
}>;
}
// 每步之前,把最近错误追加进系统提示,引导 LLM 避免重复犯错
const recentErrors = ctx.schemaErrors
.slice(-3) // 只保留最近 3 条,避免上下文膨胀
.map(e => `Step ${e.stepIndex}: ${e.error}`)
.join('\n');
if (recentErrors) {
msgs.push({
role: "system",
content: `要避免的近期 schema 校验错误:\n${recentErrors}`
});
}3. 结构化反馈循环:
每次重试都提供具体、可行动的反馈。getZodError 把校验错误格式化成人能看懂、模型能修正的文本(path: message (received: ...) 这种格式)。替代方案:OpenAI Structured Outputs 和 Anthropic Tool Use 在 API 层强制 schema 合规,可以不做客户端重试循环。
证据
- 来自 HyperAgent(Hyperbrowser 团队,
emerging),生产实现见其agent.ts。 - Self-Refine(Shinn 等人,ICLR 2024):带反馈的迭代优化提升输出质量 15%-45%。
- 和模式一的 JSONformer 思路呼应:约束解码从源头消除违规,重试是兜底。
怎么用
- 用 Zod 定义严格 schema,错误信息要清楚("elementId is required" 这种,别只给"无效")。
- 实现重试包装:最多 3 次,每次把错误反馈追加进消息。
- 注入错误上下文:每步之前,把之前步骤的最近 3 条错误放进系统提示。
- 配置化:
maxValidationAttempts、errorHistorySize、crossStepErrorCount都做成可调。
缓解策略: 跨步骤错误窗口限制在最近 3 条控 token;重复工作流用缓存跳过重试;每步设超时防止重试失控;记录失败用于改进提示词;生产部署加重试退避 + 抖动。
取舍
- 好处:3 次重试显著提高结构化输出成功率;跨步骤学习让 Agent 不在整个工作流里重复犯错;详细错误反馈引导模型精准修正;错误历史提供诊断信息;尝试次数 vs 成本/延迟可调。
- 代价:重试时多次 LLM 调用增加延迟;失败的尝试照样烧 token;错误历史不管控会膨胀上下文;有些模型就是改不对;多一套重试逻辑和错误管理。
两个模式怎么选
| 场景 | 推荐模式 |
|---|---|
| 输出要接数据库 / API / 下一个 Agent | 结构化输出规范 |
| 输出总是不合 schema,想让它自己改对 | schema 校验重试 |
| 用 OpenAI / Anthropic 官方 API | 官方结构化输出(API 层强制) |
两个模式是同一个契约的两面:结构化输出规范定义"契约长什么样",schema 校验重试保证"契约不破"。规范管设计,重试管兜底。上游能约束解码就从源头消除违规,约束不了就靠重试加跨步骤学习把它修回来。
实践清单
- 输出接下游前,先定义 schema(Zod / Pydantic / JSON Schema)
- 分类字段用枚举,数值字段加范围约束,决策字段语义写清楚
- 用框架的结构化输出能力(generateObject / tool use / response_format)
- schema 校验失败:带详细错误重试 3 次,失败回退人工复核
- 记录 schema 违规日志,用来改进提示词和 schema
- 多步工作流:维护跨步骤错误窗口,每步前注入最近 3 条错误
- 错误反馈要具体(path + message + 收到的值),别只给"无效"
- schema 别定死:留可选字段、版本化、联合类型应对演进
- schema 校验拦截提示注入(执行前挡掉恶意内容)
本章小结
- 结构化输出规范:确定性 schema 约束输出,可解析、类型安全、接得住下游。
- schema 校验重试:3 次重试 + 详细反馈 + 跨步骤错误累积,输出质量提升 15%-45%。
- 规范定义契约,重试保证契约不破。
- 输出稳了,多 Agent 交接和自动化工作流才立得住。
下一章讲验证循环:输出就算符合 schema,内容对不对还得验。确定性评分器、渲染 UI 完成门、子 Agent 编译检查。