首页
/ Headroom Phase G:wrap-CLI 观测面设计——RTK 广度扩展、tokens_saved_rtk 数据平面与代理指标体系

Headroom Phase G:wrap-CLI 观测面设计——RTK 广度扩展、tokens_saved_rtk 数据平面与代理指标体系

2026-09-06 12:04:31作者:董斯意

本文基于 Headroom 重构计划(Realignment)中的 Phase G 规格文档,完整讲解该阶段 3 个 PR 的设计与落地:将 wrap-CLI 覆盖扩展到更多 agent、接通从未被写入的 tokens_saved_rtk 数据平面、补齐缓存命中率/压缩比/令牌校验等 Prometheus 观测面。读完本文,你能理解 Headroom 为何坚持"RTK 只放在 wrap-CLI 侧、不进代理"的架构决策,并能对照 当前 Rust 代理的指标目录指标常量实现 完成指标查询、告警与容量排查。

需要首先说明的一点(该文档自身也标注了):RTK 与 lean-ctx 已整体从 Headroom 中移除——headroom/rtk/headroom/lean_ctx/ 两个包、所有 --rtk / --context-tool 标志、wrap 侧的 hooks 与 hint 文件注入、代理侧的 rtk gain 轮询均已删除,headroom/context_tool_cleanup.py 负责卸载早期版本残留在用户磁盘上的持久化状态(Claude Code hooks、PATH 符号链接、MCP 注册、标记围栏指令块等)。因此下文中 RTK 相关章节应视为历史规格与决策依据;而文档中保留下来的"非 RTK 观测项"(cache-hit rate、compression ratio、token validation)已经在 Rust 代理中真实落地,是当前可直接使用的功能。

一、Phase G 的目标、周期与关键架构决策

规格文档给出的目标(Goal)可以拆成四个部分:

  1. 扩展 RTK 覆盖:把 wrap-CLI 的 agent 覆盖面做宽(历史目标,随 RTK 移除而作废);
  2. 闭合 tokens_saved_rtk 数据平面:修复"字段存在但从未被填充"的 bug(历史目标,字段本身保留);
  3. 增加每次调用的 RTK 指标(历史目标,已随 RTK 移除而取消);
  4. 补齐当时缺失的观测面:缓存命中率、压缩比、令牌校验——这一项是当前代码库中真实存在的功能。

日历周期约 1 周,拆分为 3 个 PR。

为什么 RTK 留在 wrap-CLI 侧,而不是代理侧

这是 Phase G 最值得保留下来的决策记录。依据 Agent F 审计和 2026-05-01 的用户指令,"在代理侧调用 RTK"的方案被明确拒绝,理由有三:

  • (a) 缓存热区风险:对 tool_result 内容做压缩会碰到前缀缓存的缓存热区(cache hot zone)。整个 Realignment 计划的第一、第二条跨阶段不变量(见 REALIGNMENT/INDEX.md)就是"代理不打算修改的字节必须与到达代理的字节 SHA-256 逐字节相等"以及"系统、工具定义、旧轮次、思考/压缩项永远不被修改"。代理侧改写 tool_result 直接违背这两条。
  • (b) 并行实现冲突:与 crates/headroom-core 的 log_compressor 这类已有的日志压缩 transform 形成两套并行的实现。
  • (c) 价值主张不同:RTK 改写的是命令(commands),Headroom 压缩的是输出(outputs),二者解决的不是同一个问题。

因此"Integrate RTK with everything" 被解读为:扩展 wrap-CLI 广度 + 闭合数据平面 + 观测面。这个决策的意义在于:未来任何贡献者在考虑"要不要在代理里调用 RTK"之前,应先看到这份书面化的拒绝理由,避免重复踩缓存热区的坑。

二、PR-G1:wrap-CLI 广度扩展(历史章节)

该 PR(分支 realign-G1-wrap-more-agents,风险 LOW,约 +800 LOC)的目标是消除审计项 P5-62:在已有的 wrap claude / wrap codex / wrap aider / wrap copilot / wrap cursor 模式之上,新增 headroom wrap clineheadroom wrap continueheadroom wrap gooseheadroom wrap openhands 四个子命令。

每个 wrap 子命令遵循统一的四步模式(该模式在 headroom/cli/wrap.py 中仍是现存实现的骨架):

  1. 确保 RTK 二进制已安装(历史上为 _ensure_rtk_binary());
  2. 向 agent 的指令文件(AGENTS.md / .cursorrules 等)注入 <!-- headroom:rtk-instructions --> 标记围栏块;
  3. 启动代理(或附着到一个已在运行的代理);
  4. 用代理的环境变量覆盖来启动 agent CLI。

规划的文件清单(均为历史计划,其中 wrap 相关子命令现存活于 headroom/cli/wrap.py 的单文件实现中,而非独立子包):

文件 说明
headroom/cli/wrap/cline.py Cline(VS Code 内 agent;指令文件为 .clinerules
headroom/cli/wrap/continue_dev.py Continue(.continue/config.json 配置;系统消息注入)
headroom/cli/wrap/goose.py Goose(Block 的 CLI;.goose/config.yaml
headroom/cli/wrap/openhands.py OpenHands(通过 OPENHANDS_INSTRUCTIONS 环境变量注入指令)
headroom/cli/wrap/__init__.py(修改) 注册新子命令
headroom/cli/main.py(修改) headroom wrap --help 列出新 agent
e2e/wrap/run.py(修改) 扩展 e2e runner 覆盖新 wrapper

配套的冒烟测试约定了四个断言点——二进制已安装、指令已注入、代理已启动、对模拟 LLM 的调用可用,外加幂等性约束(test_wrap_idempotent_inject.py::test_double_injection_no_duplicate_block,即重复注入不会产生重复的指令块)。这一"指令注入必须幂等"的约束在 RTK 移除后依然有意义:context_tool_cleanup.py 之所以能安全地把 <!-- headroom:rtk-instructions --> 围栏块当作 Headroom 私有资产去清理,正是因为"这是 Headroom 自己的围栏,没有第三方会写它"。

验收标准:测试通过;手动执行 headroom wrap cline -- claude-3-7-sonnet 能拉起一个前面带代理、.clinerules 里带 RTK 指令的 Cline 会话。回滚方式是 git revert,旧 wrapper 不受影响。文档还记录了延后项:Roo Code、Devin 风格 CLI、裸 gh copilot 独立版、gpt-engineer、sweep、smol-developer 等 agent 留待采用度证明价值后以独立 PR 加入。

三、PR-G2:tokens_saved_rtk 数据平面的设计(历史章节,字段仍在)

该 PR(分支 realign-G2-tokens-saved-rtk,风险 LOW,约 +200 LOC)针对审计项 P5-60:tokens_saved_rtk 字段是死字段(已分配、从未被填充)。它的设计链路值得作为"数据平面接线"的范例拆解:

  1. 数据源:定期轮询 rtk gain --format json。代理侧的 _get_rtk_stats(当时位于 headroom/proxy/helpers.py:132)返回结构体 RtkStats { invocations: int, tokens_saved: int, last_run_at: datetime }轮询结果带 5 秒的记忆化(memoization),避免每次记账都触发子进程。
  2. 差值计算:在 headroom/subscription/tracker.py 中新增 _last_rtk_tokens_saved: int = 0 状态;每次调用 update_session_savings 时拉取 _get_rtk_stats(),计算 delta = current.tokens_saved - self._last_rtk_tokens_saved,把 tokens_saved_rtk=delta 写入贡献记录,并更新自身状态。
  3. 落点字段SubscriptionContribution(定义在 headroom/subscription/models.py)上的 tokens_saved_rtk 字段——该字段在 RTK 移除后仍然存在headroom/subscription/models.pyheadroom/subscription/tracker.py 中仍含 rtk 相关引用),只是不再有生产路径为其赋非零值。

规划的三个测试覆盖了数据平面的三类典型风险:

  • test_tokens_saved_rtk_populated_from_rtk_stats —— 字段确实从 RTK 统计中被填充;
  • test_delta_computed_correctly_across_polls —— 跨多次轮询的差值计算正确(防止把累积值当增量重复记账);
  • test_rtk_failure_zero_delta_no_throw —— RTK 轮询失败时返回零差值而不是抛异常(观测面不允许打断主记账链路)。

验收标准:测试通过;一次带 RTK 调用的 wrap 会话结束后 tokens_saved_rtk > 0。回滚语义也很干净:git revert 后该字段退回"静默的零",不影响其他功能。这段"字段先于功能存在 → 被审计发现死掉 → 接线 + 失败降级为零差值"的完整生命周期,在 REALIGNMENT/01-bug-list.md 的 P5-60 条目中有对应的审计记录。

四、PR-G3:代理观测面——当前真实落地的核心

这是 Phase G 中当前代码库中最值得关注的部分。规格列出的指标清单、告警语义、文件落点如下,其中绝大多数已在 Rust 代理中实现。

4.1 指标清单(规格 → 现状对照)

指标 类型 规格中的定义 现状
wrap_rtk_invocations_total{tool} 计数 来自 rtk gain --format json 轮询,tool 标签是 git/ls/cargo 等被改写的命令 已取消:RTK 集成整体移除
wrap_rtk_tokens_saved_per_session 直方图 会话结束时写入 已取消:同上
proxy_cache_hit_rate_per_session 直方图 由每会话的 usage.cache_read_input_tokens / total_input_tokens 计算 已落地,是 Phase H(Python 代理退役)的金丝雀门禁
proxy_compression_ratio_by_strategy{strategy, content_type} 直方图 每块压缩比 已落地
proxy_compression_rejected_by_token_check_total{strategy} 计数 压缩器跑了但输出未变小的次数(PR-B4 引入,PR-G3 保证导出) 已落地
proxy_passthrough_bytes_modified_total{path} 规范中写作 gauge 必须恒为 0(压缩开启路径之外),非零即告警 已落地,以计数器实现
proxy_rate_limit_remaining_* 规范中写作一组 从上游响应头提取 已落地,拆为 requests/tokens/input/output 四个 gauge
proxy_service_tier_count_total{tier} 计数 service_tier 分布 已落地
proxy_response_status_count_total{status} 计数 incomplete / failed / cancelled / completed / in_progress 已落地
proxy_image_generation_call_log_redacted_total 计数 请求日志中多 MB base64 图片被脱敏的次数 已落地(Python 侧)

4.2 规格中的文件落点

  • crates/headroom-proxy/src/observability/prometheus.rs —— 新增全部指标;
  • crates/headroom-proxy/src/sse/anthropic.rs —— 在 message_delta 处发出缓存命中率;
  • crates/headroom-proxy/src/sse/openai_responses.rs —— 在 response.completed 处发出;
  • crates/headroom-proxy/src/sse/openai_chat.rs —— 在最终 usage chunk 处发出;
  • crates/headroom-proxy/src/handlers/responses.rs —— 提取并记录 service_tier;当 status == incomplete 时记录 incomplete_details.reason
  • headroom/proxy/request_logger.py —— 脱敏超过 1024 字节的 base64 字符串,替换为 <base64 truncated, X bytes>(P4-45 图片日志脱敏也在此 PR 落地);
  • crates/headroom-proxy/src/observability/cache_hit_rate.rscompression_ratio.rs —— 两个新模块(当前在 crates/headroom-proxy/src/observability/ 目录中可查证);
  • docs/observability.md —— 记录每个指标的含义与漂移时的运维动作(当前仓库中该文档已成文,含完整指标目录与 PromQL 示例)。

规划的验收测试包括:cache_hit_rate_emitted_per_sessioncompression_ratio_emitted_per_strategypassthrough_bytes_modified_zero_when_no_compressionservice_tier_loggedincomplete_status_logged_with_reason(集成测试位于 crates/headroom-proxy/tests/integration_metrics.rs),以及 Python 侧的 tests/test_image_log_redaction.py::test_large_base64_truncated——该测试文件在当前 tests/ 目录中真实存在。

4.3 源码级实现证据

指标名称与标签集中管理。 从源码结构看,所有 PR-G3 指标线名与标签键都集中在 crates/headroom-proxy/src/observability/metric_names.rsMETRIC_* 是线名常量、METRIC_*_HELP 是注册时的 HELP 文本、LABEL_* 是标签键常量。文件头注释明确写道这是 Realignment 构建约束"可配置:每个指标名 + 标签词表集中定义在一处"的落点——重命名一个指标时,代码评审只需看一个文件。这个"重命名只出现在一个 diff 里"的工程约束,比指标本身更值得借鉴。

标签基数纪律(Cardinality discipline)。 同一文件给出了有界标签词表的完整实现:

  • service_tier 标签词表是严格闭集 {auto, default, flex, on_demand, priority, scale} 加一个哨兵值 othervalidate(raw) 函数对入站 JSON 中的 service_tier 做大小写敏感匹配,未识别的值被分桶到 other 并打 tracing::warn!——原始入站值永远不会直接用作标签,恶意客户端每请求发一个随机 service_tier 也无法击穿标签基数(cardinality DoS),同时线格式漂移会在日志中"大声"出现而不是被静默分桶。
  • response_status 是五值枚举 completed / incomplete / failed / cancelled / in_progressin_progress 是非终态入口——客户端中途断流时,按"最后观测到的状态"计数,观测方能看到请求是流中途关闭的。
  • docs/observability.md 的"Cardinality discipline"一节进一步说明:auth_mode 是 3 值枚举、provider 3 值、strategycontent_type 都是 &'static str(来自压缩器的 BlockAction::Compressedheadroom_core::transforms::ContentType);而 Python 代理的 model 标签因为取自请求体(客户端可控),在记录时刻用 MAX_DISTINCT_MODELSheadroom.telemetry.context)封顶,超出后并入 "other" 哨兵并打一次性警告。结论是:不存在任何客户端可控值能驱动无界标签基数的代码路径

缓存命中率的发射闸门。 proxy_cache_hit_rate_per_session 直方图只在 SSE 流完整结束才采样:Anthropic 侧要求 StreamStatus::MessageStop,OpenAI Chat 侧要求 state.usage.is_some()(最终 usage chunk 只在流完成时到达),OpenAI Responses 侧要求 terminal_status().is_some()。客户端中途断流时,通道关闭但终态标志未置位——此时记录日志并跳过采样,而不是把"半截流"的脏样本计入分布。从源码结构看,这一"H2 aborted-stream gate"设计保证直方图里的每个样本都对应一次完整计费会话,是缓存命中率数据可信的前提。

按策略独立的压缩比(H1 修复)。 proxy_compression_ratio_by_strategy 每个策略使用该策略自己的 before/after 令牌数采样(经 Outcome::Compressed.per_strategy_tokens 从 live-zone manifest 穿透)。修复前,一个 body 上跑了多个策略时会把同一个聚合比值重复发给每个策略标签,导致按策略分列的仪表盘读到"垃圾数据"。配合 proxy_compression_rejected_by_token_check_total(压缩器跑了但输出未严格变小、保留原文的次数),运维可以区分"策略根本没在缩"和"策略跑了但被令牌校验拒绝"两种失效形态。

缓存安全告警接线(C2)。 proxy_passthrough_bytes_modified_totalproxy.rs 发出:当派发器承诺逐字节透传(Outcome::NoCompressionOutcome::Passthrough)但最终 body 字节长度发生变化时,按字节差在请求路径标签下累加。检查发生在 prompt_cache_key 注入器之前,因此注入器有意的字节修改不会触发告警——这正是规格中"必须恒为 0(压缩开启路径之外),非零即告警"语义的实现方式,与 Realignment 的全局不变量第 1 条(字节忠实)形成"实现 + 自检"闭环。

H3 force-zero 与启动形态。 prometheus crate v0.13 的 gather() 会整体跳过空 MetricVec——家族第一次被带标签元组累加前,连 HELP/TYPE 行都不出现。为了让运维从启动起就看到完整目录,handle_metrics 在首次抓取前用哨兵标签 __init__ 对每个 counter/gauge MetricVec 做 +0 触碰;因此 PromQL 查询统一带 {... != "__init__"} 过滤(docs/observability.md 中的全部示例查询都包含该过滤)。直方图做 force-zero——合成的 observe(0.0) 会贡献真实样本、污染分位数读数,所以两个直方图家族在首个真实会话后才出现在抓取结果中。配套的 H4 契约把 prometheus 依赖在 Cargo.toml 中精确钉版= "0.13.4",不用 caret),并给出了升级后必须重跑的 5 步回归脚本。

4.4 可复制的查询与运维动作

规格文档的验收标准包含"手动抓取 /metrics 能看到新指标家族"。当前文档给出的可直接使用入口:

curl -s http://127.0.0.1:8787/metrics

典型运维查询(全部摘自 docs/observability.md,均含 __init__ 过滤):

# 缓存安全告警,恒应为 0
sum(rate(proxy_passthrough_bytes_modified_total{path!="__init__"}[5m]))

# 按策略 p50 压缩比
histogram_quantile(0.50, sum by (strategy, le) (rate(proxy_compression_ratio_by_strategy_bucket{strategy!="__init__"}[1h])))

# 压缩器跑了但被令牌校验拒绝的策略(高比率 = 该压缩器需要调参)
sum by (strategy) (rate(proxy_compression_rejected_by_token_check_total{strategy!="__init__"}[1h]))

# 上游限流余量(越小越接近被限流)
proxy_rate_limit_remaining_tokens{provider="anthropic"}

其中 proxy_cache_hit_rate_per_session 还承担了 Phase H 金丝雀门禁的职责:判断"发布 Rust、退役 Python"的脚本对 p50/p95/p99/均值四条查询全部与 Python 基线比对——单看一个分位数不够,只在长会话尾部出现的回归会溜过中位数检查,任何一条低于基线即门禁失败。

五、SUPERSEDED 的部分与残留清理机制

Phase G 文档开头的 SUPERSEDED 声明定义了它的历史边界,值得逐条对照现状:

  • RTK 与 lean-ctx 全部移除headroom/rtk/headroom/lean_ctx/ 包不存在;headroom/context_tool_cleanup.py 是当前代码库中处理该遗留的唯一模块。它的 docstring 完整记录了被移除集成的"磁盘持久化足迹":下载的 rtk(Rust Token Killer)与 lean-ctx 二进制、Claude Code PreToolUse hooks、PATH 上的符号链接、注入到十余个 agent hint 文件的标记围栏指令块、Claude Code 的 MCP 注册。
  • 清理策略的工程细节(均出自 headroom/context_tool_cleanup.py 的文件级注释):
    • 溯源判定:只删 Headroom 安装(或促使上下文工具安装)的文件。MCP 条目的 command、hook 脚本体只有在其路径落在 Headroom 管理 bin 目录内才视为 Headroom 的;hook 条目命名 ~/.claude/hooks 下已分类脚本时继承该脚本的判定;
    • 保守原则~/.local/bin/{rtk,lean-ctx} 仅当它是指向 Headroom bin 目录的符号链接时才被 unlink——用户自己的构建永不触碰;JSON 配置解析失败时报告并跳过,绝不覆盖(手工编辑的笔误不能让用户丢配置);工具自己对配置的备份原样保留;
    • 幂等与一次迁移headroom wrap / headroom unwrap 每次运行都会调用 purge_context_tool_artifacts;机器级清理(hooks、二进制、MCP 注册)在一个工作区内只做一次,随后用 .context-tools-purged 印章文件标记完成,避免每次 wrap 都重写机器级文件;而项目/配置目录作用域的 hint 文件每次启动都检查,因为下次启动可能位于不同项目或不同的 CODEX_HOME
    • 明确承认的边界:两个场景无法自动判定(用户 PATH 上先于 Headroom 已有 lean-ctx 时写出的配置;#1698 之后写入、只 exec 裸 rtk 而不引用管理路径的 hook),文档选择"记录为限制"而不是强行修复——后者会留下一个"静默空转"的 hook(#487、#1698 的已知残留)。
  • tokens_saved_rtk 字段:如前所述,字段在 headroom/subscription/models.py 中保留,RTK 轮询链路(_get_rtk_stats)随功能删除。
  • docs/rtk-architecture.md:规格中原计划新增该文档以书面化"RTK 只留在 wrap-CLI 侧"的决策;RTK 移除时该文档随功能一起被删除(Phase G 文档开头声明),相关决策背景仍完整保留在本文引用的规格文档内。

从源码结构看,metric_names.rs 第 97 行起的注释是这套"先实现、后退役"轨迹的最直接证据:proxy_image_generation_call_log_redacted_total 的 Rust 侧计数器因"无生产发射点"被移除(图片脱敏的唯一真源在 Python 代理 headroom/proxy/prometheus_metrics.py),两个 wrap_rtk_* 名称"彻底消失"——因为它们测量的 RTK 集成已从 Headroom 中删除。

六、Phase G 验收清单与审计项闭合

规格文档末尾的验收摘要(逐项标注现状):

  • wrap CLI 覆盖扩展到 cline/continue/goose/openhands —— 历史项(wrap 子命令本体保留,RTK 相关步骤移除);
  • tokens_saved_rtk 字段端到端填充 —— 历史项(字段保留,轮询链路随 RTK 移除);
  • 每次调用的 RTK Prometheus 指标 —— 已取消
  • 每会话缓存命中率指标 —— ✅ 落地(docs/observability.md + metric_names.rs);
  • 每块压缩比直方图 —— ✅ 落地;
  • 令牌校验拒绝计数 —— ✅ 落地;
  • 透传字节被修改告警 gauge —— ✅ 落地(counter 实现,告警语义不变);
  • 限流头观测与导出 —— ✅ 落地(4 个 gauge);
  • service_tier 分布指标 —— ✅ 落地(带闭集词表 + other 哨兵);
  • 响应终态(incomplete | failed | cancelled)带原因记录 —— ✅ 落地;
  • 图片 base64 日志脱敏 —— ✅ 落地(Python 侧计数 + 1024 字节阈值替换);
  • docs/rtk-architecture.md 决策文档 —— 已随 RTK 删除

文档结尾声明:Phase G 使审计项 P4-41、P4-42、P4-45、P5-58、P5-60、P5-61、P5-62、P6-68、P6-69、P6-72 退出未决清单(审计项全文见 REALIGNMENT/01-bug-list.md,其中 P5-60 即"字段已分配、从未填充"的 tokens_saved_rtk)。

七、可复用的工程实践小结

从 Phase G 规格与当前实现中,可以提炼出四条在 LLM 代理/网关类项目里直接可迁移的做法:

  1. 观测面指标与全局不变量绑定:把"必须恒为 0"的缓存安全指标(透传字节被修改)做成一等公民并接上告警,使"字节忠实"这条架构不变量从文档承诺变成可被监控证伪的运行时断言;
  2. 标签基数纪律:所有客户端可控的输入值一律经过闭集词表验证 + 哨兵分桶后再进标签,原始值永不直接外泄为标签;
  3. 指标目录的启动形态契约:force-zero 触碰让抓取形态从进程启动就可预测,同时用精确钉版 + 回归脚本守护依赖库的"实现定义行为";
  4. 退役功能的清理即产品:功能删除后,用溯源判定、幂等印章、保守跳过原则来处理用户磁盘上的持久化残留,并把"无法自动判定的边界"显式写成已知限制——context_tool_cleanup.py 是这一模式的完整范本。

如需继续深入,建议的阅读顺序:REALIGNMENT/INDEX.md(跨阶段不变量与保留原语清单)→ REALIGNMENT/01-bug-list.md(被 Phase G 闭合的审计项原文)→ docs/observability.md(指标目录、PromQL 与接线细节)→ crates/headroom-proxy/src/observability/metric_names.rs(指标常量与标签词表源码)。

登录后查看全文
热门项目推荐
相关项目推荐