SOP:给 logs/req_shape.jsonl 补记工具名(总账 #21)¶
主题目录:
shape-toolname-20261002/。交付物:本 SOP、verify_shape_tools.py、backup/。 变更对象:proxy.py(采集端)、cost_audit/step_profile.py(消费端)。
1. 背景¶
step_profile.py 要回答「一个会话里哪几步可以合并成一次取数」,但 logs/req_shape.jsonl
每步只记 角色:字节数:哈希,没有工具维度,无法区分 Read / Glob / Grep / 命令 / MCP。
上个主题实测其「疑似可合并组」判据对字节阈值跨 26 倍敏感(2.2%~57.6%),不可用;
根因收敛到「缺工具名」这一点,挂账为总账 #21(需改 proxy.py,属红线级变更)。
2. 目标¶
变更后可达的可验证状态:
req_shape.jsonl每步的shape新增tool_names:本步末尾最近一次 assistant 工具调用的 名字列表(如["Read"]、["Glob","Grep"]);无工具调用为[]。- 只记名字,不记参数与文件路径,仍满足「不落任何对话内容」的既有边界。
- 现网请求转发链路不受影响(纯旁路记录);
analyze_prefix_fail.py(只读head/tools_hash)行为不变。
3. 前置条件¶
- 工程根
/home/rain/Documents/services/token_proxy,内含venv/。 - 生产代理以 systemd 用户服务
token-proxy运行(端口 8788);run.sh与 systemd 互斥。 - 离线验证用「假上游 + 临时端口」,不触碰 8788、不写生产
logs/。
4. 影响范围¶
| 项 | 说明 |
|---|---|
| 主机 | 仅 192.168.31.250(rain),单机 |
| 需停服 | 是:systemctl --user restart token-proxy 会打断在途流式回答(数秒) |
| 窗口期 | 顺序执行、无外部依赖,数分钟内完成 |
| 数据 | logs/req_shape.jsonl 追写新字段,旧记录保留原样(向后兼容);usage.jsonl 不受影响 |
| 上游 | 请求体不被修改(占位在旁路 build_shape_fingerprint),不影响 prompt 缓存 |
5. 分步操作¶
步骤 1:备份(高危前置)¶
cd /home/rain/Documents/services/token_proxy
mkdir -p shape-toolname-20261002/backup
ts=$(date +%Y%m%d-%H%M%S)
cp -p proxy.py "shape-toolname-20261002/backup/proxy.py.bak.$ts"
cp -p cost_audit/step_profile.py "shape-toolname-20261002/backup/step_profile.py.bak.$ts"
chmod 600 shape-toolname-20261002/backup/*.bak.*
预期:backup/ 下出现两个 600 权限的 .bak.<ts> 文件,md5sum 与原件一致。
台账见 backup/README.md。
坑:备份须与被改文件分离放置(不放工程根),否则易与「可删的工作备份」混淆。
步骤 2:采集端加字段(proxy.py)¶
在 build_shape_fingerprint 里:新增 shape["tool_names"],从末尾向前取最近一条含
tool_calls 的消息,提取 tool_calls[].function.name(新函数 _tool_names)。
只取名,不取 arguments。
坑:只加新字段、不改 head/tail 既有格式——analyze_prefix_fail.py 依赖 head 做整体比较,
改格式会波及它;step_profile.parse_fp 严格按 3 段解析 role:len:hash。
# 实现体:proxy.py(函数 _tool_names / build_shape_fingerprint),实际执行以实现体为准
./venv/bin/python -m py_compile proxy.py # 预期无输出,退出码 0
步骤 3:离线验证(红线:不可只看代码推断)¶
./venv/bin/python shape-toolname-20261002/verify_shape_tools.py
该脚本做两段:A. 纯函数断言(无工具 / 单工具 / 多工具 / 仅 tool 结果 / 从尾部取最近一次 /
畸形输入不抛异常 / 指纹不含参数与文件路径);B. 端到端(临时目录 + 假上游 + 临时端口拉起改后
proxy.py,发 4 条请求,断言 req_shape.jsonl 的 tool_names 依次为 [],[Read],[Glob,Grep],[]、
usage.jsonl 记账正常、假上游收到 4 次、运行日志无异常栈)。
预期:全部 [通过],退出码 0;末尾打印临时目录供取证。
失败判断:任一 [失败] → 回到步骤 7 回滚,修正后重跑。
步骤 4:消费端消费字段(cost_audit/step_profile.py,只读工具)¶
- 新增
tools_of()读取shape.tool_names;明细表新增「工具」列。 - 「疑似可合并组」判据改为后处理
merge_groups(),并新增一档只读组(组内每步工具全属READ_TOOLS = {Read, Glob, Grep, LS, SearchCodebase, WebFetch, WebSearch})——只读组是更可信的 可合并候选;旧记录(无tool_names)只读组自然为 0,不影响其余指标。
./venv/bin/python -m py_compile cost_audit/step_profile.py # 预期退出码 0
./venv/bin/python cost_audit/step_profile.py --top 5 # 预期正常出表(此时仍全为旧记录)
步骤 5:重启生产代理使采集端生效(高危)¶
systemctl --user restart token-proxy
sleep 3
systemctl --user is-active token-proxy # 预期 active
curl -s -m 3 http://127.0.0.1:8788/healthz # 预期 {"status": "ok", ...}
坑:不要用 ./run.sh start(会与 systemd 抢 8788);重启会打断在途流式回答,需确认无关键在途任务。
6. 验证¶
cd /home/rain/Documents/services/token_proxy
./venv/bin/python - <<'EOF'
import json
rows=[json.loads(l) for l in open('logs/req_shape.jsonl',encoding='utf-8') if l.strip()]
for r in rows[-3:]:
s=r.get('shape') or {}
print(r.get('ts'), r.get('route'), "tool_names=", s.get('tool_names'), "keys=", sorted(s.keys()))
EOF
预期:重启之后的记录 keys 含 tool_names,值为本步工具名列表(如 ["RunCommand"]);
重启之前的记录不含该字段(向后兼容)。若新记录仍无该字段 → 检查是否加载了旧 proxy.py(见第 7 章)。
补充:跑 ./venv/bin/python cost_audit/step_profile.py --conv <会话前缀> --all,明细「工具」列应显示工具名。
7. 回滚¶
触发判据(任一即回滚):/healthz 非 200 / 代理进程未 active / 新记录出现异常字段或运行日志有 Traceback。
cd /home/rain/Documents/services/token_proxy
cp -p shape-toolname-20261002/backup/proxy.py.bak.20261002-162823 proxy.py
cp -p shape-toolname-20261002/backup/step_profile.py.bak.20261002-163616 cost_audit/step_profile.py
systemctl --user restart token-proxy
curl -s -m 3 http://127.0.0.1:8788/healthz # 预期 {"status": "ok", ...}
回滚后:采集端不再输出 tool_names;消费端 tools_of 对无字段记录返回 [],不报错。备份存放位置与
用途见 shape-toolname-20261002/backup/README.md。
8. 风险说明¶
| 风险 | 缓解 |
|---|---|
| 重启打断在途流式回答 | 选择无关键任务时执行;severe 时秒级恢复 |
| 记录含工具参数泄露文件路径 | 实现体只取 function.name;验证 A7/B5 显式断言不含参数字符串 |
改 head/tail 格式波及 analyze_prefix_fail.py |
不改既有格式,只新增顶层字段 |
| 旧消费脚本读新记录报错 | 新增字段可缺省;tools_of 对缺失/畸形返回 [] |
误用 run.sh 抢端口 |
只用 systemctl --user;SOP 与红线均标注互斥 |
附录 A:为什么不另出 RUNBOOK¶
本次未产生新可复用交付物,也未改变运行方式:代理仍由 systemctl --user 管理(见
docs/BASELINE.md 第 1 节),step_profile.py 的用法与上一主题一致(见
step-profile-20261002/SOP.md)。因此不新增 RUNBOOK;日常运行与排障信息以 docs/BASELINE.md
与 docs/SOP.md 为单一源。
附录 B:交付物清单¶
| 文件 | 说明 |
|---|---|
proxy.py(改) |
build_shape_fingerprint 新增 tool_names;新增 _tool_names() |
cost_audit/step_profile.py(改) |
新增 tools_of() / merge_groups();明细加工具列;新增只读组统计 |
shape-toolname-20261002/verify_shape_tools.py |
离线验证(A 纯函数 7 项 + B 端到端 8 项) |
shape-toolname-20261002/backup/ |
proxy.py.bak.*、step_profile.py.bak.*、README.md |
shape-toolname-20261002/SOP.md |
本文件 |
来源:
/home/rain/Documents/services/token_proxy/shape-toolname-20261002/SOP.md| 最后更新:2026-10-02 16:38(由build/sync.py同步,请勿在站点侧修改)