context-mode ADR-0004 深度解读:ctx_stats 单会话压缩率为何改用 strict-compression 严格公式
【免费下载链接】context-modeContext window optimization for AI coding agents. Sandboxes tool output (98% reduction), persists session memory, and enforces routing across 17 platforms via MCP + hooks.项目地址: https://gitcode.com/GitHub_Trending/cl/context-mode
本篇技术指南围绕 context-mode 仓库中的架构决策记录 docs/adr/0004-stats-strict-compression-formula.md 展开,剖析ctx_stats报告"Section 1(Without context-mode / With context-mode 对照条)"在 v1.0.148(PR #685 hotfix)中从"基础设施体量统计"回归"真实压缩率"的完整过程。读者将掌握:为什么eventDataBytes必须从单会话压缩率分子分母中剔除、strict-compression 公式的精确语义与空状态处理、该决策如何与data_hash去重列、lifetime 汇总及 ADR-0001 多写者约束共存,以及对应的源码实现与回归测试证据。
背景:一次从"诚实压缩率"到"基础设施体量统计"的语义漂移
ctx_stats是 context-mode 内置的 MCP 元工具之一(Claude Code 中可通过/context-mode:ctx-stats调用,其他平台在聊天中直接输入ctx stats即可触发),其报告按会话(Section 1)、捕获统计(Section 2)、作用域递进(Section 3)与成本框架(Section 4)分层呈现。其中 Section 1 的Without context-mode / With context-mode横向条形图,本意是回答一个用户最关心的直觉问题:context-mode 到底把多少字节挡在了模型上下文窗口之外。
然而在两次互不相关的 bug 级联之后,这个条目的语义从"诚实的压缩率"悄悄漂移成了"基础设施体量统计"。
v1.0.134 SLICE B:一个为了掩盖"退化 100%"的应急补丁
第一次偏移来自v1.0.134 SLICE B(提交ce62275,2026-05-15),针对的是 src/session/analytics.ts 中的一个退化显示缺陷:在全新会话中bytesReturned == 0时,原始公式
pct = 1 - max(1, returned) / (avoided + returned)会坍缩到约 100%——即便bytesAvoided同样为零,也会画出一条虚高的条形图。SLICE B 的战术性修法是把eventDataBytes同时加进比值的两侧:
Without = bytesAvoided + bytesReturned + eventDataBytes With = max(1, bytesReturned + eventDataBytes)提交信息将其命名为 "bar ratio degenerate fix"——这明确是一个UX 补丁,而不是经过设计的指标语义。Git 考古(Git Archaeologist 的审计结论)同时确认:真正驱动成本的rule_content重复问题,在整个修复过程中从未被纳入考量。
v1.0.148 Bug A+C+D+E+F 级联:真实信号终于流入,公式却开始说谎
第二次偏移来自v1.0.148的 Bug A+C+D+E+F 级联修复(PR #685)。schema 迁移与 per-conversation 聚合器修复终于让公式看到了多年被静默低估的真实bytesAvoided数据。可当真实信号开始流动时,SLICE B"两侧都加 eventDataBytes"的公式立刻暴露出问题:对于用户凭直觉知道压缩率应该高达 95%+ 的会话,报告显示的却是约 56%。
报告者机器上产生了实证证据:
bytesAvoided= 2,898 KB(Bash/Read 重定向节省 + sandbox PID 突发流量)bytesReturned= 140 KB(打印的 ctx_* 输出)eventDataBytes= 2,136 KB——其中84%(约 496 份)是同一份 CLAUDE.md 的重复副本,由 SessionStart hook 在多次 resume 周期中反复捕获;schema 中专门为此准备的data_hash去重列虽然被填充,但公式从未使用它
同一份数据,两种公式给出天差地别的结论:
- SLICE B 公式:显示56%被挡在窗口外
- strict-compression 公式(本 ADR):显示95.4%被挡在窗口外
这 49 个百分点的差距,就是 SLICE B 引入的低估。
决策:Section 1 必须使用 strict-compression 严格压缩公式
ADR 的核心裁决十分明确:per-conversation 的 Section 1 条形图必须改用严格压缩公式,且eventDataBytes从两侧同时剔除:
if (bytesAvoided + bytesReturned == 0) { // 空状态 —— 尚无可测量的重定向活动。 // 不要绘制退化的条形图。输出一行诚实的提示: "No measurable redirect activity captured yet — bars will appear once context-mode diverts its first payload." } else { Without = bytesAvoided + bytesReturned With = max(1, bytesReturned) pct = (1 - With / Without) * 100 }为什么eventDataBytes必须被排除
决策背后的理由是一条清晰的概念边界:hook 捕获的 payload 字节被写入 SessionDB,是为了构建知识库,它们是分析基础设施(analytics infrastructure),而不是曾经进入模型上下文窗口的字节。把这类字节渲染进 Section 1,等于把两个性质完全不同的量混为一谈,产出的必然是一个误导性的数字。
这一定义在源码中留下了完整的注释证据。src/session/analytics.ts 中渲染 Section 1 的代码段明确写道:
Without = bytesAvoided + bytesReturned—— 模型"本应重新看到、却被 context-mode 转移走"的字节;With = max(1, bytesReturned)—— 模型在 context-mode 介入后"实际重新看到"的字节;- 注释特别标注:
eventDataBytes是"hook 捕获的原始 payload(工具参数、提示词正文),用于知识库,它们从不进入模型上下文窗口"。SLICE B 把它们加进任一侧(为了躲避退化 100% 条),会错误地代表上下文成本,把实时会话的真实压缩率从约 95% 压到约 56%。
该实现同时把eventDataBytes归位到它真正属于的地方——Section 2(捕获统计),那里它以"captures count"("1,000 things — files, errors, decisions, agent runs")的形式正确表达 hook 层记录了什么。
空状态:用一句诚实的提示替代退化条形图
公式中的空状态分支是对 SLICE B 症状的根治而非掩盖:当bytesAvoided + bytesReturned == 0(会话早期、schema 迁移恢复中、或工具密集工作尚未重新命中索引),不再绘制 0% 或 100% 的退化条形图,而是输出一行提示,并跳过 Without/With 对照条——诚实优先于装饰(源码注释原文:"honesty over decoration")。
值得注意的是,空状态分支与"诚实的 100%"是两条不同的路径:当bytesReturned == 0但bytesAvoided > 0时(每个被测量的字节确实都被挡在了窗外),With = max(1, 0) = 1,百分比会如实逼近 99.99%——这是诚实的 100%,测试明确允许其存在。
边界:lifetime 汇总(Section 3/4)不受影响
ADR 特别划清了作用域:lifetime 的 Section 3/4 汇总(例如 "14.7 MB kept out across 200 projects")不受本 ADR 影响。它们聚合的是bytesAvoided + eventDataBytes + snapshotBytes,而 lifetime 层级的用户预期一直是"context-mode 存入存储的全部字节"——对这一层级而言这是正确的口径。只有 per-conversation 的%条目的语义被修正。
这一口径差异在 src/session/analytics.ts 的RealBytesStats接口注释中有完整定义:四个字节来源分别对应session_events表的data_bytes、bytes_avoided、bytes_returned与session_resume表的快照长度,而totalSavedTokens = (eventDataBytes + bytesAvoided + snapshotBytes) / 4;bytesReturned被报告但不并入totalSavedTokens——因为它代表模型已经付费看过的字节,加进去会重复计入用户账单上已有的部分。
修复前后对比:同一份数据的三代口径
ADR 给出了报告者机器数据在三代公式下的完整对照表:
| 指标 | v1.0.147(损坏) | v1.0.148 + SLICE B | v1.0.148 + 本 ADR |
|---|---|---|---|
| Without | 158 KB | 5,177 KB | 3,038 KB |
| With | 158 KB | 2,279 KB | 140 KB |
| % kept out | 0%(恒等) | 56%(SLICE B 附带) | 95.4% |
| Runtime multiplier | 1× | 2× | 22× |
| Lifetime headline | 14.7 MB ✓ | 14.7 MB ✓ | 14.7 MB ✓ |
其中 22× 乘数代表的是:这场对话因 context-mode 的重定向而获得的实际上下文窗口跑道延长——这正是用户直觉上期望看到的指标。v1.0.147 的"恒等"是因为 schema 聚合器缺陷导致公式看不到真实的bytesAvoided,只能把 Without 与 With 渲染成同一个 158 KB。
源码级验证:公式在仓库中的真实落点
渲染实现
strict-compression 公式的完整实现位于 src/session/analytics.ts:先取realBytes.conversation的bytesAvoided/bytesReturned两个测量值,命中空状态则输出提示行;否则按convBytesWithout = measuredAvoided + measuredReturned、convBytesWith = Math.max(1, measuredReturned)计算,再换算 token(4 字节/token),并用dataBar()绘制两侧条形,最终输出一行:
Without context-mode 3.0 MB ████████████████████████████ 759,500 tokens With context-mode 140 KB ██ 35,000 tokens 95.4% kept out of context · your AI ran 22× longer before /compact firedconvMult(Math.round(convTokensWithout / convTokensWith))就是上表中 22× 乘数的来源。
worktree 拆分的字节归属
一个容易忽略的实现细节:Section 1 的bytesReturned("With context-mode")与bytesAvoided("kept out")并非简单读库汇总。在 src/session/analytics.ts 的 worktree 拆分逻辑中:
bytesReturned=当前会话的检索返回(真正进入当前实时窗口的字节);bytesAvoided= 整个 worktree 移动的字节(avoided + 每个会话的检索)减去落入你窗口的部分,并钳制在 ≥ 0,保证边缘 DB 永远不会产生负条形图。
这种按worktreeHash(而非"项目根 + 时间")作用域的方式,确保用户并行打开的其他 worktree 不会串入统计,而本会话派生的子代理 fan-out 又能被完整计入。同样地,src/session/retrieval-marker.ts 作为 server→hook 的桥接层专门度量"With context-mode"的检索字节——因为 context-mode 自己的ctx_search/ctx_fetch_and_index不会触发插件自身的 PostToolUse hook,必须由 MCP server 侧直接测量。而bytesAvoided的写入路径由 src/session/event-emit.ts 的emitIndexWriteEvent承担,配合 src/session/extract.ts 从ctx_fetch_and_index返回的 "Fetched and indexed5 sections(47.50KB)" 前导文本中解析出 KB 数值,才能让每次检索的节省如实入账。
回归测试:四条硬断言
ADR 决策第 4 条要求四个 fixture 测试断言新语义。v1.0.134 SLICE B的 describe 块在 tests/analytics/format-report.test.ts 中被重命名为v1.0.148 Bug G — Section 1 bar uses strict-compression formula,四个用例分别钉死:
- 空状态:
eventDataBytes有 50,000 字节(捕获确实存在)但bytesAvoided + bytesReturned == 0时,输出必须匹配No measurable redirect activity captured yet,且不得渲染Without context-mode/With context-mode条形; - 诚实的混合比例:
bytesAvoided=6,000、bytesReturned=4,000、eventDataBytes=100,000时,显示 60% 而非把 eventDataBytes 折进来后的约 5%——注释直白地警告:若回退到 SLICE B 口径,"显示的比例会差得离谱"; - 诚实的 100%:
bytesReturned=0但bytesAvoided=10,000时,With=max(1,0)=1,比例如实落在 ≥ 99%; - 一位小数精度:真实 live-window 数据(8.9 MB kept out / 10.2 KB retrieval,真实比例 99.888%)必须渲染为99.9%,而不是四舍五入后的整数 100%——防止"过度宣称"。
同语义的实库回归测试位于 tests/session/real-bytes-stats.test.ts,用 Mert 机器上的真实行(bytesAvoided=2,898,000、bytesReturned=140,000、eventDataBytes=2,139,000)硬断言百分比落在 94~96 区间,并注明三代口径的差异:"strict → ~95%,SLICE B → ~56%,Bug E+F 修复前 → ~6%"。
后果与边界:一次纯读侧的指标语义修正
ADR 明确列出了六条可验证的后果:
- 显示百分比将跳变:现有用户首次在 v1.0.148 后调用
ctx_stats,Section 1 会从约 56% 跳到约 95%。这是指标语义变更,不是数据丢失;lifetime 数字与捕获计数与 v1.0.147 完全一致。 - 空状态处理显式化:新会话不再看到退化的 0%/100% 条形图,而是单行提示。
data_hash去重列不再是正确性承重点:去重是 EM 审计决策树中的候选修复之一(Option B,得票 86%),但 strict-compression 才是正确修复——因为rule_content重复问题只有在你要统计eventDataBytes时才成立,而我们恰恰不统计它。从源码看,data_hash列(如 src/adapters/openclaw/plugin.ts 与 src/adapters/pi/extension.ts 用sha256(data).slice(0,16)填充)仍作为去重基础设施保留,只是不再为 Section 1 的正确性负责。- 测试更新:上述四个 fixture 断言即为其直接产物。
- README 与 release notes 同步:此前基于 lifetime 公式引用的约 98% 节省数据仍然有效;新的 per-conversation 头条数字与 strict-compression 比例一致。
- ADR-0001(多写者)保持不变:这是最关键的架构边界——本 ADR 只改读侧公式。无 schema 新增、无锁、无 EXCLUSIVE pragma,SQLite WAL + busy_timeout 不变量按 docs/adr/0001-sessiondb-multi-writer.md 原样保留。
总结:指标定义比显示美观更重要
ADR-0004 的完整教训可以浓缩为一句话:一个为掩盖显示缺陷而临时引入的公式,会在真实信号涌入后变成系统性低估的源头。SLICE B 的eventDataBytes双加策略本质上是"为了让条形图不难看"而把分析基础设施字节伪装成上下文字节;strict-compression 公式则回归了三个可辩护的原则——分子分母只容纳真正进出上下文窗口的字节、空状态用诚实提示替代退化条形图、lifetime 与 per-conversation 各用各的正确口径。对任何想为 Agent 工具链设计"节省指标"的开发者而言,这个 ADR 提供了可复用的范式:先明确"什么才算真正进入了窗口",再决定展示什么。
【免费下载链接】context-modeContext window optimization for AI coding agents. Sandboxes tool output (98% reduction), persists session memory, and enforces routing across 17 platforms via MCP + hooks.项目地址: https://gitcode.com/GitHub_Trending/cl/context-mode
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考