提出五条质量门禁特性:确定性、单一性、可操作、可版本化、可移植,并明确区分结构化验证(确定性规则)与语义评估(LLM裁判)的适用场景。
当确定性 Agent 测试被表达为可复用的质量门而非散落在测试文件里的一次性断言时,它的价值会大得多。
门要回答的是一个狭义的工程问题:运行在写数据前是否经过了验证?重试次数是否在策略范围内?Token 用量是否被记录且在预算内?每个启动的 span 是否都结束了?结果应该足够稳定、快速和具体,让开发者知道该修复什么。
LLM 法官在语义评估中仍有作用。它们不应该是阻止结构性 Agent 回归进入生产的唯一防线。
一个实用的质量门有五个属性:
确定性:相同的归一化 trace 产生相同的结果。
狭义性:它评估一个明确的契约。
可操作性:失败能识别出规则、证据和期望状态。
可版本化:规则和 trace schema 的变更可以被审查。
可移植性:它可以在本地运行、在 CI 中运行,也可以针对回放的 fixture 运行。
"答案质量 6/10"是一个信号,但它不是一个狭义的工程契约。"写工具在授权完成前运行了"才是。
门引擎应该消费归一化的元数据,而不是框架特定的回调对象。
type StepKind = 'run' | 'model' | 'tool' | 'retrieval' | 'policy' | 'fallback';
type TraceStep = {
id: string;
parentId: string | null;
sequence: number;
name: string;
kind: StepKind;
status: 'ok' | 'error' | 'blocked' | 'cancelled';
attempt?: number;
inputTokens?: number;
outputTokens?: number;
durationMs?: number;
metadata?: Record<string, string | number | boolean | null>;
};
type AgentTrace = {
schemaVersion: 1;
fixture: string;
status: TraceStep['status'];
steps: TraceStep[];
};
在评估前归一化易失字段。随机 ID 可以保留用于父子检查,但时间戳、临时路径、请求 ID 和原始 payload 不应影响确定性结果。
规则应返回结构化证据,而不是直接抛出断言错误。
type Severity = 'error' | 'warning';
type RuleResult = {
ruleId: string;
severity: Severity;
passed: boolean;
message: string;
evidence?: Record<string, string | number | boolean>;
};
type TraceRule = {
id: string;
version: number;
severity: Severity;
evaluate(trace: AgentTrace): RuleResult;
};
function result(
rule: TraceRule,
passed: boolean,
message: string,
evidence?: RuleResult['evidence'],
): RuleResult {
return {
ruleId: `${rule.id}@${rule.version}`,
severity: rule.severity,
passed,
message,
evidence,
};
}
对规则进行版本化使基线变更变得显式。如果 max_model_calls 的含义变了,审查者能看到是策略变了,而不是假设 Agent 发生了回归。
function requireSteps(required: string[]): TraceRule {
return {
id: 'required_steps',
version: 1,
severity: 'error',
evaluate(trace) {
const actual = new Set(trace.steps.map((step) => step.name));
const missing = required.filter((name) => !actual.has(name));
return result(
this,
missing.length === 0,
missing.length === 0
? 'All required steps ran'
: `Missing required steps: ${missing.join(', ')}`,
{ missingCount: missing.length },
);
},
};
}
必需步骤规则适用于验证、检索、策略检查和强制清理。不要要求每一个实现细节;门应该保护对用户、成本、安全或正确性重要的行为。
对于顺序依赖,比较 recorder 的单调序列。对于并发工作,使用父子关系而非完成顺序来做断言。
function requireOrder(before: string, after: string): TraceRule {
return {
id: `order:${before}:${after}`,
version: 1,
severity: 'error',
evaluate(trace) {
const left = trace.steps.find((step) => step.name === before);
const right = trace.steps.find((step) => step.name === after);
if (!left || !right) {
return result(this, false, 'Cannot evaluate order: step missing');
}
return result(
this,
left.sequence < right.sequence,
left.sequence < right.sequence
? `${before} occurred before ${after}`
: `${after} occurred before required dependency ${before}`,
{ beforeSequence: left.sequence, afterSequence: right.sequence },
);
},
};
}
授权前写和检索前生成是好的因果门。对每个 trace 步骤排序会制造脆弱的测试并阻止无害的并行化。
const noExternalWorkAfterBlock: TraceRule = {
id: 'no_external_work_after_block',
version: 1,
severity: 'error',
evaluate(trace) {
const block = trace.steps.find((step) => step.status === 'blocked');
if (!block) return result(this, true, 'Run was not blocked');
const forbidden = trace.steps.filter((step) => {
return (
step.sequence > block.sequence &&
(step.kind === 'model' || step.kind === 'tool')
);
});
return result(
this,
forbidden.length === 0,
forbidden.length === 0
? 'No model or tool work occurred after the block'
: `External work continued after block: ${forbidden
.map((step) => step.name)
.join(', ')}`,
{ forbiddenCount: forbidden.length },
);
},
};
这比只检查最终状态更强。一个运行可能报告 blocked,但如果编排错误地继续,仍可能泄露 model 或 tool 调用。
重试应包含一个显式的 attempt 字段。按操作和父 span 计算尝试次数,而不是查找连续名称,因为并行事件可能交错。
function maxAttempts(stepName: string, limit: number): TraceRule {
return {
id: `max_attempts:${stepName}`,
version: 1,
severity: 'error',
evaluate(trace) {
const attempts = trace.steps
.filter((step) => step.name === stepName)
.map((step) => step.attempt ?? 1);
const maximum = attempts.length === 0 ? 0 : Math.max(...attempts);
return result(
this,
maximum <= limit,
maximum <= limit
? `${stepName} stayed within ${limit} attempts`
: `${stepName} reached attempt ${maximum}; limit is ${limit}`,
{ maximumAttempt: maximum, limit },
);
},
};
}
对于更广泛的循环检测,定义一个稳定的状态签名,例如 planner_state + selected_tool + outcome。当相同签名超出策略地重复时失败。在合法的迭代工作流中,仅靠步骤名模式匹配通常会产生误报。
缺失的使用数据不应静默地变成零。当没有测量数据时,成本门不能通过。
function maxTotalTokens(limit: number): TraceRule {
return {
id: 'max_total_tokens',
version: 1,
severity: 'error',
evaluate(trace) {
const modelSteps = trace.steps.filter((step) => step.kind === 'model');
const missingUsage = modelSteps.filter((step) => {
return step.inputTokens === undefined || step.outputTokens === undefined;
});
if (missingUsage.length > 0) {
return result(this, false, 'Model usage is missing', {
missingUsageCount: missingUsage.length,
});
}
const total = modelSteps.reduce((sum, step) => {
return sum + (step.inputTokens ?? 0) + (step.outputTokens ?? 0);
}, 0);
return result(
this,
total <= limit,
total <= limit
? `Token usage ${total} is within budget ${limit}`
: `Token usage ${total} exceeds budget ${limit}`,
{ totalTokens: total, limit },
);
},
};
}
根据提供商的不同,缓存的输入可能需要自己的字段和成本策略。将原始使用维度保留在归一化 trace 中,这样门就不依赖一个有损的 totalTokens 值。
在评估 Agent 行为之前,验证遥测本身是否可信:
每个非根父节点存在于同一个 trace 中。
序列号唯一且单调。
恰好存在一个根步骤。
存在终止状态。
数值指标有限且非负。
不出现禁止的 payload 键。
一个格式错误的 trace 应该以一个检测工具错误告终,而不是产生误导性的策略结果。
type GateReport = {
fixture: string;
passed: boolean;
failures: RuleResult[];
warnings: RuleResult[];
results: RuleResult[];
};
function evaluateTrace(
trace: AgentTrace,
rules: TraceRule[],
): GateReport {
const results = rules.map((rule) => rule.evaluate(trace));
const failures = results.filter((item) => {
return !item.passed && item.severity === 'error';
});
const warnings = results.filter((item) => {
return !item.passed && item.severity === 'warning';
});
return {
fixture: trace.fixture,
passed: failures.length === 0,
failures,
warnings,
results,
};
}
将报告写成 JSON 用于自动化,写成简短的 Markdown 用于 pull request 日志或 CI 注解。包含规则版本、证据、fixture 名称,以及到归一化 trace 产物的链接或路径。
精确的 trace 快照难以维护。优先使用持久化指标的摘要:
type TraceBaseline = {
fixture: string;
requiredTools: string[];
maximumModelCalls: number;
maximumTokens: number;
maximumAttemptsByTool: Record<string, number>;
};
同时使用绝对和相对限制。从 100 到 150 增长 50% 的 token 可能无害;但从 20,000 到 30,000 增长 50% 可能成本高昂。相反,增加 500 token 对小型工作流意义重大,对大型工作流则是噪声。
要求在同一个 pull request 中同时进行行为变更和基线更新。审查应解释为什么新的预算或工具路径是可接受的。
脚本化编排测试应使用假时钟和精确预算。真实模型和网络测试有自然方差,需要更宽的统计阈值。
不要对两者使用同一阈值。一个本地 fixture 突然花费十秒可能表明存在 bug。一次越过窄 latency 阈值的真实提供商调用可能只反映了瞬态基础设施状况。
对于真实运行,比较足够样本下的滚动分布,如中位数和尾延迟。除非项目有能力可靠地运行这些检查,否则将这些趋势检查放在最快的 pull request 门之外。
一个提供商中立的 CI 任务可以遵循以下顺序:
1. Run scripted agent fixtures
2. Validate every normalized trace
3. Evaluate the configured rule set
4. Write JSON and Markdown reports
5. Exit non-zero when error-severity rules fail
6. Upload reduced trace artifacts for failed fixtures
7. Retain artifacts for a short, explicit period
使用项目现有的运行时版本文件和使用包管理器 lockfile,而不是将设置细节硬编码到文章或门引擎中。真实模型凭证应对确定性任务不可用。
在一个独立的任务中运行语义评估,并具有明确的授权、成本控制和一个更慢的节奏。
太多脆弱的门会让开发者忽略整个系统。只有当一条规则保护一个有意义的契约且有一个负责人时,才添加它。
对正确性、安全性或硬预算违规使用 error。对需要审查但不应立即阻止的趋势使用 warning。跟踪 warning 的时效;一个永远无法操作的 warning 应该被移除或转化为真正的策略。
当一个门因可接受的行为反复失败时,修复规则或基线。不要将永久的红 CI 正常化。
Agent 质量门在将执行视为工程产物时效果最好。一个归一化的 trace、一套版本化的规则和一个证据丰富的报告,可以在几秒内捕获缺失的验证、未授权的工作、重试风暴、成本回归和损坏的检测工具。
对 trace 可以证明的契约使用确定性门。将语义法官留给语言和推理质量,那里才真正需要概率评估。这种分离使 CI 更快,使每个失败都更值得信赖。
下一篇文章将把规则转化为集成:适配器如何将不同的 TypeScript Agent 框架翻译成一个 trace 模型,而不将核心耦合到任何一个单一 SDK。