章节05 / 14
- 01AI Harness 不是测试脚本,而是 AI 系统的质量控制台
- 02先写任务协议,再谈评测指标
- 03Golden Dataset:把“感觉不错”变成可回归样例
- 04评测不是一个分数:判分器、断言和人工复核怎么组合
- 05结构化输出 Harness:先挡住形状错误,再处理业务错误
- 06Tool Harness:模型只能提议动作,执行权必须被隔离
- 07RAG Harness:先评检索,再评回答
- 08Agent Harness:把多步智能体变成可暂停、可恢复、可审计的状态机
- 09红队与安全 Harness:把提示注入当成常规回归项
- 10观测 Harness:trace 里该看见什么,不该记录什么
- 11CI 回归门禁:让 Prompt、模型和检索改动都要过关
- 12线上反馈回流:用户反馈怎样变成下一版样例
- 13模型路由与发布 Harness:灰度、回滚和成本风险一起看
- 14综合项目:交付一个 AI Harness Engineering 蓝图
本文目录12 节
结构化输出 Harness:先挡住形状错误,再处理业务错误
本指南介绍如何通过构建双层防御 Harness(Zod物理形状校验与业务语义校验)来拦截 AI 失控输出,设计自动重试与隔离队列(Quarantine Queue),并结合 OpenTelemetry 观测模型退化。
- 理解基础 Prompt 调试与 API 调用机制
- 了解 TypeScript/Node.js 异步编程与中间件概念
- 掌握两类 AI 错误的本质区别与分层防御策略
- 能独立编写基于 Zod 的输出形状校验中间件与自动修复机制
- 具备设计隔离队列(Quarantine Queue)并与 OpenTelemetry 观测指标对接的能力
在构建基于大语言模型(LLM)的 Agent 和自动化工作流时,我们经常遭遇“AI 产出破坏核心业务逻辑”的窘境。尽管我们可以通过 Prompt 来约束输出,但由于模型生成的不确定性,进入业务系统的 JSON 数据依然经常失控。本指南将介绍如何为 AI 应用建立一个被称为“Harness”的输出防御屏障——通过将物理校验与业务校验解耦,并在异常发生时执行自我修复与安全隔离,阻断任何异常数据污染下游系统的可能。
学习本单元后,你将拥有一个可复用的双层防御中间件、包含指数退避的原地重试机制、以及将可疑数据下放至隔离队列(Quarantine Queue)的完整设计。
面对失控的 LLM:什么是输出层 Harness 屏障
将大模型接入传统确定性软件系统的最大挑战在于:模型无法保证 100% 遵守格式协议。即使是最先进的模型,也可能因为温度、上下文长度、甚至 prompt 的微小变化而发生输出退化。
输出层 Harness 屏障是部署在 LLM 接口与业务数据库/下游系统之间的一层专用中间件。它的核心目标是:绝不相信 LLM 返回的原始字符串,直到该字符串通过了物理与业务的双重审查。
为了处理这种不确定性,Harness 将错误分为两个完全不同的维度进行管理:
- 形状错误(Shape Error):也称物理格式错误。例如模型输出的 JSON 存在残缺、多了截断括号、返回了 Markdown 块、或者缺少了 schema 约定的必填字段。这类错误导致下游系统直接在解析阶段(如
JSON.parse())崩溃。 - 业务错误(Business Error):物理格式完全合法,能够被解析为标准的 JSON,但是数值或关系严重违反了业务规则。例如:商品数量为空、价格为负数,或者将用户角色赋予了不存在的权限组。
通过对这两类错误进行分层拦截,我们不仅能提升系统的健壮性,还能为未来的自动化对齐与模型微调积累精确的错误数据集。
第一道防线:用 Zod 定义物理“形状”并拦截非标准 JSON
如果模型输出的 JSON 存在语法残缺,Harness 应该立即在边缘侧拦截并触发原地修复(In-context Repair),不要直接抛出给前端,因为用户无法理解裸露的解析报错,只有通过 Harness 缓冲重试才能在不打扰用户的前提下挽回请求。因此,第一道防线专注解决物理“形状”校验。
在代码实现中,我们使用 Zod 来定义严格的 Schema。这种强类型的声明不仅能作为模型的 JSON Schema 输入(例如配合 OpenAI 的 Structured Outputs 使用),更能在系统边界建立严格的数据物理屏障。
坏样例 1:物理形状破损的 JSON
大模型在网络超时、Token 溢出或受到 Prompt Injection 时,极易产生如下截断的数据:
{
"order_id": "ORD-2026-9912",
"items": [
{ "id": "item_001", "price": 99.9
(由于达到 Token 上限或网络丢包,末尾的花括号和中括号完全缺失)
防御方案:Zod Schema 的严格定义
我们在中间件中编写如下 schema:
import { z } from "zod";
export const OrderOutputSchema = z.object({
order_id: z.string().min(1, "Order ID 不能为空"),
items: z.array(
z.object({
id: z.string(),
price: z.number().positive("价格必须为正数"),
})
),
total_price: z.number()
});
export type OrderOutput = z.infer<typeof OrderOutputSchema>;
通过将原始字符串输入 Zod,第一道防线将彻底拦截格式残缺的文本。任何尝试执行 JSON.parse 并直接塞入下游逻辑的代码都会在此处被阻断,并抛出物理解析异常。
结构化输出失败时的隔离检查表
| 失败类型 | 测试输入 | 观察症状 | 诊断步骤 | 修复动作 | 验收标准 |
|---|---|---|---|---|---|
| JSON 形状错误 | 截断的订单 JSON | 解析失败或字段缺失 | 先记录原始输出,再运行 schema 校验 | 触发一次带错误原因的修复请求 | 修复后通过 schema,原始错误被记录 |
| 业务语义错误 | 空商品列表但总价为负 | schema 通过但业务不成立 | 检查业务规则和数据库约束 | 路由到 quarantine,不写主表 | 队列中有错误原因和复核人 |
| 模型拒答错误 | 可回答问题返回空对象 | 输出合法但无法完成任务 | 比对任务协议和上下文 | 标记为 dataset 失败样例 | 下次评测能稳定复现 |
如果 schema 已经通过,先不要直接写入数据库,因为业务语义仍可能错误;否则 Harness 会把“格式正确”误当成“结果可信”。 如果修复请求连续失败,应该停止自动重试并进入隔离队列,因为继续请求只会增加成本和污染日志。 如果隔离队列中的错误反复出现,必须把它晋级为评测样例,只有回归样例能复现时,修复才算进入 Harness。
第二道防线:校验业务语义与模型幻觉
当物理校验通过后,原始文本已成功转化为符合结构的 JSON 对象。但此时我们依然不能放松警惕:模型可以产出格式极其完美、但业务逻辑荒谬的数据。
坏样例 2:合法但业务不合理的 JSON
{
"order_id": "ORD-2026-9913",
"items": [],
"total_price": -50.0
}
(语法解析完全正确,但商品列表为空且总价为负数,这属于严重逻辑幻觉)
业务校验的接入点
针对这类错误,Harness 引入了“状态校验器(Semantic Validators)”。业务校验往往需要连接外部环境。例如,我们需要查询数据库确定 order_id 是否重复,或者核算 total_price 是否等同于 items 中单价之和。如果检测到物理形状正确但业务数据异常(如负数订单金额),我们必须将该请求路由至 Quarantine(隔离)队列,除非下游系统具备天然的幂等性与数据回滚能力,否则一旦让脏数据注入核心数据库将产生难以排查的坏账。
业务语义校验不仅可以编写在 Zod 的 .refine() 方法中,也可以以插件的形式独立挂载。例如:
async function validateBusinessRules(order: OrderOutput): Promise<{ isValid: boolean; reason?: string }> {
// 1. 业务逻辑校验:商品列表不能为空
if (order.items.length === 0) {
return { isValid: false, reason: "商品列表不能为空,至少需包含一件商品" };
}
// 2. 状态一致性校验:核对总价
const computedTotal = order.items.reduce((sum, item) => sum + item.price, 0);
if (Math.abs(computedTotal - order.total_price) > 0.01) {
return {
isValid: false,
reason: `总价计算不一致。模型标称总价 ${order.total_price},但项目累加为 ${computedTotal}`
};
}
return { isValid: true };
}
双层防御架构的错误流转设计与重试退避机制
将错误明确分层后,我们的防御 Harness 就能采取差异化的补救手段,最大化挽回请求吞吐量:
+------------------+
| LLM 原始输出 |
+--------+---------+
|
v
+------------------+
| 第一层:物理校验 | --- [物理失败] ---> [原地修复/重试机制]
+--------+---------+ |
| (合格 JSON) v
v 如果重试达到上限
+------------------+ |
| 第二层:业务校验 | --- [业务失败] |
+--------+---------+ |
| (完全合格) |
v v
+------------------+ +------------------+
| 写入核心数据库 | | Quarantine 队列 |
+------------------+ +------------------+
针对形状错误的补救:原地自我修复(In-context Repair)
在 Anthropic 关于 tool use 与 tool_result 的循环机制中,我们能够发现一种模式:当工具调用的参数不符合指定的 schema 时,开发人员不应当直接给前端报错。根据 Tool use with Claude 的规范,应用应该利用 tool_result 扮演 Harness 守卫,向 Claude 返回一个结构化的错误信息(说明具体的错误位置和不合法的 schema 规则),引导它在下一轮交互中自我修复。
同理,对于 OpenAI 而言,Function calling 文档说明了虽然可以通过指定 response_format 来约束模型输出,但网络断联或 Token 限制依然会引发未捕获的形状错误。因此,我们应该:
- 捕获 Zod 解析错误;
- 构建带有明确 Schema 指导和特定错误位置的修复 Prompt(修剪掉与核心业务无关的冗长 prompt,仅保留上下文和错误数据);
- 向模型发出一次低 Temperature(例如 0.0)的修复请求。
针对业务错误的隔离:Quarantine 队列
业务错误往往意味着模型的微调权重对业务规则缺乏理解,或者 Prompt 的语义边界超出了模型的逻辑极限。此时原地重试极难解决问题(模型会固执地给出同样错误的计算)。此时的最佳做法是:
- 立即中断此请求的自动化链条;
- 将该异常 Payload、输入 prompt 以及对应的校验错误报文写入隔离队列(Quarantine Queue);
- 触发告警通知人工干预或回溯;
- 提供一条安全的“降级通道”,向调用方或前端用户返回中立、安全的兜底反馈。
实时观测:将异常映射至 OpenTelemetry 语义指标
你的防御 Harness 不仅是系统的保护伞,更是 AI 系统健康度的体温计。如果系统频繁在第一层被拦截,说明底层模型的稳定性严重退化或提示词遭到意外修改;如果频繁在第二层拦截,说明业务逻辑边界发生了偏移,需重新评估评测集。
根据 OpenTelemetry GenAI semantic conventions(下文简称 OTel GenAI SemConv)的要求,任何大模型的调用、输入输出、延迟和 token 消耗都应通过标准的 Span、Metric 和 Event 记录。我们在 Harness 中需要对以下自定义属性进行埋点记录:
gen_ai.response.validation.status: 表示校验是否通过(取值passed,shape_failed,business_failed)。gen_ai.response.validation.error_message: 具体失败的错误栈或不满足的规则。gen_ai.response.validation.retry_count: 当前请求为了解决校验错误所执行的原地修复重试次数。
通过将这些属性标准化注入 OpenTelemetry 的 Span 中,监控平台(如 Prometheus / Grafana / Datadog)就可以第一时间捕捉到物理/业务异常率的飙升,在用户感知到错误之前自动发出告警。
此外,在构建自动化多步 Agent 时,根据 Evaluate agent workflows 关于 trace grading 和 dataset 可重复性的指导,评测平台在回放开发阶段的 Trace 时,必须能够通过这些 OTel 属性,精准算得“由于格式错误而额外消耗的物理 Token 数”与“累积请求延迟(Latency Overhead)”。如果在多 Agent 协作场景中遇到高频次的 Schema 物理退化,系统可以动态降级为强制结构化输出模式(如 OpenAI 官方的 Strict mode ),但不要在一开始就锁死该配置,因为 Strict 模式会带来首字延迟(TTFT)的大幅增加以及部分复杂 Tool 定义的编译限制。
防御实战:从输入、拦截到隔离队列的全链路实现
接下来,我们将实现一个完整的结构化输出 Harness,用以保护订单创建流程。这个 Demo 包含了 Zod 物理拦截、业务对账校验、最大 2 次的原地退避修复重试、以及隔离队列写入的设计。
1. 核心依赖安装
在你的 Node.js / TypeScript 项目中安装相关依赖:
npm install zod dotenv
npm install -D typescript @types/node
2. 输出防御 Harness 实现
新建 orderHarness.ts:
import { z } from "zod";
// ---- 1. 定义物理 Schema ----
export const OrderSchema = z.object({
order_id: z.string().min(1, "Order ID 不能为空"),
items: z.array(
z.object({
id: z.string(),
price: z.number().positive("单价必须大于 0"),
})
),
total_price: z.number()
});
export type Order = z.infer<typeof OrderSchema>;
// ---- 2. 模拟 LLM 调用的物理/业务注入器 ----
// 用于在教学中模拟各种损坏的 LLM 原始输出
export type MockOutputType = "valid" | "shape_broken" | "business_broken";
class MockLLMService {
private type: MockOutputType;
constructor(type: MockOutputType) {
this.type = type;
}
async generateOrderText(): Promise<string> {
if (this.type === "shape_broken") {
// 缺失闭合,物理形状破损的 JSON
return `{
"order_id": "ORD-2026-9901",
"items": [
{ "id": "item_001", "price": 49.9`;
}
if (this.type === "business_broken") {
// 物理完全合格,但账目不平,业务严重不合理
return JSON.stringify({
order_id: "ORD-2026-9902",
items: [
{ id: "item_101", price: 100.0 },
{ id: "item_102", price: 200.0 }
],
total_price: 150.0 // 100 + 200 !== 150
}, null, 2);
}
// 完全合法的输出
return JSON.stringify({
order_id: "ORD-2026-9903",
items: [
{ id: "item_201", price: 10.0 },
{ id: "item_202", price: 15.5 }
],
total_price: 25.5
}, null, 2);
}
// 模拟原地修复调用
async repairOrderText(brokenText: string, errorMessage: string): Promise<string> {
console.log(`[Mock LLM] 收到物理修复请求。原损坏文本: "${brokenText.trim()}", 错误原因: "${errorMessage}"`);
// 模拟修复成功:重新返回合法的物理 JSON
return JSON.stringify({
order_id: "ORD-2026-9901-REPAIRED",
items: [{ id: "item_001", price: 49.9 }],
total_price: 49.9
}, null, 2);
}
}
// ---- 3. 隔离队列模拟 ----
class QuarantineQueue {
private queue: Array<{ payload: string; errors: string[]; stage: string; timestamp: Date }> = [];
push(payload: string, errors: string[], stage: 'shape' | 'business') {
const record = { payload, errors, stage, timestamp: new Date() };
this.queue.push(record);
console.error(`🚨 [Quarantine Queue] 已将可疑数据丢入隔离区![阶段: ${stage.toUpperCase()}]`);
console.error(` - 原始 Payload: ${payload}`);
console.error(` - 拦截错误原因: ${errors.join(" | ")}`);
}
getRecords() {
return this.queue;
}
}
const quarantineQueue = new QuarantineQueue();
// ---- 4. 核心防御 Harness 管道 ----
export class OrderHarness {
private llm: MockLLMService;
private maxRetries = 2;
constructor(outputType: MockOutputType) {
this.llm = new MockLLMService(outputType);
}
// 处理物理形状拦截
private parsePhysicalShape(rawText: string): { success: true; data: Order } | { success: false; error: string } {
try {
const parsed = JSON.parse(rawText);
const validated = OrderSchema.safeParse(parsed);
if (!validated.success) {
const errMsg = validated.error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ');
return { success: false, error: `Schema 校验失败: ${errMsg}` };
}
return { success: true, data: validated.data };
} catch (e: any) {
return { success: false, error: `JSON 解析异常: ${e.message}` };
}
}
// 处理业务规则拦截
private validateBusiness(order: Order): { success: true } | { success: false; errors: string[] } {
const errors: string[] = [];
if (order.items.length === 0) {
errors.push("商品列表为空,无法生成订单");
}
const totalSum = order.items.reduce((sum, item) => sum + item.price, 0);
if (Math.abs(totalSum - order.total_price) > 0.01) {
errors.push(`账目不一致:明细项累加值为 ${totalSum},但模型返回总额为 ${order.total_price}`);
}
if (errors.length > 0) {
return { success: false, errors };
}
return { success: true };
}
// 外部执行主入口
async executeWorkflow(): Promise<Order | null> {
let rawOutput = await this.llm.generateOrderText();
let retryCount = 0;
let currentError = "";
// ---- 第一阶段:物理防御环 (含重试) ----
while (retryCount <= this.maxRetries) {
console.log(`[Harness 形状防御] 正在对输出结果进行物理审查... (尝试次数: ${retryCount + 1}/${this.maxRetries + 1})`);
const shapeResult = this.parsePhysicalShape(rawOutput);
if (shapeResult.success) {
console.log("✅ [Harness 形状防御] 物理拦截通过。进入第二阶段校验...");
// ---- 第二阶段:业务防御环 (直落隔离区,不执行原地修复) ----
const businessResult = this.validateBusiness(shapeResult.data);
if (businessResult.success) {
console.log("🎉 [Harness 业务防御] 业务安全检查通过。数据已安全送达下游系统。");
return shapeResult.data;
} else {
console.error("❌ [Harness 业务防御] 拒绝接收此数据:业务逻辑矛盾。正在将 Payload 流转至 Quarantine 队列...");
quarantineQueue.push(rawOutput, businessResult.errors, "business");
return null; // 中断链路,降级保护
}
}
// 物理校验失败,记录当前错误
currentError = shapeResult.error;
console.warn(`⚠️ [Harness 形状防御] 物理检查被拦截:${currentError}`);
if (retryCount < this.maxRetries) {
retryCount++;
console.log(`[Harness 自动恢复] 启动原地退避修复,准备向 LLM 投递修复反馈...`);
rawOutput = await this.llm.repairOrderText(rawOutput, currentError);
} else {
break;
}
}
// 达到重试上限依然无法修复物理形状,流转至隔离区并降级
console.error("🚨 [Harness 严重错误] 达到物理修复上限,依然无法获取合法物理 JSON。注入隔离区...");
quarantineQueue.push(rawOutput, [currentError], "shape");
return null;
}
}
3. 执行脚本测试
新建 testRunner.ts 进行验证驱动:
import { OrderHarness } from "./orderHarness";
async function runTests() {
console.log("=== 场景 A:物理形状破损 JSON,预期执行修复并成功通关 ===");
const harnessA = new OrderHarness("shape_broken");
const resA = await harnessA.executeWorkflow();
console.log("最终产出:", resA ? "成功获取对象" : "失败(已被安全拦截)", "\n");
console.log("=== 场景 B:合法 JSON 但业务数据矛盾,预期直接丢入 Quarantine 队列,无重试 ===");
const harnessB = new OrderHarness("business_broken");
const resB = await harnessB.executeWorkflow();
console.log("最终产出:", resB ? "成功获取对象" : "失败(已被安全拦截)", "\n");
console.log("=== 场景 C:黄金流程,完全合法的数据 ===");
const harnessC = new OrderHarness("valid");
const resC = await harnessC.executeWorkflow();
console.log("最终产出:", resC ? "成功获取对象" : "失败(已被安全拦截)", "\n");
}
runTests();
可以通过 npx ts-node testRunner.ts 运行测试并观察控制台输出逻辑的完整演变。
验证与验收:构建你自己的输出防御测试集
在本节课的学习中,你的学习交付物包括:
- 基于 Zod 构建的物理 Schema 验证文件;
- 具备物理与业务双层过滤规则的校验中间件;
- 物理退化下的指数级修复重试函数;
- 当物理与业务规则失效时能安全兜底并执行记录的隔离队列设计。
验收方式与自测实验
你可以通过修改 testRunner.ts 来输入以下边缘用例,确保你的 Harness 机制行为正确:
- 用例 1(物理空值):让 LLM 输出空字符串
""或非 JSON 字符"Error: model overloaded.",预期:Harness 捕获 JSON 物理异常,重试 2 次均失败后,记录shape_failed报错信息并顺利丢入Quarantine,系统降级无崩溃。 - 用例 2(业务边界):修改
items内的商品单价为-1。预期:在第一阶段直接被 Zod 阻断,拦截于物理校验层(因为positive()限制属于基本物理形状约束),触发物理重试。这验证了 Zod schema 是否成功将负值拦截于外。 - 用例 3(多步 Trace 对账):结合 Mock 计数器。确保在第一阶段重试时,每次重试均能生成一条包含
gen_ai.response.validation.retry_count递增指标的观测日志。如果该指标被第三方评测系统采集,应该能在其测试看板内反映由于 LLM 格式损坏导致的多步开销损耗。
来源、复核与时效说明
本课程内容的设计基于以下前沿开源协议与主流云端大模型接口的最佳应用工程规范编写:
- 关键来源:
- OpenAI Function calling & Structured Outputs: 参考 OpenAI 官方文档关于指定特定物理 JSON Schema 的标准限制条件,推导出物理拦截在客户端或中间件重试的补救边界。
- Anthropic Tool Use: 参考 Tool use with Claude 关于物理校验错误返回
tool_result的机制,推导出原地修复(In-context Repair)架构。 - Model Context Protocol (MCP): 参考 MCP 开源规范中强调的外部数据源与协议边界隔离,得出不信任外部 Server 返回实体、必须在物理形状侧由内侧 Harness 统一兜底的结论。
- OpenTelemetry GenAI Semantic Conventions: 参考关于 GenAI 领域的 Span 和 Metric 属性命名标准,在 Harness 中融入
gen_ai.response.validation等规范,为评测、回溯以及生产告警奠定了标准度量机制。
- 访问时间:2026-05-28。
- 时效与复核触发条件:
- 当 OpenTelemetry 社区将 GenAI 语义规范(GenAI Semantic Conventions)推进到 1.0.0 稳定版且合并至官方 SDK 时,需复核文中相关字段的名称定义。
- 当大语言模型厂商(如 OpenAI、Anthropic、Google)的原生 API 完全保证 100% 格式对齐、且能保证物理层零截断时(如物理 Sandbox 的严格 JSON 解析模式在各类轻量模型中完全普及),输出层第一道防线的物理重试次数和策略可调整为静态降级保护。
结构化输出Harness:先挡住形状错误,再处理业务错误:把判断写进 Harness 证据链
本课的交付物要能进入 CI、评测台账或人审流程,而不是停在文档里。本课交付物是 一个输出 schema、校验中间件、失败重试和 quarantine 队列设计,它必须能被复跑、复核、追踪和复盘。
如果 结构化输出Harness:先挡住 还没有对应的输入样例,先不要讨论自动化覆盖率,因为没有样例就无法判断 harness 是否真的抓住问题。 如果一次评测失败会影响发布,应该保留失败输入、模型输出、工具轨迹和判分理由,否则团队只能凭记忆争论。 如果你准备把某个结果自动放行,必须先写清人工复核的例外条件,只有例外条件明确时,门禁才不会变成新的风险源。
工具执行异常要同时查参数、权限、幂等键、审计日志和人工确认记录。围绕 结构化输出Harness:先挡住形状 做排查时,记录步骤、样例、指标、风险、修复动作和复测结果。
OpenAI 的《Function calling》说明:支撑工具调用、应用层执行、参数 schema 和工具 harness 设计。;这意味着 结构化输出Harness:先挡住 要把来源转成可执行断言。Anthropic 的《Tool use with Claude》提醒:支撑 Claude tool_use/tool_result 循环、工具执行顺序和 harness 边界。;因此本课必须写清自动判断和人工判断的边界。OpenTelemetry 的《OpenTelemetry GenAI semantic conventions》提供的证据是:支撑 GenAI span、metric、event、token/latency 属性和标准化观测。;所以当前结论按 2026-05-28 的来源状态使用。
复核触发条件要具体:如果模型 API/SDK、评测工具版本、Agent 框架、RAG 指标、安全规范、合规政策、平台 release/changelog 或关键来源页面变化,需要重新运行本课样例并更新 harness。
练习验收:把 结构化输出Harness:先挡住形状 接入一个最小 AI 应用,提交包含输入样例、评测结果、失败诊断、来源证据、人工复核结论和下一步修复动作的记录。缺少任何一项,都标记为 needs_review。