跳转至

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 同步,请勿在站点侧修改)