SOP:工具执行埋点(tool-timing Hook)¶
主题目录:
tool-timing-20261007/|变更日期:2026-10-07|实现体:tool-timing-hook/tool-timing.py
1. 背景¶
收尾自查的五组指标(A 效率 / B 成本 / C 积累 / D 工具 / E 手段)全部是 agent 侧—— 我自己的步数、token、工具合并、子智能体。被操作一侧(工具执行、远端命令、等待)没有任何口径。
而唯一的时间数据源 logs/usage.jsonl 只落 ts + token:两次落盘的差值 =
上游生成 + 工具执行 + 网络往返,三者不可分(proxy.py 的 _record() 不含任何耗时字段)。
2026-10-07 全量拆解显示:>60 s 的慢步只占 10.6%(2022 步)却吃掉 63% 的活跃时间(105 h / 167 h),
但无法判断这些时间花在工具上还是花在生成上——这就是本次变更要解决的事。
2. 目标¶
变更完成后应达到的可验证状态:
logs/tool_timing.jsonl每完成一次工具调用即追加一行,dur_ms为该工具执行时长(毫秒);- 能按工具类型聚合出「工具执行侧」的时长与占比,从而把步间隔拆成「生成侧 / 工具侧」;
- 埋点绝不影响工具调用:任何异常静默
exit 0,不向 stdout 写任何内容; - 幂等:重复投递同一事件不产生第二行记录。
3. 前置条件¶
- Trae 客户端已支持
PreToolUse/PostToolUseHook(本项目已有 6 个 Hook 在跑,满足); - 事件字段含
hook_event_name与tool_use_id(2026-09-23 由posttooluse-guard/probe.jsonl实测确认); ~/.trae-cn/hooks.json可写;- 改
hooks.json后必须重启 Trae 才生效(与 miss 无关,是客户端加载时机)。
4. 影响范围¶
| 项 | 说明 |
|---|---|
| 涉及主机 | 仅本机(Trae 客户端侧),不涉及任何远端主机、不涉及 token_proxy 服务 |
| 是否停服 | 否。重启 Trae 会中断当前会话的实时对话,但代理 token-proxy 服务不受影响 |
| 窗口期 | 无要求;建议在无在途任务时重启 |
| 新增开销 | 每次工具调用多 2 次 python3 启动,实测一对约 125 ms(不影响 p50 11 s 的步间隔) |
| 新增落盘 | logs/tool_timing.jsonl(append-only)+ tool-timing-hook/state/(1 h 自动回收) |
| 上下文影响 | 零——两个事件均不输出 additionalContext,不向模型注入任何文本 |
5. 分步操作¶
步骤 1:部署 Hook 脚本¶
脚本已由本次变更创建于 tool-timing-hook/tool-timing.py。它由 hooks.json 直接以绝对路径调用,
不需要复制到别处,也不需要可执行位(命令行用 python3 <脚本> 调用)。
ls -l ~/Documents/services/token_proxy/tool-timing-hook/tool-timing.py # 预期:文件存在
python3 -c "import ast,sys; ast.parse(open('$HOME/Documents/services/token_proxy/tool-timing-hook/tool-timing.py').read())" # 预期:无输出(语法通过)
坑:脚本必须放在 tool-timing-hook/ 目录内——状态目录与日志路径均由脚本自身位置推导
(<脚本目录>/state、<脚本目录>/../logs/tool_timing.jsonl)。挪位置会同时挪走 state,日志落在新位置。
步骤 2:挂载 hooks.json(幂等)¶
在 PreToolUse 与 PostToolUse 各追加一条 matcher: "*" 的条目。下面的脚本可重复执行:
已挂载时报「零变更、全跳过」,未挂载时先备份再插入。
python3 - <<'PY'
import json, os, shutil, time
P = os.path.expanduser("~/.trae-cn/hooks.json")
CMD = ('s="$HOME/Documents/services/token_proxy/tool-timing-hook/tool-timing.py"; '
'[ -f "$s" ] && python3 "$s" || true; exit 0')
ENTRY = {"matcher": "*", "hooks": [{"type": "command", "command": CMD, "timeout": 10}]}
d = json.load(open(P, encoding="utf-8"))
changed = []
for ev in ("PreToolUse", "PostToolUse"):
items = d["hooks"].setdefault(ev, [])
if any(CMD == h.get("command") for it in items for h in it.get("hooks", [])):
continue
items.append(json.loads(json.dumps(ENTRY)))
changed.append(ev)
if changed:
ts = time.strftime("%Y%m%d-%H%M%S")
shutil.copy2(P, P + ".bak." + ts)
json.dump(d, open(P, "w", encoding="utf-8"), ensure_ascii=False, indent=1)
print("已挂载:", changed)
else:
print("幂等检查:两条已挂载,本次零变更、全跳过")
PY
预期输出(已挂载时):幂等检查:两条已挂载,本次零变更、全跳过。
坑:命令行结尾必须是 || true; exit 0——Hook 的退出码会被 Trae 当作协议,
非 0 会让整个工具调用失败(本项目其它 Hook 同理)。
步骤 3:确认生效(无需重启)¶
实测(2026-10-07):hooks.json 改动即时生效,不必重启 Trae。
新增两条挂载后,下一次工具调用即开始落盘:hooks.json mtime 07:17:28 → 首条埋点 07:22:06,
期间未重启 Trae;65 条样本 miss_pre 全为 false、id_src 全为 tool_use_id。
原「须重启 Trae 才生效」的说法已证伪(旧结论基于"配置只在启动时读取"的推断,非实测)。
python3 -c "import json,os;d=json.load(open(os.path.expanduser('~/.trae-cn/hooks.json')));print([ 'tool-timing' in h['command'] for ev in ('PreToolUse','PostToolUse') for it in d['hooks'][ev] for h in it['hooks']].count(True), '条 tool-timing 挂载')" # 预期:2 条
失败判断:挂载数为 2 但 logs/tool_timing.jsonl 长时间无新增 → 重启 Trae 兜底,再按第 8 章排查。
(LS matcher 对应的判据 G 是否同样即时生效未验证——guard_events 中尚无 G 记录。)
步骤 4:首次数据核对¶
重启后随便触发一次工具调用(如读一个小文件),再查看日志:
tail -n 5 ~/Documents/services/token_proxy/logs/tool_timing.jsonl
预期:出现形如
{"ts": "…", "dur_ms": 123.4, "tool": "Read", "llm_tool": "Read", "session": "…", "agent": "", "id_src": "tool_use_id", "miss_pre": false} 的行。
失败判断:重启后仍无新增行 → 核对 hooks.json 是否 JSON 合法、脚本路径是否存在;
重启后 miss_pre 恒为 true → Post 挂载了、Pre 未挂载(回到步骤 2 复查)。
6. 验证¶
| 层级 | 命令 | 预期 |
|---|---|---|
| 离线自测(18 项) | python3 tool-timing-20261007/verify_tool_timing.py |
全部通过(0 项失败);连跑两次同样通过(幂等) |
| 结构校验 | 见步骤 3 的命令 | 2 条 tool-timing 挂载 |
| 真机 | tail -n 5 logs/tool_timing.jsonl |
有新增行,miss_pre: false |
| 聚合可用 | RUNBOOK 第 3 章「按工具聚合」命令 | 输出各工具的中位 / p90 / 合计占比 |
自测覆盖:Pre 静默、state 写入与回收、Post 落盘、dur_ms 合理、配对键来源、重复投递幂等、
孤立 Post(miss_pre=true)、非法输入({} 之外的 5 种)静默、并行同名工具各记一行。
7. 回滚¶
备份位置:tool-timing-20261007/backup/home/rain/.trae-cn/hooks.json.bak.20261007-071701,
台账见 backup/README.md。
回滚步骤(二选一):
# 方式 A:整体还原(会一并回滚本次之前的其它 hooks.json 改动,慎用)
cp -p ~/Documents/services/token_proxy/tool-timing-20261007/backup/home/rain/.trae-cn/hooks.json.bak.20261007-071701 ~/.trae-cn/hooks.json
# 方式 B:只摘除本次新增的两条(推荐)
python3 - <<'PY'
import json, os
P = os.path.expanduser("~/.trae-cn/hooks.json")
d = json.load(open(P, encoding="utf-8"))
for ev in ("PreToolUse", "PostToolUse"):
d["hooks"][ev] = [it for it in d["hooks"][ev]
if not any("tool-timing.py" in h.get("command", "") for h in it.get("hooks", []))]
json.dump(d, open(P, "w", encoding="utf-8"), ensure_ascii=False, indent=1)
print("已摘除 tool-timing 挂载")
PY
回滚后必须重启 Trae 才生效。回滚后验证:hooks.json 内不再含 tool-timing.py,且工具调用不再新增日志行。
logs/tool_timing.jsonl 与 tool-timing-hook/state/ 是数据不是配置,回滚时不删——
需要保留已采集的样本供分析。删除由用户对照 backup/README.md 自行决定。
8. 风险说明¶
| 风险 | 缓解 |
|---|---|
| Hook 阻塞工具调用 | 脚本仅读写本地小文件,实测一对 ~125 ms;hooks.json 设 timeout: 10 秒 |
| 脚本异常导致工具失败 | 顶层 except Exception 静默 + sys.exit(0);命令行再套 \|\| true; exit 0 |
| PreToolUse 的 stdout 干扰协议 | 脚本不输出任何内容(自测含「stdout 为空」断言) |
state/ 无界增长 |
保留期 1 h(STALE_SECONDS),Pre/Post 两端触发回收,删除写入 state/prune.log 留痕 |
| 埋点文本进上下文抬高成本 | 两个事件均不注入 additionalContext,零上下文成本 |
tool_use_id 缺失 |
退化为 <session>_<tool> 键(记 id_src: fallback),并行同名调用可能错配,仅兜底 |
| 误判「工具变慢」 | 本埋点量的是工具自身执行时长,不含模型生成;与步间隔含义不同,分析时勿混用 |
附录:交付物清单¶
| 文件 | 说明 |
|---|---|
tool-timing-hook/tool-timing.py |
实现体(Pre/Post 双事件共用) |
tool-timing-20261007/verify_tool_timing.py |
离线自测(18 项,可重复运行) |
tool-timing-20261007/SOP.md |
本文档 |
tool-timing-20261007/RUNBOOK.md |
运行与使用手册 |
tool-timing-20261007/backup/ |
hooks.json 备份与台账 |
logs/tool_timing.jsonl |
产出数据(append-only,勿删) |
来源:
/home/rain/Documents/services/token_proxy/tool-timing-20261007/SOP.md| 最后更新:2026-10-07 07:29(由build/sync.py同步,请勿在站点侧修改)