传统软件的 bug 多在"代码逻辑出错了"——你能读 stack trace。Agent 的 bug 多在"模型决策出错了"——stack trace 给你的是一句它怎么想的,而不是它怎么错的。本章讲怎么把 Agent 这种黑盒变白盒,让你和 Agent 一起 debug。

14.1 Agent bug 的四种错误形态

错误形态              现象                典型解法         根因层
─────────────────────────────────────────────────────────
Prompt bug           模型理解任务错误     改 prompt + 例    Prompt
Tool bug             tool 拖了 water          fix tool 实现     Tool  
Loop bug             卡 / 自我提前终         调预算/校验       Loop
Context bug          信息被误信 / 没看见     压缩/重排         Context

这四层 bug 出错的修法是完全不同的,混在一起 = 接 debug。要建立"先分类 → 后修"反射: 先问错在哪层。

14.2 三种必须的工件

任何 Agent harness,至少要留下三种工件,否则 debug = 玩猜:

工件 作用 何时写
Trace 日志 每步 thought / action / observation 每次 tool_use 前后
Token 用量 每步 input/output tokens + 累计 API 响应回来的瞬间
Final state 终止时的 history tail / failures / artifacts stop_reason 触发
版本 - 额外一项 Prompt 版本号 + 模型版本 session 启动

OpenCode 默认在 .opencode/log/ 下记录,Claude Code 在 ~/.claude/logs。你的自写 harness 也要把这三件写下来。

14.3 极简 Trace Format

import dataclasses, json, datetime
from pathlib import Path

@dataclasses.dataclass
class Step:
    id:               int
    ts:               str
    user_or_assistant:str
    tool_name:        str
    tool_input:       dict
    tool_output:      str
    input_tokens:     int
    output_tokens:    int
    cum_total_tokens: int
    note:             str = ""

def write_trace(steps: list[Step], out: Path = Path(".trace")) -> None:
    out.mkdir(parents=True, exist_ok=True)
    (out / "trace.jsonl").write_text(
        "\n".join(json.dumps(dataclasses.asdict(s), ensure_ascii=False) for s in steps)
    )

def read_trace(out: Path = Path(".trace")) -> list[Step]:
    return [Step(**json.loads(l)) for l in (out / "trace.jsonl").read_text().splitlines()]

jsonl 而不是 json - 你可以 tail -f 实时盯着 trace。

14.4 调试 5 招

14.4.1 单步重放

# 拿一段失败 session 的 trace,重放每一步并让你深入
python tools/replay.py .trace/trace.jsonl --interactive

实现:把 trace 当作固定 history 喂给模型,在第 N 步停下,让你用 REPL 修改后继续。找到决策错误的那一步的定位神器。

14.4.2 PAT (Print All Tool calls)

# 几次工具调用,让常用工具的输出更"易读"
jq -r '. | select(.tool_name != null) | "Step \(.id): \(.tool_name)(\(.tool_input)) -> \(.tool_output[:200])"' \
   .trace/trace.jsonl

归档 trace 不止给当下用 - 还可以在 60 天后回溯显式崩。

14.4.3 Diff First, Code Second

Agent 改 patch 看完,只看 model 写的 diff, 不看 file after。一行 diff 比 100 行最终文件能更快速地报告问题。

14.4.4 Token 预算报警

def warn_token(step: Step, threshold: int = 200_000) -> None:
    if step.cum_total_tokens > threshold:
        logger.warning(
            f"Agent at step {step.id} hit {step.cum_total_tokens} tokens, "
            f"超过 {threshold=}. 考虑压缩 trace 或 prompt 创新 strageg."
        )

14.4.5 Stop_Reason Inspection

在 Anthropic Messages API里,看 stop_reason 让你快速分类:

for step in trace:
    if step.tool_name is None:
        continue
    if step.stop_reason == "max_tokens":
        print(f"WARN: step {step.id} 超出 max_tokens, 输出被截断")
    elif step.stop_reason == "tool_use":
        print(f"step {step.id}: 调用工具后继续循环")
    elif step.stop_reason == "end_turn":
        print(f"step {step.id}: 自报完成 (需要 DONE 校验)")

max_tokens 截断很隐蔽——模型看起来回答正常,但你没注意到它中间思考被砍了。

14.5 Prompt bug 调试:怎样让模型改

Agent trace step 7 thought: "我要 commit"
                   action: bash git commit -m "fix: lg"
                   obs:    [error: nothing staged]

根因: prompt system 说"完成后 commit",但模型 没意识到它前面没 stage 元件。修复 = 在 prompt 加一条 “commit 前先 git status 验证 staged diff 非空”,让模型意识到。

SYSTEM:
- 完成任务时,如果 final step 是 git commit / push,前 10 行内必须先 bash git status,确保 Diff 非空再 commit

不要把对付 LLM 的修正写到代码里 —— 它属于 prompt。Prompt bug 修一次,下次永远起效;代码 if/case 加多了会让 harness 变臃肿。

14.6 Loop bug 的三种修法

不要每次都改 prompt —— Loop bug 大多要改为 loop 逻辑。

现象 病灶 修法
同一 tool 反复调失败 失败被压(反) 失败 ≥2 次 raise + 让 LLM 总结
过早终"Done" 缺乏 validation 在 DONE 前加 Verification Loop
跑飞到 step 60 缺 budget max_tool_calls + max_tokens

14.7 工具即测试: 把 Tool 输出做回归

把 AI 改的 patch 当作既定值;让它出产出时陪你做 diff:

# 非-AI 测试保障修复不倒退
pytest tests/test_ai_workflow.py -v     # 你的 agent 内部测试

# 全 return 词典压 play obj 比较前/后
diff -u <(git show HEAD:.opencode/skills/SKILL.md) .opencode/skills/SKILL.md

工具是 ground truth,模型版本可以变。AI/Agent progression = keep good state + write good test.

14.8 一段 trace 阅读实战

下面是一段 真的 .trace 文件,模拟一个跑检验 / fix bug 的过程:

{"id":1,"ts":"14:01:02","tool":"bash","input":{"cmd":"rg \"def \" lib/validators.py | head"}}
{"id":2,"ts":"14:01:05","output":"def is_email\n  def is_phone\n  def is_cents\n  def is_url\n  def is_handle"}
{"id":3,"ts":"14:01:07","tool":"bash","input":{"cmd":"ls tests/"}}
{"id":4,"ts":"14:01:09","output":"test_emails.py\npytest.ini"}
{"id":5,"ts":"14:01:11","tool":"edit","input":{"path":"tests/test_validators.py","content":"def test_is_phone..."}}
{"id":6,"ts":"14:01:14","tool":"bash","input":{"cmd":"pytest tests/test_validators.py"}}
{"id":7,"ts":"14:01:25","output":"12 passed"}

看图诀窍

  • 看步骤对子: (1,2) / (3,4) / (5,「无输出→意味着 edit 没失败」) / (6,7) - tool/action 与 observation 配对缺失 = bug信号
  • 观察断点: step 5 之后没"thought"记录 → harness 没记 thinking → 应加
  • token 推算: edit 5 步骤没 input_tokens 记录 → 你的 Step struct 漏忠

读完 trace 5 分钟胜过修改 prompt 半小时 - 因为发现不是 prompt 的问题,是 loop 的问题,prompt 改了白改。

14.9 OpenTelemetry:把 Agent 接入公司已有观测

到一个团队规模,trace 要链接到既有可观测平台。把 LLM/Agent 数据做成 span:

from opentelemetry import trace

tracer = trace.get_tracer("ai.harness")

def traced_step(step: Step) -> Step:
    with tracer.start_as_current_span(f"step-{step.id}") as span:
        span.set_attribute("ai.tools.name",    step.tool_name)
        span.set_attribute("ai.tokens.input",  step.input_tokens)
        span.set_attribute("ai.tokens.output", step.output_tokens)
        span.set_attribute("ai.cum.total",     step.cum_total_tokens)
    return step

接到 Jaeger/Honeycomb/Datadog 之后,Agent 可以和 Web/app 性能同时观察 - 不可或缺在 prod debug。

14.10 小结

  • 4 类错误: Prompt / Tool / Loop / Context bug,先归类再修
  • 3 类工件: trace / tokens / state,必须留
  • 调试 5 招: 单步重放 / PAT / diff first / token 报警 / stop_reason 检查
  • 修法: Prompt bug 改 prompt,Loop bug 改 loop 逻辑,不混

下一篇: 《15 MCP 协议详解》 - 第二篇收口, 我们进 MCP, 把 Agent 跟"外部世界"连接起来。

Summary: 4 类错 / 3 类工件 / 5 招调试,让 Agent 从黑盒变白盒, debug 从猜测变有图。