跳转至

SOP:收紧 wrapup.py 的「tools 顺序抖动」判据

变更日期:2026-10-07  执行人:Agent(用户授权)  对象:cost_audit/wrapup.py

1. 背景

收尾自查里有一行「tools 顺序抖动(TOOLS_NORM_SINCE 之后):N 步」,用于验证 proxy 的 normalize_tools_order()(转发前按 function.name 排序)是否消除了 Trae 重排 tools 数组导致的冷启动。

它长期报「1 步(08:41:40)」。2026-10-07 取证(req_shape.jsonl,05:55:24 之后 569 步)后确认这是误报:

项 实测
distinct 原始 tool_defs 序列 2
distinct 排序后集合 2
差异 仅少 AskUserQuestion,无其它增删
n=27(含该工具) 567 步
n=26(缺该工具) 2 步(08:41:40、08:41:53,随后恢复 27)

判据是:若真有顺序抖动,「原始序列种类」应多于「集合种类」;实测两者相等(2 = 2)→ 该窗口内一次顺序抖动都没有。

那一步(08:40:49 → 08:41:40,同 conv dda5be9c,间隔 51 s)的 miss 从 2803 跳到 54975 是真的,但成因是客户端把 AskUserQuestion 从工具列表里摘掉(27 → 26),不是顺序变化。而排序补不回缺失的工具,proxy 侧对此无解。

根因:tools_order_since() 只比 tools_hash 是否变化(tools_hash 由 json.dumps(tools, sort_keys=True) 算出,sort_keys 只排 dict 的键、不影响数组顺序,故对顺序与增删都敏感),把「工具增删」一并计成了「顺序抖动」。

2. 目标

tools_hash 变化时,还须工具名集合相同才计入;工具增删不计。

可验证状态:离线 4 项用例全通过;真机 wrapup.py 输出中该行消失(n 由 1 → 0)。

3. 前置条件

  • 对 cost_audit/wrapup.py 有写权限;
  • python3 可用;
  • logs/req_shape.jsonl 中目标区的记录含 shape.tool_defs(2026-10-02 起有)。

4. 影响范围

  • 主机:单机(本机 token_proxy 工程),一个文件、三处。
  • 组件:wrapup.py 的 tools_order_since() 与其 docstring。wrapup.py 是离线只读取数脚本,不在代理请求路径上。
  • 是否停服:否;随改随生效。
  • 窗口期:无要求。在跑作业:无影响。

5. 分步操作

步骤 1:备份(高危前置)

cd /home/rain/Documents/services/token_proxy
TS=$(date +%Y%m%d-%H%M%S)
mkdir -p wrapup-tools-jitter-20261007/backup/cost_audit
cp -p cost_audit/wrapup.py \
      wrapup-tools-jitter-20261007/backup/cost_audit/wrapup.py.bak.$TS

预期:backup/cost_audit/wrapup.py.bak.<TS>(17631 B)。 失败判断:目录不可写或 cp 非 0 → 停止。

步骤 2:改三处(必须成套)

  1. docstring:在既有判据后补「2026-10-07 补:tools_hash 变化时还须工具名集合相同才计入」并写明取证依据;
  2. 存储结构:hash_by_key[键] = tools_hash → shape_by_key[键] = (tools_hash, frozenset(tool_defs))(tool_defs 非列表时记为 None);
  3. 判据:a and b and a != b → 追加 na is not None and na == nb。
python3 -c "import ast; ast.parse(open('cost_audit/wrapup.py', encoding='utf-8').read()); print('语法通过')"
grep -n 'shape_by_key\|na == nb' cost_audit/wrapup.py   # 预期:存储 2 处 + 判据 1 处

坑:三处必须成套。只改判据不改存储,hash_by_key 里取不到第二个元素 → 解包报错;只改存储不改判据,na == nb 无人使用、等于没修。

坑:frozenset 只保留名字、丢掉顺序,这正是判据所需;不要用 sorted(defs) 的字符串比较,那会重新引入顺序敏感。

步骤 3:(无)不涉及其它文件。

6. 验证

层级 命令 预期
语法 python3 -c "import ast; ast.parse(open('cost_audit/wrapup.py').read())" 无异常
用例(4 项) python3 wrapup-tools-jitter-20261007/verify_tools_jitter.py 全部通过,退出码 0
C1 集合相同、顺序不同 同上 计入(1 步)—— 真顺序抖动仍能被抓到
C2 集合不同(少一项) 同上 不计入(0 步)—— 本次修复对象
C3 hash 相同 / C4 tool_defs 缺失 同上 均不计入
真机 python3 cost_audit/wrapup.py 「tools 顺序抖动」行消失(n 0;修复前为 1)

实测(2026-10-07):4 项用例全通过;真机该行已消失。

7. 回滚

备份位置:wrapup-tools-jitter-20261007/backup/cost_audit/wrapup.py.bak.20261007-084937(登记见 backup/README.md)。

cd /home/rain/Documents/services/token_proxy
cp -p wrapup-tools-jitter-20261007/backup/cost_audit/wrapup.py.bak.20261007-084937 \
      cost_audit/wrapup.py
python3 cost_audit/wrapup.py        # 回滚后预期:「tools 顺序抖动:1 步」重新出现

回滚后验证:该行重新报 1 步即确认回到旧判据。回滚触发量化:若用例 C1 不再计入(真顺序抖动被漏掉)→ 回滚。

8. 风险说明

风险点 缓解措施
漏报:2026-10-02 前的旧记录无 tool_defs → 一律不计 有意为之(宁漏不误报);该区的记录自 10-02 起才有 tool_defs
语义变化导致与历史数字不可比 该统计的语义已由「hash 变化」收窄为「纯顺序抖动」,跨 2026-10-07 的数字不可直接比较(已在 docstring 写明)
排序补不回工具增删,导致「工具集变化」这类冷启动无统计口径 本次不拓展(用户范围仅限收紧判据);若日后需要,应另立一项「工具集变化冷启动」,与本项分开报
误改 sort_keys 语义 tools_hash 的计算属 proxy 侧,本次未动

9. 同日再收紧:加「有序序列」判据、「定义内容变动」单列(2026-10-07 22:38)

9.1 背景

第 2 节收窄后,该行仍报「1 步(20:30:43)」。取证(会话 3367ddb1)发现:

项 实测
20:28:09~20:30:20(十步) tools_hash 恒为 8b3e6ab274
20:30:43 切 d22e5b891a,hit=12800 / miss=84309(整段冷启动)
20:30:44 回 8b3e6ab274(msgs 34→33,似同轮重试),miss 2790
20:31:01 起 稳定 d22e5b891a,命中恢复正常
上述三步 shape.tool_defs 27 个工具,名字与顺序完全相同

名字与顺序都没变、hash 却变 → 差异在工具定义内容(描述 / schema 等,req_shape 不落内容)。 根因:第 2 节的集合判据(frozenset)主动丢弃顺序,无法区分「顺序变了」与「顺序没变、内容变了」。

9.2 目标

tools_hash 变化时三层分流;可验证状态:离线 5 用例全通过,真机「顺序抖动」行消失(1 → 0)、 新增「定义内容变动:1 步(20:30:43)」。

条件 归类
集合不同 工具增删 → 不计(第 2 节已定)
集合相同、序列不同 顺序抖动(措施覆盖,计入原行)
集合相同、序列也相同 定义内容变动(措施不覆盖,单列新行)

9.3 影响范围

同第 4 节:单机、单文件;wrapup.py 为离线只读取数脚本,不在代理请求路径上;不停服、无窗口要求。

9.4 分步操作

  1. 备份(高危前置):cp -p cost_audit/wrapup.py wrapup-tools-jitter-20261007/backup/cost_audit/wrapup.py.bak.<TS>;
  2. 四处成套改:
cd /home/rain/Documents/services/token_proxy
grep -n 'names = (\[d for d in defs' cost_audit/wrapup.py   # 存储:frozenset → 有序 list
grep -n 'set(na) == set(nb)' cost_audit/wrapup.py           # 判据:改为集合比较
grep -n 'if na != nb' cost_audit/wrapup.py                  # 分流:序异→抖动 / 序同→内容变动
grep -n 'csamples' cost_audit/wrapup.py                     # 返回 4 元组 + main() 单列输出

坑:存储必须改回有序 list——第 2 节用 frozenset 正是为了丢顺序,本次要判顺序,仍用 frozenset 则 C5 与 C1 不可分。 坑:返回由 2 元组扩为 4 元组,main() 与离线用例 verify_tools_jitter.py 必须同步,否则解包报错。

  1. 同步给 wrapup-tools-jitter-20261007/verify_tools_jitter.py 新增用例 C5。

9.5 验证

层级 命令 预期
语法 python3 -c "import ast; ast.parse(open('cost_audit/wrapup.py', encoding='utf-8').read())" 无异常
用例(5 项) python3 wrapup-tools-jitter-20261007/verify_tools_jitter.py 全通过、退出码 0
真机 python3 cost_audit/wrapup.py 「顺序抖动」行消失;出现「定义内容变动:1 步(20:30:43)」

实测(2026-10-07 22:38):语法通过;5 项用例全通过;真机输出「tools 定义内容变动(…):1 步 —— 最近:2026-10-07 20:30:43」,抖动行消失。

9.6 回滚

备份:wrapup-tools-jitter-20261007/backup/cost_audit/wrapup.py.bak.20261007-223813(登记见 backup/README.md)。

cd /home/rain/Documents/services/token_proxy
cp -p wrapup-tools-jitter-20261007/backup/cost_audit/wrapup.py.bak.20261007-223813 cost_audit/wrapup.py
python3 cost_audit/wrapup.py     # 回滚后预期:「tools 顺序抖动:1 步」重新出现

回滚触发量化:用例 C1 不再计入(真顺序抖动被漏掉)→ 回滚。

9.7 风险说明

风险点 缓解措施
语义变动与第 2 节数字不可比 第 2 节的「顺序抖动」定义本次不变(仍是集合相同);本次只是把其中「序列也相同」的一类移出,故原行数字仍可比
「定义内容变动」无归因口径 只报步数与时刻,不猜具体字段(req_shape 不落 tools 内容);要定位需另抓原始请求体
该类是否值得再优化 生效后 15 h 仅 1 步 ≈ 0.084 元(≈0.13 元/天,与顺序抖动旧值 0.17 元/天同量级)——列观察项,暂不投入

来源:/home/rain/Documents/services/token_proxy/wrapup-tools-jitter-20261007/SOP.md | 最后更新:2026-10-07 22:39(由 build/sync.py 同步,请勿在站点侧修改)