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