跳转至

SOP:proxy 侧空闲后探测前缀缓存(cache_probe)

  • 主题目录:cache-probe-20261008/|实施日期:2026-10-08|实施人:Agent(用户授权)
  • 实现体:proxy.py(新增 parse_cache_probe_config() 与 CacheProbe 类)、config.yaml(新增 cache_probe 段)
  • 关联:docs/BASELINE.md §2.1《不切会话路线:手动压缩点模型》、tools-norm-verify-20261007/SOP.md

1. 背景

用户确定的长期路线是「不切会话」——所有上下文留在同一对话里,靠手动压缩控制规模。 这条路线的唯一风险是空闲过久后上游前缀缓存失效:下一次真实请求会整段重建, 实测重建成本 0.25~0.35 元/次(同会话空闲 ≥1 h 的样本中 24.5% 会全量失效)。

此前只能靠「空闲时长」猜(1-3 h 档全 miss 率 19%、6-12 h 42.9%、>24 h 45.5%), 猜错就有代价:该压没压 → 白付重建费;不该压却压了 → 白付压缩费并打断任务。

本变更把「猜」换成「探」:空闲够久时先发一个复用会话前缀的极小请求, 读回 cached_tokens 算命中占比,当场判定缓存是否还在。

2. 目标

  1. 会话距上次请求 ≥ idle_seconds(默认 3600 s)时,自动探测一次上游前缀缓存;
  2. 命中占比 ≥ hit_ratio(默认 0.90)→ 放行,业务请求照常转发;
  3. 命中占比 < 阈值 → 拒绝本次请求(HTTP 500 + type=cache_expired), 提示用户先压缩上下文再继续(客户端回一句「继续」即可重试);
  4. 空闲不足阈值 → 零额外请求,完全不介入;
  5. 探测任何异常(超时 / 网络错误 / 无 usage)一律放行,不得影响业务可用性;
  6. 探测请求单独落盘,不污染业务会话统计。

3. 前置条件

  • 已备份 proxy.py 与 config.yaml(见 §7);
  • ./venv/bin/python -m py_compile proxy.py 通过;
  • 离线验证通过(cache-probe-20261008/offline_verify.py,7/7);
  • 重启代理会打断在途流式回答 —— 选无在跑任务时执行。

4. 影响范围

项 内容
主机 本机
组件 proxy.py(请求前置判定)、config.yaml(cache_probe 段)
服务 systemd 用户服务 token-proxy.service(8788);需重启才生效
是否停服 是(重启约 1 s),会打断在途流式回答
窗口期 选无在跑任务时
影响面 全局 —— 所有经该代理的会话;但只在「该会话空闲 ≥1 h 后的下一次请求」上动作

5. 分步操作

步骤 1:配置探测参数

在 config.yaml 增加 cache_probe 段(实现体:parse_cache_probe_config())。

cd /home/rain/Documents/services/token_proxy
grep -n -A8 '^cache_probe:' config.yaml

预期:可见 enabled / idle_seconds / probe_tokens / hit_ratio / timeout / max_convs 六项。

坑:idle_seconds: 0 表示不设空闲阈值、每次请求都探测,仅供离线调试; 线上务必保持 3600。hit_ratio 必须落在 (0, 1],越界会被回退为默认并记 warning。

步骤 2:接入探测(proxy.py)

在 _prepare_body() 末尾把本次请求的前缀挂到 meta(不在此处更新快照):

grep -n '_probe_messages' /home/rain/Documents/services/token_proxy/proxy.py

坑(本次踩到并已修):快照必须在判定完成之后更新。若在 _prepare_body() 里 更新,CacheProbe 读到的是刚写入的时间戳,空闲时长恒为 0、探针永不触发。 离线验证第一版就是因此 4 项失败。

坑 2(2026-10-08 实测补修):截断必须停在「工具调用配对完整」处。上游要求 assistant 的 每个 tool_calls 都有紧随其后的 tool 结果;_truncate_messages 若只按 token 预算硬切, 边界可能落在两者之间(或一组 tool 结果中间),补上探测用的 . 消息后上游会直接 400 → 探测拿不到 usage → 降级放行,等于白探一次。实测:4 次探测里 2 次正是如此。 现口径=按预算截断后回退到最后一个「无悬空 tool_calls」的位置;配套在 _send_probe 补一条 缓存探测无 usage: status=… body=… 便于归因。

步骤 3:在 do_POST() 中插入判定

请求转发前先判定;命中不足则拒绝,不再走上游。

grep -n -A6 'if self.cache_probe is not None:' /home/rain/Documents/services/token_proxy/proxy.py | head -20

预期:do_POST() 中一处(判定 + 拒绝 + return)、_record() 中一处(回填 prompt_tokens)。

步骤 4:重启代理使改动生效

systemctl --user restart token-proxy
sleep 1
curl -s http://127.0.0.1:8788/healthz      # 预期 {"status":"ok",...}
journalctl --user -u token-proxy -n 20 --no-pager | grep '代理启动'

预期启动日志末段含 缓存探测=开启(空闲>=3600s, 探前 50000 tok, 命中<90% 拦截, 超时 15s)。

注意:本步会打断当前正在进行的流式回答。

6. 验证

6.1 离线验证(上线前必做,不碰线上)

cd /home/rain/Documents/services/token_proxy
./venv/bin/python cache-probe-20261008/offline_verify.py

预期 9 项全通过:

用例 断言
U1 首次请求不探测 无快照 → 上游只收到 1 次
U2 缓存有效放行 探测 + 业务共 2 次,且首次为探测
U3 缓存失效拦截 500 返回 500 / type=cache_expired,业务请求未转发(上游只收到探测)
U4 探测无 usage 时放行 降级放行
U5 探测体形态正确 从第 0 条起截断(messages 数 < 完整值)、首条 system、保留 tools、max_tokens=1、stream=false
U6 新会话不探测 无快照 → 不探测
U7 开关关闭时不探测 enabled: false → 不介入
U8 截断后无悬空 tool_calls 预算恰好切在 assistant(tool_calls) 与其 tool 结果之间时,截断结果配对完整(悬空列表为空)
U9 反例:旧口径必现悬空 同一夹具下,修复前的纯预算截断必须产生悬空 tool_call_id——确保该判据可被证伪

6.2 线上生效验证

# 探测记录特征:client=cache-probe、probe=true、conv_key=null
grep -c '"probe": true' /home/rain/Documents/services/token_proxy/logs/usage.jsonl
journalctl --user -u token-proxy --since '10 min ago' --no-pager | grep '缓存探测'

预期:平时计数不增长(无空闲则无探测);出现空闲 ≥1 h 的会话后再看,应有 缓存探测 conv=… 空闲=…s 命中=…/… 日志。

失败判据:若出现 缓存探测无 usage: status=… body=…,说明探测请求被上游拒绝 (修复前实测两次 400,根因是截断切断了工具调用配对)。该行会带出上游状态码与响应首段 供定位;配对修复后不应再出现。

7. 回滚

  • 备份位置:cache-probe-20261008/backup/,逐条登记见 backup/README.md。
  • 回滚步骤:

只回退本次截断修复(保留 cache_probe 功能):

cd /home/rain/Documents/services/token_proxy
cp -p cache-probe-20261008/backup/proxy.py.bak.20261008-120526 proxy.py
systemctl --user restart token-proxy
curl -s http://127.0.0.1:8788/healthz      # 预期 {"status":"ok",...}

整体回滚(去掉 cache_probe 功能):

cd /home/rain/Documents/services/token_proxy
cp -p cache-probe-20261008/backup/proxy.py.bak.20261008-005121 proxy.py
cp -p cache-probe-20261008/backup/config.yaml.bak.20261008-005121 config.yaml
systemctl --user restart token-proxy
curl -s http://127.0.0.1:8788/healthz
  • 更快的等效回滚:只把 config.yaml 的 cache_probe.enabled 改为 false 并重启 —— 探测全部短路,行为与回滚代码等价(推荐先试这一步)。
  • 回滚后验证:/healthz 返回 200;logs/usage.jsonl 不再出现 "probe": true 记录。

8. 风险说明

风险 缓解
探测误判 → 无谓拦截、打断任务 阈值 0.90 取自实测:失效时残留命中 ≤25.2 K / 探测 50 K = 50%,离阈值有 40 个百分点余量;且拦截后回一句「继续」即可重试
探测本身的花费 命中时约 0.001 元/次;失效时约 0.03 元/次(该段本该付的 miss 费)。按历史频率(53 次空闲/3 周)折算净成本 ≈ 0.04 元/3 周
探测拖慢空闲后首次请求 多一次往返(约 1~3 s),且只在空闲 ≥1 h 后发生一次;timeout: 15 兜底,超时即放行
探测故障拖垮代理 探测全程包在 try/except 中,任何异常放行;prompt_tokens<=0 也放行
探测记录污染统计 落盘时 conv_key=null + client=cache-probe + probe=true,按 conv_key 聚合的脚本天然忽略它
与「tools 顺序规范化」冲突 无冲突:快照取自 normalize_tools_order() 之后的 tools,与转发给上游的一致
探测因消息形态被上游拒绝 → 白探一次 截断改为「配对完整处停下」(见第 5 章坑 2);仍失败时 _send_probe 打 缓存探测无 usage 供归因;失败一律放行、不影响业务
idle_seconds: 0 被误留在线 config 注释已标注「仅调试用」;启动日志会打印实际生效值

来源:/home/rain/Documents/services/token_proxy/cache-probe-20261008/SOP.md | 最后更新:2026-10-08 12:08(由 build/sync.py 同步,请勿在站点侧修改)