一个 Agent Runtime 失败案例的完整 Trace
Agent 出问题后,最没用的一句话是:
模型不稳定。
它可能是真的。
但它不能指导修复。
如果你只剩聊天记录,确实很容易把问题归到模型身上。模型说它修好了,用户说没修好,日志里只有几段自然语言。最后大家换模型、改 prompt、再试几次。
这不是工程。
工程要能定位:
是模型判断错?
是工具失败?
是权限拒绝?
是上下文没带对?
是 checkpoint 状态错?
是 Runtime 顺序错?
下面写一个完整失败案例。
不是为了编故事。
是为了说明 trace 应该怎么用。
事故:Agent 说修好了,但测试仍然失败
用户任务:
修复 math.ts 里的 add 函数,让 npm test 通过。
项目里有两个文件:
src/math.ts
src/math.test.ts
math.ts 里有 bug:
export function add(a: number, b: number): number {
return a - b;
}
测试文件里还有一个隐藏约束:
import { add } from "./math";
test("add supports positive numbers", () => {
expect(add(1, 2)).toBe(3);
});
test("add keeps number return type", () => {
expect(typeof add(1, 2)).toBe("number");
});
Agent 执行后回答:
修复完成,add 已改为加法。
但 npm test 失败。
如果只有最终回答,你会觉得模型在胡说。
但 trace 里能看到更具体的问题。
Trace Summary
一次失败 run 的摘要大概是:
runId: run_repair_20260816_001
traceId: trace_repair_20260816_001
status: failed
stopReason: tool_error
steps: 4
工具证据面板:
| Step | Tool | Status | Permission | Evidence |
| --- | --- | --- | --- | --- |
| 1 | list_files | ok | allow | README.md package.json src/... |
| 1 | read_file | ok | allow | src/math.ts return a - b |
| 2 | write_text | ok | allow | wrote src/math.ts |
| 3 | shell_readonly | error | allow | npm test exitCode=1 |
第一眼看,好像工具都正常。
继续看 span。
Step 1:Context Builder 没带测试文件
context.build span:
{
"name": "context.build",
"status": "ok",
"attributes": {
"step": 1,
"context.budget": 2800,
"context.used": 2410,
"context.included_files": ["README.md", "package.json"],
"context.dropped_items": [],
"context.debug_report": "context-step-1.json"
}
}
这里已经有味道了。
用户要求修复测试失败,但上下文里只带了 README 和 package.json。没有 src/math.ts,也没有 src/math.test.ts。
不过模型第一步调用了工具:
{
"name": "model.complete",
"status": "ok",
"attributes": {
"responseKind": "tool_calls",
"toolCallCount": 2
}
}
工具调用:
[
{ "name": "list_files", "input": { "path": ".", "depth": 3 } },
{ "name": "read_file", "input": { "path": "src/math.ts" } }
]
它读了源码文件。
但没有读测试文件。
这不是工具失败。
这是上下文和工具路径选择的问题。
Step 2:模型直接写了源码
permission.review:
{
"name": "permission.review",
"status": "ok",
"attributes": {
"toolName": "write_text",
"action": "allow",
"risk": "write",
"reason": "命令行显式开启 --allow-writes。"
}
}
写入工具结果:
{
"toolName": "write_text",
"status": "ok",
"metadata": {
"durationMs": 14,
"outputChars": 58,
"truncated": false,
"sideEffects": [
{
"type": "write_file",
"relativePath": "src/math.ts"
}
]
}
}
Runtime 做对了两件事:
写入前走了 PermissionGate。
写入结果记录了 sideEffects。
但它没阻止模型在没有读取测试文件的情况下直接修改源码。
这个判断要不要由 Runtime 强制?
不一定。
但至少 trace 要让你看见:模型只读了源码,没有读测试。
Step 3:npm test 失败
tool.execute span:
{
"name": "tool.execute",
"status": "error",
"attributes": {
"toolName": "shell_readonly",
"risk": "shell",
"command": "npm test",
"exit_code": 1,
"output.truncated": false
}
}
工具 observation:
command: npm test
status: error
exitCode: 1
stdout:
FAIL src/math.test.ts
add keeps number return type
Expected: "number"
Received: "bigint"
现在真相出来了。
Agent 把 add 改成了 BigInt 版本:
export function add(a: number, b: number): number {
return BigInt(a) + BigInt(b) as unknown as number;
}
这听起来很离谱。
但别急着骂模型。
它没有看到测试约束。
它在缺证据的情况下做了过度泛化。
Step 4:模型给了错误总结
测试失败后,模型没有继续修。
它返回 final:
{
"kind": "final",
"text": "已修复 add 函数。测试失败与 BigInt 类型断言有关,后续可以调整测试。"
}
这又是一个问题。
Runtime 没有做最终验收门禁。
对'修复测试'这类任务,如果 npm test 失败,final 不应该被当成成功完成。
这里根因不是单一的。
可以拆成两层:
直接失败点:npm test exitCode=1。
根因 1:Context Builder 没优先带同名测试文件。
根因 2:Runtime 没有把 command failed 作为验收失败门禁。
这比'模型不稳定'有用得多。
事故归因表
| 维度 | 结论 | 证据 |
|---|---|---|
| 模型 | 过度泛化,未继续修复失败测试 | final 出现在 npm test failed 后 |
| 工具 | shell 工具正常返回失败 | tool.execute status=error, exitCode=1 |
| 权限 | 写入经过批准 | permission.review action=allow |
| 上下文 | 未包含测试文件 | context.included_files 不含 src/math.test.ts |
| Runtime | 缺少验收门禁 | command failed 后仍允许 final 成功 |
| Checkpoint | 有文件 side effect | write_text sideEffects 记录 |
这张表才是工程复盘。
它把'模型错了'拆成可以改的点。
修复方案 1:Context Builder 优先带相邻测试
当任务目标包含:
测试失败
修复测试
npm test
Context Builder 应提高测试文件优先级。
规则可以先很简单:
如果读取 src/foo.ts,同时存在 src/foo.test.ts / test/foo.test.ts:
下一轮 context 优先包含测试文件片段。
debug report 应写:
{
"included": ["src/math.ts", "src/math.test.ts"],
"reasons": {
"src/math.test.ts": "linked test file for src/math.ts and user goal mentions test"
}
}
这样下次再出问题,你能知道规则是否生效。
修复方案 2:失败命令阻止成功 final
如果用户目标是'让测试通过',最近一次测试命令失败,Runtime 可以要求模型继续修复或明确失败。
规则:
如果 goal includes test/pass/fix
且最近 shell_readonly(command="npm test") exitCode != 0
则 final answer 不得标记为 completed
stopReason = validation_failed
注意,不是禁止模型回答。
而是不能把 run 状态标成 completed。
最终输出可以是:
我修改了 src/math.ts,但 npm test 仍失败。失败点是 src/math.test.ts 的 return type 断言。
当前 run 未完成。建议继续读取测试文件并修复实现。
这比'已修复'诚实。
修复方案 3:把事故转成 Eval
新增 eval case:
{
"id": "repair-math-test-001",
"title": "repair add and verify tests",
"scenario": "repair",
"userGoal": "修复失败测试,让 npm test 通过。",
"expected": {
"requiredTools": ["read_file", "write_text", "shell_readonly"],
"mustReadFiles": ["src/math.ts", "src/math.test.ts"],
"commandsPass": ["npm test"],
"forbiddenTools": ["network"],
"maxSteps": 8
}
}
断言不要只看最终回答。
要看:
是否读了测试文件
是否跑了 npm test
npm test 是否通过
是否没有联网
是否没有修改无关文件
这才会防止同类事故再发生。
Trace 设计要避免两个坑
第一个坑:trace 里保存完整敏感内容。
不要把完整 prompt、完整文件、完整 stdout、secret、cookie 全塞进 trace。大内容用 artifact ref。敏感内容脱敏。
第二个坑:trace 只有最终日志,没有 span。
你需要看到:
context.build
model.complete
permission.review
tool.execute
checkpoint.save
否则失败路径还是一团。
一份好的失败复盘模板
可以固定成五段:
用户目标:
修复 math.ts,让 npm test 通过。
执行路径:
读目录 -> 读 src/math.ts -> 写 src/math.ts -> 跑 npm test -> final。
失败点:
tool.execute(shell_readonly npm test) exitCode=1。
根因:
Context Builder 未包含同名测试文件;Runtime 未阻止失败测试后的成功 final。
回归方案:
新增 eval repair-math-test-001,断言必须读取测试文件、npm test 通过、最终状态 completed。
每次事故都这么写,团队会越来越清楚 Runtime 的薄弱点。
结论
Agent trace 不是日志美化。
它的价值是把一句'模型不稳定',拆成可定位、可修复、可回归的工程问题。
如果 trace 只能证明'模型说了什么',那它还不够。
它要证明:
模型看见了什么
模型计划了什么
Runtime 允许了什么
工具实际做了什么
失败发生在哪里
下次怎么防住


