4 minutes
Agent 调试与可观测性
传统软件的 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 从猜测变有图。