作者展示了 agent 工具调用的 trace 结构设计(args_hash 而非原始参数、token 使用量记录),以及 eval case 调试方法。
"运行评估集"和"当前 agent 是否健康"是两个不同的问题,而且很容易只建第一套基础设施。评估集离线运行,针对的是你已经想到的 case。生产流量不会问你"能不能给我发一个你没预料到的票类型",它直接就发了。可观测性就是用来告诉你这种事正在发生的——说起来不太光鲜,但它同样是一切离线评估得以实现的前提:没有逐步记录 agent 实际做了什么,你就无法调试一个失败的评估 case。
这个 build 中每次工具调用都会记录一条结构化记录——src/trace.ts:
export interface TraceStep {
trace_id: string;
step_id: number;
tool_name: string;
args_hash: string; // 哈希存储,永远不存原始参数
duration_ms: number;
result_summary: string;
model: string;
token_usage: { input: number; output: number };
}
这里有两个细节看起来微不足道,实则不然:
args_hash,而不是原始参数。 这条 trace 日志设计为可以放心地长期保留、发送到监控系统、或粘贴到 bug 报告中——这些场景都不应该让你担心工具调用参数里可能嵌入了什么秘密。哈希意味着你仍然可以确认两次调用使用了相同的参数(比如调试幂等性时),但永远不需要持久化实际的值:
export function hashArgs(args: Record<string, unknown>): string {
return "sha256:" + createHash("sha256").update(JSON.stringify(args)).digest("hex").slice(0, 8);
}
result_summary,截断版本。 完整的工具结果可能很大(比如一个 kb_search 返回完整文章正文的情况)—— 每一步都记录完整内容会让 trace 输出无法阅读,也会让存储它的系统膨胀。summarizeResult 取前几个字段并截断过长的值:
const MAX_FIELD_LEN = 70;
export function summarizeResult(result: Record<string, unknown>): string {
const entries = Object.entries(result).slice(0, 4);
return entries.map(([k, v]) => `${k}=${truncate(JSON.stringify(v))}`).join(" ");
}
终端里可读,其他地方结构化
trace 日志同时充当 CLI 输出——这个 build 的核心目标就是可检查,所以实时观察 agent 运行很重要。早期每行是一个原始 JSON blob,技术上完整但实际完全不可读。解决方案是做了一轮格式化,而不是引入新的日志系统:
export function logTrace(step: TraceStep): void {
const timing = `${step.duration_ms}ms, ${step.token_usage.input}→${step.token_usage.output} tok`;
console.log(` ${DIM}[${step.step_id}]${RESET} ${CYAN}${step.tool_name}${RESET} ${DIM}(${timing})${RESET}`);
console.log(` ${step.result_summary}`);
}
在真实终端中的输出:
[1] order_lookup (0ms, 1077→23 tok)
order_id="ord_1005" status="processing" items=[...] total_usd=45
[2] kb_search (1ms, 1268→20 tok)
articles=[...] relevance_scores=[0.48,0.24,0.24]
颜色会在 stdout 不是真实 TTY 时自动禁用(process.stdout.isTTY),所以把这个输出 pipe 到文件或 CI 日志时不会留下字面的转义码垃圾——小事一桩,但正是这类小事决定了 trace 日志是被人真正阅读还是被忽略。
每步 trace 是基础,但上面还有三个不同层次,各自回答不同问题:
Trace logging —— 每次运行、每次工具调用:输入、响应、延迟、成本。这是从上面的载荷已经得到的东西。
Online metrics —— 时间维度的聚合数字:成功率、升级率、每次运行的平均工具调用数、每次运行的平均 token 成本。比如你会在这一层注意到升级率在一周内从 8% 悄悄爬升到了 15%。
Drift detection —— 周与周之间的指标对比。这个最容易跳过但最不应该跳过:模型提供商的更新会导致静默的回归,而且你的代码完全没有任何改动。上个月在评估集上得分 95% 的 agent,这个月可能开始出现不同类型的失败——是因为底层模型变了,不是因为你写的任何东西变了。
这个 repo 没有实现 online metrics 或 drift detection——它是一个 CLI 参考 build,没有持续请求量来做聚合——但 trace 载荷的写法专门为后续叠加这些层预留了空间,而不需要改动 tracing 代码本身。这才是真正的设计目标:最小载荷不是"你现在需要的指标",而是"任何指标系统以后都会需要的原始材料"。
回到 Part 3 的那个评估失败:
[FAIL] hard_04 (hard)
- trajectory: expected [order_lookup, refund_eligibility, issue_refund] as a subsequence, got [order_lookup, refund_eligibility]
这条失败信息的存在,完全是因为 tracing。没有工具调用的结构化逐步记录,"评估失败了"就是你得到的全部信息——而不是为什么失败。那个失败背后的实际 bug(Part 5 的幽灵退款提议),之所以能被找到,正是因为轨迹是可见的,而不仅仅是最终的通过/失败结果。
Part 7: Iterating to Green: Real Bugs, and When You'd Actually Reach for a Framework → 完结本系列:完整迭代日志——用这个 agent 对评估集运行发现的所有真实 bug、每个 bug 的修复方式,以及什么时候你实际上应该引入一个框架而不是继续用这个原始循环。