文章提出用任务完成率、步数分布和工具调用有效率三个代理指标,在用户投诉前发现Agent退化。
传统的监控回答的是"系统还活着吗"。对于 Agent 来说,它永远活着。你真正关心的退化表现为:状态码一样、延迟一样,但回答质量下降了,工具调用出错的频率更高了。
没有人会发警报给你。三周后客服说"最近这东西越来越不好用了",但没人说得清是什么时候开始的。
有两个东西可以解决这个问题。代理信号(在任何人投诉之前就会变动的指标),以及一个固定的测试集,可以反复运行来确认代理信号所暗示的问题。
这些指标都不直接测量质量,但都与质量相关,而且收集起来都是免费的。
每一次运行都恰好结束于一种终结状态。
export type Outcome =
| "complete" | "refused" | "gave_up"
| "max_turns" | "budget_exceeded" | "tool_failed" | "timeout";
metrics.increment("agent.outcome", 1, { outcome, model, promptVersion });
完成(complete)的占比是仪表盘上最有用的一个数字。它会因为真实的原因而变动——模型降级、工具损坏、提示词变更——而且会在有人提工单前几天就开始变动。
如果同一个任务 Agent 需要更多步骤才能完成,说明它在变差。
metrics.histogram("agent.turns", turns, { intent, model });
关注 p50,而不是均值。p50 从 4 变到 6,即使均值保持平稳,也是一个真实的回归。
模型生成的参数无法通过 schema 校验的频率。
const parsed = schema.safeParse(call.input);
metrics.increment("agent.tool_args", 1, {
tool: call.name,
valid: String(parsed.success),
});
这个指标异常敏感。一个悄然变动的模型在文字质量明显下降之前,schema 校验失败率就会显著升高。
Agent 用略微不同的参数反复调用同一个工具,或者在不同轮次之间自相矛盾的频率。
const repeats = countRepeatedCalls(trace); // same tool, similar args
metrics.histogram("agent.repeat_calls", repeats, { model });

代理信号告诉你有东西变了,但它们无法告诉你答案是否真的变差了。为此你需要一组固定的输入和已知的正确答案,按计划反复运行。
export type GoldenCase = {
id: string;
input: string;
mustCall?: string[]; // tools that should be used
mustNotCall?: string[]; // tools that must not be
assert: (out: AgentOutput) => Promise<Verdict>;
};
断言是难点部分,而且应该尽可能地确定性。用可检验的属性来判断,而不是评判文字表述:
{
id: "refund-under-limit",
input: "I want a refund for order ord_4471, it arrived broken",
mustCall: ["get_order", "refund_order"],
mustNotCall: ["send_email"],
assert: async (out) => {
const call = out.toolCalls.find((c) => c.name === "refund_order");
if (!call) return fail("no refund attempted");
if (call.input.orderId !== "ord_4471") return fail("wrong order");
if (call.input.amountCents !== 8900) return fail("wrong amount");
return pass();
},
}
这类结构性断言在不同模型版本和反复运行中都是稳定的。对措辞的断言则不是——它们会在同义改写时失败,然后训练你忽略整个测试套件。
对于输出质量本质上就是文字的情况,使用带评分标准的模型评判者,并接受它有更多噪音。把这些用例跑三次,取多数结果。
// nightly, plus on every deploy that touches prompts, tools or model
export async function runGolden(): Promise<GoldenReport> {
const cases = await loadCases();
const results = await pMap(cases, async (c) => {
const out = await runAgent(c.input, prodLikeCtx()); // real prompts, stub tools
return { id: c.id, verdict: await c.assert(out), turns: out.turns };
}, { concurrency: 4 });
const passed = results.filter((r) => r.verdict.ok).length;
metrics.gauge("golden.pass_rate", passed / results.length);
return { results, passRate: passed / results.length };
}
使用录制的响应来 stub 工具,这样套件测量的是模型和提示词,而不是你集成那边出了什么状况。真实的 system prompt、真实的工具 schema、真实的模型 ID。
夜间运行很关键,因为你要防范的是你没有主动做的变更。提供方一侧的变更不需要发版就会生效,而只在 CI 中运行的套件是看不到它的。
const today = await runGolden();
const base = await loadBaseline();
const regressed = base.results.filter((b) =>
b.verdict.ok && !today.results.find((t) => t.id === b.id)?.verdict.ok);
if (regressed.length >= 2) {
await pager.notify(`golden: ${regressed.length} cases regressed`, {
cases: regressed.map((r) => r.id),
model: process.env.MODEL_ID,
});
}
说出具体的用例名称才能让凌晨三点的告警有可操作性。"通过率降到了 87%"只能让人耸耸肩;但"refund-under-limit 和 refund-over-limit-needs-approval 都退化了"才是一个诊断结论。
用两个用例而不是一个,因为单个不稳定的用例最终一定会触发,然后训练所有人都忽略这个告警。
logger.info("agent run", {
runId, outcome, turns, costUsd,
model: cfg.model,
promptVersion: cfg.promptVersion,
toolsetVersion: cfg.toolsetVersion,
retrievalIndexVersion: cfg.indexVersion,
});
当质量下降时,第一个问题是"什么变了"。每次运行带上四个版本字段就把这个问题变成了一次查询。没有它们,就成了一次跨越发布日志和提供商变更日志的考古工作。

问用户。对输出点一个赞或踩,与 run id 一起存储。
await db.feedback.create({ data: { runId, verdict, userId } });
响应率很低,而且样本偏向于不满的用户。但它仍然是唯一一个来自真正知道答案是否有用的人发出的信号——将它与运行轨迹关联起来,就把一条投诉变成了一個可复现的用例,而这正是黄金集最需要的东西。
《AI That Ships》涵盖了 AI 特性的评估和监控——代理指标、黄金集和结构性断言、回归告警,以及让"什么变了"变得有答案的版本化管理。
