从零搭建 Agent Harness 系列(二十五)剧本回放评测与 Trace 尺子
系列二十四把 MCP 和自家信封的 A2A 接进 Registry。之后又补了官方 SendMessage / Agent Card,以及把 spawn_subagent 挂上 Factory。阶段十二要回答另一件事:固定任务上行为有没有回退,一次 Run 慢在哪。
一到十一的 go test 测的是函数。评测测的是整条 ReAct:终态对不对、调了哪些工具、取消后是否成对。性能先有尺子,再谈优化。这一刀两件事一起做:剧本回放,以及在已有 Trace 上记三个数。
本文对应 go-tiny-claw 提交:
1 | 8fd935d feat: 用剧本回放评测 Agent,并在 Trace 上记下延迟尺子 |
官方 A2A 与 spawn_subagent 在 ee4048b,不在本篇展开。
一、评测不是再写一遍 engine 测试
test/engine 里已经有预算、取消、断档。每份测试自己捏一个假 Provider,证明某一个分支。评测要的是同一套入口、多条固定任务:给一句 prompt,跑完 Engine.Run,对历史和工作区打分。
1 | 任务是否完成 |
真模型贵、飘、难进 CI。第一刀只用剧本:按轮吐固定的 assistant / ToolCall,工作区是 t.TempDir()。
二、eval 在根上,只调用 internal
评测不是 Engine 的一部分,循环不该 import eval。它和 cmd/claw 一样,是消费方:
1 | eval → internal/engine |
放根目录 eval/,一个包,先不分 cases/。go test ./eval 就能跑。以后要 cmd/claw_eval,直接 import 这个包,不必从 test/ 里往外搬。
1 | eval/script.go 剧本 Provider,可带 Delay |
Run 自己 StartSpan("Eval"),Engine 再挂上 Agent.Run。评测结束时父 span 还在,尺子从子 span 读,不用改 Run 的返回值。
默认 Registry 只有 read_file / write_file / edit_file,不接 MCP、不拉 Factory。审批默认 AllowOnce;要测拒绝就传入 DenyHandler。
三、六条任务证明什么
| 任务 | 过线条件 |
|---|---|
| 只读作答 | 终态含文件原文,只调了 read_file |
| 唯一编辑 | edit_file 一次,磁盘内容变对 |
| 危险写入被拒 | write_file 被 Deny,文件不存在,Observation 写「拒绝」 |
| 取消半截工具 | 工具堵住后 cancel,该 ToolCallID 仍有 Observation |
| Token 预算停 | MaxTokensPerRun 触顶,Generate 不再打第二次 |
| TTFB + 墙钟 | 剧本延迟 40ms、工具睡 40ms,尺子都 ≥ 30 |
预算那条复用阶段九的闸:第一次调用用满额度,第二次 checkRunBudget 直接错。评测断言的是「停了,而且假模型只被叫过一次」,不是再实现一遍预算。
四、尺子写在已有 Trace 上,不另打日志
机制早就有:首 Token 超时 15 秒、Session 累计 Token、工具墙钟。缺的是数字。超时逻辑不动,只在 generate 里记下第一次吐字(或非流式 Generate 成功返回)距开始的毫秒,属性名 ttfb_ms。同一 span 只记一次。
endRunBudget 会把 toolElapsed 清零。所以根 span 的 defer 必须先快照再清:
1 | prompt_tokens / completion_tokens |
eval.MetricsFrom 找到 Agent.Run,读这四个数。CI 只跑假 Provider。真模型的「首 Token 800ms」是夜间任务,不要进 go test。
五、阶段十二还没做完的
1 | 真模型评测(同一套断言,分数允许浮动) |
TotalCostCNY 仍是 0。MCP 工具热刷新也还没做。评测集以后加任务,往 cases_test.go 里加函数即可;框架不必跟着涨。
总结
二十四解决「手和同事从哪来」。二十五解决「怎么知道没回退,以及慢在哪」:
1 | eval/ 单向调用 internal,不进 Engine |
有回放才能改循环而不瞎;有尺子才知道慢的是模型、工具还是写 Trace。部署治理另开。