16. 评测、调试与最终项目
Agent 开发最大的错觉是“刚才那次看起来能跑,所以它是对的”。模型输出不稳定,真实仓库复杂,工具和上下文状态很多。如果没有评测和调试体系,你会在每次改 prompt 或工具描述后重新手工试一遍,而且无法判断退化来自哪里。
三层测试
教学项目至少需要三层测试:
- 单元测试:工具参数校验、路径解析、输出截断、写队列、换行和 BOM 处理。
- Loop 测试:faux provider 驱动 tool use、错误回传、steering、压缩、abort。
- 回放测试:读取一段会话日志,重建上下文,验证投影结果。
真实模型端到端测试只做 smoke。它可以告诉你系统能连通,但不适合作为主要回归测试。主要回归必须确定、便宜、可重复。这也是为什么前面所有章节都把 faux provider 和事件流当一等公民:可测试性不是最后补的,而是从协议层就设计进去的。机器可读模式(print / JSON)本身就是天然的评测入口——喂一段输入,断言输出的事件序列和退出码。
Harness 竞态测试
普通 Loop 测试会验证“输入 A 得到事件 B”,但 Harness Engineering 最容易坏的是时间关系。测试要用 deferred promise 主动把 provider、工具或 listener 卡在精确边界,再从另一条异步链触发变更。至少覆盖这些 invariant:
- 当前 provider request 使用固定 turn snapshot;运行中切换 model、tools、resources 或 system prompt 只影响下一安全点后的请求。
- 第二个结构性操作在 busy 时稳定拒绝;steering、follow-up 与 next-turn 进入各自队列,不能共享一个含糊的 pending input 数组。
- Agent 消息先持久化,
message_endlistener 产生的 session entry 后持久化;Promise 的完成顺序不能改变日志顺序。 waitForIdle()等待最后一个异步 listener 与 pending write,不在 provider stream 结束时提前返回。- abort 最终恢复
idle,并按契约清理或保留各个队列;失败清理也不能吞掉尚未写入的事实。
这类测试不要使用固定 sleep(100)。测试应持有显式 barrier:先断言 harness 尚未 idle,再释放 listener,最后等待完成 promise。这样失败说明契约真的被破坏,而不是 CI 机器刚好比较慢。
会话回放
会话日志天然是调试材料。一次失败任务可以导出为 JSONL,测试 harness 读取后重建 active branch,断言:
- 上下文里是否包含该有的系统规则。
- 压缩摘要是否保留关键事实。
- 最后一个 tool result 是否正确进入下一轮。
- 模型切换是否影响后续请求。
- 分支 leaf 是否指向预期 entry。
这比截屏或人工描述 bug 更可靠。回放测试还有一个额外价值:它是格式迁移的安全网。第八章说过日志格式会演化,把真实的旧格式会话文件留作测试样本,每次迁移后回放一遍,就能保证老用户的历史不会在升级中损坏。
成本与性能
Agent 应该统计每次模型请求的 usage、耗时、工具耗时和重试次数。没有这些数据,你无法回答“为什么这个任务花了这么久”或“哪个模型最贵”。成本信息可以作为事件和 session entry 记录,不必每次都进入模型上下文。
常见指标:
- input tokens。
- output tokens。
- cache read/write tokens。
- reasoning tokens。
- provider latency。
- tool latency。
- retry count。
- compaction count。
- estimated cost。
缓存和推理 token 值得单列。缓存读通常远比普通输入便宜,一个 system prompt 前缀稳定、缓存命中率高的 Agent,成本可能是命中率低的同类系统的几分之一——这直接呼应第十一章“稳定内容放前面”的组装顺序。推理 token 是输出的一部分但单独计价,不统计就会低估成本。
把成本落成一个具体公式会更清楚。每次 assistant 响应带回一组 usage,乘以模型目录里的单价即可:
turn_cost =
input_tokens * price_input
+ output_tokens * price_output
+ cache_read_tokens * price_cache_read // 约为 price_input 的 0.1 倍
+ cache_write_tokens * price_cache_write
+ reasoning_tokens * price_output // 推理 token 按输出计价
举个例子:一次 turn 命中缓存 40k token、新输入 2k、输出 500、推理 800。如果把那 40k 缓存按普通输入价计算,成本几乎翻倍;按 0.1 倍计算,缓存部分只相当于 4k 普通输入。这就是为什么“稳定前缀加高缓存命中率”能把长会话的账单压到几分之一。也正因如此,usage 必须分五类字段记录,而不是只存一个总数——总数一旦记下,就再也拆不出缓存和推理各占多少了。
最终项目:tiny-agent
最终项目不是复刻某个现成产品,而是证明你理解 Agent 工程边界。选择一个小型 TypeScript 仓库,让 tiny-agent 完成一次真实修改:
- 读取用户目标。
- 搜索相关文件。
- 读取目标文件。
- 精确编辑实现。
- 运行指定检查命令。
- 根据失败继续修正。
- 生成最终摘要。
- 写入完整会话日志。
- 退出后恢复会话,说明刚才做了什么。
验收时不要只看最终代码是否正确,还要看过程是否可审计。
Capstone 验收清单
你的 tiny-agent 需要满足:
- Provider 可替换,至少有真实 provider 和 faux provider,错误能分类为可重试与不可重试。
- Tool call 由消息内容驱动,不靠硬编码步骤,也不因 stop reason 丢掉工具。
- 工具参数校验失败会回喂模型,多余字段被规整而非硬拒。
- read/edit/write/bash 都有路径边界和截断策略,edit 有规范化匹配和换行保留。
- 写操作有 diff details 和模型摘要分离,同文件写操作按真实路径排队。
- 会话日志是追加式 JSONL,带 parentId 树结构和版本号。
- 恢复后不重新执行历史工具。
- 压缩不删除历史,只改变上下文投影,且能在上下文溢出时自动重试。
- steering 和 follow-up 有独立队列和明确的消费时机。
- harness 为每次 provider request 建立稳定 turn snapshot,运行中配置变更只在下一安全点生效。
- 结构性操作在 busy 时拒绝,pending session writes 按 Agent 消息之后的确定顺序落盘,
waitForIdle()不提前返回。 - JSON 模式 stdout 只输出机器可读事件,退出码有约定。
- 权限门和项目信任在所有运行壳中一致生效。
- faux provider 测试覆盖至少一个工具错误自我修正场景。
调试手册
当 Agent 行为异常时,按这个顺序查:
- 看 session log,确认事实是否写入。
- 看 harness timeline,确认 phase、turn snapshot、save point、pending writes 和 settled 的顺序。
- 看 context projection,确认发给模型的内容是否正确(第十一章的组装调试视图在这里最有用)。
- 看 provider adapter,确认 stop reason 和 tool call 是否转换正确,跨模型时 id 和 thinking 是否处理妥当。
- 看 tool result,确认错误是否可修正、是否可行动。
- 看 system prompt,确认规则顺序和冲突。
- 最后再怀疑模型本身。
这条顺序很重要。很多“模型不听话”的问题,其实是上下文投影漏了约束,或者工具错误写得不可行动。把责任先落在自己能控制的确定性层,再落到不确定的模型,调试效率会高得多。
结束语
构建 Coding Agent 的核心不是找到一段神奇 prompt,而是把一个不确定的模型放进确定的工程边界里:协议清晰,工具受控,状态可恢复,事件可观察,权限可审计,扩展有边界,并由 harness 把所有变化限制在可解释的时间点。做到这些之后,模型能力提升会自然变成系统能力提升;否则模型越强,系统越难控制。这也是这套教程从第一章到最后一章反复回到同一张分层图的原因——每一层解决一个确定性问题,合起来就是一个能真正替你改代码、而且你敢让它改的系统。