Headroom Phase E 深度解析:LLM 代理的缓存稳定化体系——确定性排序、自动缓存断点与漂移遥测
本文围绕 Headroom 重构计划(REALIGNMENT)中的 Phase E「缓存稳定化」展开,完整覆盖其 6 个 PR 的目标、实现方案、测试与验收标准。读完本文,你将理解为什么 LLM 代理转发链路中 tools 数组顺序、JSON Schema 键序、cache_control 断点位置和 prompt_cache_key 注入会直接影响提示词缓存命中率,并掌握 Headroom 用 Rust 代理补齐这些能力的具体设计与验证方法。
背景:为什么需要"缓存稳定化"这一独立阶段
Headroom 的定位是在工具输出、日志、文件与 RAG 分片到达 LLM 之前先做压缩(Library、proxy、MCP server 三种形态)。重构计划的总纲 REALIGNMENT/00-overview.md 给出的核心结论是:旧版心智模型「压缩 = 从历史中挑选丢弃」是错误的,正确模型是「透传神圣不可侵犯;只压缩 live zone(最新一轮内容),类型感知、哈希键控、位置保持」。system prompt、tools、历史轮次等缓存热区永远不被触碰。
在这个模型下,任何对「请求前缀」的字节级扰动都会击穿上游提供商的提示词缓存。审计(见 REALIGNMENT/01-bug-list.md 的 P3 章节)确认,Rust 代理路径中有一整类「缺失的基础设施」——bug 编号 P3-28 至 P3-32 与 P3-35:
| Bug 编号 | 缺失能力 | 对应 PR |
|---|---|---|
| P3-28 | Rust 路径没有 tools 数组确定性排序 | PR-E1 |
| P3-29 | JSON Schema 键从未被递归排序 | PR-E2 |
| P3-30 | 没有 prompt_cache_key 自动注入 |
PR-E4 |
| P3-31 | 没有 cache_control 自动断点放置(Anthropic) |
PR-E3 |
| P3-32 | 没有易变内容检测器 + 客户告警 | PR-E5 |
| P3-35 | 没有缓存击穿漂移检测遥测 | PR-E6 |
Phase E 正是这 6 项缺失的实现,也是权威工程指南实施清单中的「Phase 3」。计划排期 1 周、6 个 PR,大部分可并行,其中 E3 与 E4 应当成对落地(因为它们共享 Phase F 的 per-mode 策略依赖)。
PR-E1:tools 数组确定性排序(Rust)
- 分支:
realign-E1-tool-array-sort - 风险:LOW 规模:约 +200 LOC
- 依赖:被 PR-B2 阻塞;阻塞 PR-E2
目标:消除 P3-28。在请求转发出站前,将 tools[] 按工具名做字母序排序。该操作必须满足幂等性——对已排序数组再次排序是 no-op。Rust 侧实现的排序键与输出字节必须与 Python 路径的既有实现保持一致(序列化差异除外)。
Python 侧的参考实现是 headroom/proxy/handlers/anthropic.py 中的 _sort_tools_deterministically(L476-L504),其排序键由 _tool_sort_key(L354-L366)给出:
@staticmethod
def _tool_sort_key(tool: dict[str, Any]) -> tuple[str, str]:
"""Deterministic sort key for Anthropic/OpenAI-style tool definitions."""
name = (
str(tool.get("name", ""))
or str(tool.get("function", {}).get("name", ""))
or str(tool.get("type", ""))
)
try:
canonical = json.dumps(tool, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
except Exception:
canonical = str(tool)
return (name, canonical)
注意这个排序键的设计:主键是工具名(兼容 Anthropic 的 name 字段与 OpenAI 的 function.name 嵌套结构),次键是整个工具定义的规范化 JSON 序列化(sort_keys=True + 紧凑分隔符)。这意味着不仅数组顺序确定,同名/无名工具的相对位置也是确定的——无名工具回退到按序列化内容(或 MD5)参与排序。
同样值得注意的是 Python 实现中的一条重要守卫:当任一工具已携带 cache_control 标记时,整个排序直接跳过。源码注释解释了原因——工具上的断点意味着「缓存到包含该工具为止的全部前缀」,重排数组会改变断点内包含哪些工具;若存在两种不同 TTL 的标记,重排可能把 1 小时的断点放到 5 分钟断点之后,而 Anthropic 会直接拒绝这种顺序(仓库中对应 #2939 类问题)。这一守卫在 Rust 侧有镜像(any_tool_has_cache_control,位于 crates/headroom-proxy/src/compression/live_zone_anthropic.rs),相关的 TTL 顺序约束还有专门测试 tests/test_cache_control_ttl_order.py。
PR-E1 在 Rust 侧的具体改动:
- 新增
crates/headroom-proxy/src/compression/tool_def_normalize.rs:
pub fn sort_tools_deterministically(tools: &mut Vec<&RawValue>) -> Result<()> {
// Sort key: tool["name"] string, fallback to MD5(serialized) for unnamed tools.
tools.sort_by_key(|t| {
let parsed: serde_json::Value = serde_json::from_str(t.get()).unwrap_or_default();
parsed.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string()
});
Ok(())
}
-
修改 三个 live zone 处理器,在转发前对请求体中的
tools数组调用上述函数(且仅在 PAYG 模式下生效,受 Phase F 的 PR-F2 策略门控): -
新增测试(
crates/headroom-proxy/tests/integration_tool_sort.rs):sort_alphabetic_by_name—— 按名字母序排序idempotent_resort_no_change—— 重排幂等byte_stable_across_runs—— 跨运行字节稳定
验收标准:测试通过,且 Rust 与 Python 两个实现输出的 tools[] 字节级相等(跨语言 parity)。仓库中 Python 侧的既有回归可作参照:tests/test_proxy_handler_helpers.py 与 tests/test_issue_728_empty_tools_injection.py 都直接调用了 _sort_tools_deterministically 验证排序行为。
回滚:git revert。注意文档标注的残余风险:Rust 路径的排序与客户端工具顺序对齐,但仍存在缓存击穿风险(对已缓存了「客户端原始顺序」的会话,切换为排序顺序会触发一次重新缓存)。
PR-E2:JSON Schema 键递归排序
- 分支:
realign-E2-schema-key-sort - 风险:MEDIUM 规模:约 +300 LOC
- 依赖:被 PR-E1 阻塞;阻塞 PR-E5
目标:消除 P3-29。E1 只解决了 tools 数组的顺序问题;E2 深入到每个工具的 input_schema 内部,递归排序 JSON Schema 中所有对象节点的键。覆盖范围包括嵌套的 properties、definitions、oneOf、anyOf、allOf、if/then/else、additionalProperties 等任意深度。
风险说明值得展开:递归排序理论上可能破坏 schema 语义——如果存在依赖顺序的 if/then/else 或 oneOf 结构。文档的判断是「JSON Schema 的对象键序无语义(键序不影响校验),数组序才有语义」,因此实现策略是:
- 对象节点:重建为按字母序排列的
IndexMap(有序 map,保证序列化时键序固定); - 数组节点:顺序一律保留——
prefixItems的位序语义、oneOf候选项的优先级顺序都必须原样保持。
改动落点同样在 tool_def_normalize.rs,新增 sort_schema_keys_recursive 函数,遍历每个 Object 节点并重建为字母序的 IndexMap。
新增测试(crates/headroom-proxy/tests/integration_schema_sort.rs):
| 测试 | 验证点 |
|---|---|
flat_schema_keys_sorted |
扁平 schema 键被排序 |
nested_properties_sorted |
嵌套 properties 递归排序 |
oneof_array_order_preserved |
oneOf 数组顺序不被打乱 |
definitions_keys_sorted |
definitions 键排序 |
idempotent_resort |
幂等性 |
验收标准:测试通过;并额外要求对真实生产工具 schema 做快照测试——例如 Claude Code 的 Read 工具 schema——把排序后的字节固定下来,防止未来实现漂移。
回滚:git revert;代价是 schema 键序回到客户原始顺序(缓存收益变小但行为正确)。
PR-E3:cache_control 断点自动放置(Anthropic)
- 分支:
realign-E3-cache-control-auto-place - 风险:MEDIUM-HIGH(向客户端请求中自动追加字节,且仅 PAYG 模式) 规模:约 +400 LOC
- 依赖:被 PR-A4、PR-F1、PR-F2 阻塞;不阻塞他人
目标:消除 P3-31。当检测到 PAYG(按量付费)模式——依赖 Phase F 的 PR-F1 认证模式分类——且客户请求中没有任何 cache_control 标记时,自动放置最多 4 个 ephemeral 断点:
- system prompt 末尾(1 个)
tools[]末尾(1 个)- 最后一条「稳定」对话历史边界之后(1 个,"稳定"的判定阈值可配置)
- 最新一条用户消息之前(1 个)
OAuth 与订阅模式:绝不自动放置——在这些模式下自动追加断点可能使授权范围失效(void scope),属于比缓存收益严重得多的风险。
实现方案:
- 新增
crates/headroom-proxy/src/compression/cache_control.rs,核心签名为pub fn auto_place_breakpoints(body: &mut serde_json::Value, auth_mode: AuthMode):遍历请求体结构,向 system、tools、历史、当前用户消息的末尾块追加cache_control: {type: "ephemeral"}。 - 修改 crates/headroom-proxy/src/compression/live_zone_anthropic.rs,仅在 PAYG 下调用。
新增测试(crates/headroom-proxy/tests/integration_cache_control_auto.rs)覆盖了全部关键策略分支:
payg_auto_places_4_markers—— PAYG 下恰好放置 4 个标记oauth_no_auto_placement—— OAuth 模式零自动放置subscription_no_auto_placement—— 订阅模式零自动放置customer_set_markers_respected_no_addition—— 客户已有标记时一个都不多加ttl_ordering_correct_1h_before_5m—— TTL 顺序正确(1h 在 5m 之前),这正是 E1 中「有标记则跳过排序」守卫所保护的同一条上游约束
验收标准:测试通过;带既有标记的客户请求字节不变;代表性 PAYG 请求在正确位置获得 4 个标记。
回滚:git revert。没有自带 cache_control 的客户将失去自动断点,缓存收益变小但行为依然正确。
PR-E4:prompt_cache_key 自动注入(OpenAI)
- 分支:
realign-E4-prompt-cache-key-inject - 风险:MEDIUM 规模:约 +250 LOC
- 依赖:被 PR-F1、PR-F2 阻塞
目标:消除 P3-30(审计确认此前代码库中对该字段是「零引用」)。对 PAYG 模式下 OpenAI Chat Completions 与 Responses 两类请求,若客户未设置 prompt_cache_key,自动注入一个由稳定会话哈希派生的值,使 OpenAI 的缓存路由更黏性(sticky)。
两条硬约束:
- OAuth/订阅模式绝不注入——上游 CLI 可能已经填充该字段,Headroom 必须逐字节保留客户原值;
- 派生必须确定性:
derive_prompt_cache_key(session_id, model)返回{session_id}_{model_family},同一会话 + 同一模型族永远得到同一 key。
改动落点:
- 修改 crates/headroom-proxy/src/compression/live_zone_openai.rs 与 crates/headroom-proxy/src/compression/live_zone_responses.rs,各增加
inject_prompt_cache_key步骤; - 新增
crates/headroom-proxy/src/session.rs中的pub fn derive_prompt_cache_key(session_id: &str, model: &str) -> String。
新增测试(crates/headroom-proxy/tests/integration_prompt_cache_key.rs):
payg_auto_injects_when_absent—— 缺失时自动注入customer_value_preserved—— 客户值被保留oauth_no_injection/subscription_no_injection—— 非 PAYG 模式不注入same_session_same_key_deterministic—— 同会话 key 确定性
回滚:git revert。PAYG 下 OpenAI 缓存路由黏性下降,功能不受影响。
E3 与 E4 虽然分属 Anthropic/OpenAI 两个协议面,但共享同一个前置——Phase F 的认证模式分类(PR-F1)与 per-mode 策略门(PR-F2)——因此文档明确要求两者成对落地:同一份「按模式决定能否改写请求」的策略,同时约束两侧行为。
PR-E5:易变内容检测器(只告警,不改写)
- 分支:
realign-E5-volatile-detector - 风险:LOW 规模:约 +400 LOC
- 依赖:被 PR-E2 阻塞
目标:消除 P3-32。在提示词前部(system prompt / instructions)检测动态内容——时间戳、UUID、JWT token、构建哈希、随机化 ID——然后通过一行日志 + 一个 Prometheus 指标向客户暴露。明确不重写请求:Python 时代的 headroom/transforms/cache_aligner.py 曾经做的是「检测到即重写」,该重写路径已在 Phase A 的 PR-A2 中被删除,E5 只继承其检测价值、剥离其改写副作用。
实现方案——新增 crates/headroom-core/src/transforms/volatile_detector.rs:
pub struct VolatileDetector { /* compiled regex set */ }
impl VolatileDetector {
pub fn scan(&self, content: &str) -> Vec<VolatileFinding>;
}
pub struct VolatileFinding {
pub kind: VolatileKind, // Timestamp | Uuid | Jwt | BuildHash | ...
pub byte_offset: usize,
pub matched: String,
pub recommendation: String, // "Move to metadata"
}
内置的模式集合(全部为预编译正则):
| 类型 | 模式 |
|---|---|
Timestamp |
ISO 8601 时间戳正则 |
Uuid |
UUID v4 正则 |
Jwt |
eyJ... 开头的 JWT 形状 |
BuildHash |
长度 ≥32 的长十六进制串 |
| (时间戳补充) | 与当前时刻在同一数量级(order of magnitude)内的 Unix epoch 数值 |
接线位置与 E1 相同的三个 live zone 文件:对 system prompt(Anthropic)、instructions 字段(OpenAI)以及 Responses 请求运行扫描,有发现则记告警日志并递增指标。
新增测试分两层:
- 检测器单元测试(
crates/headroom-core/tests/volatile_detector.rs):detects_iso_8601_timestamp、detects_uuid_v4、detects_jwt_shape、detects_build_hash,以及关键的负例no_false_positives_on_normal_prose(普通散文不得误报); - 代理集成测试(
crates/headroom-proxy/tests/integration_volatile.rs):warning_logged_on_volatile_system_prompt(触发告警日志)、system_prompt_bytes_unchanged(system prompt 字节零变化——这是"只告警不改写"承诺的可执行验证)。
验收标准:测试通过;且检测器在 4KB system prompt 上运行时间 <1ms,不得拖慢请求路径。
回滚:git revert。损失一个检测能力,无功能回归。
PR-E6:缓存击穿漂移检测遥测
- 分支:
realign-E6-cache-bust-detector - 风险:LOW 规模:约 +500 LOC
- 依赖:被 PR-B2 阻塞
目标:消除 P3-35。对每个请求计算前缀哈希(system + tools + 前 N 条稳定消息),按会话跟踪前缀哈希序列;当同一会话的相邻轮次间前缀哈希发生变化时,递增计数器并记录 prefix_drift 事件,标注是哪个子系统造成了变更(Phase A 落地后预期为"无",但如果发生,团队必须知道)。
实现方案——新增 crates/headroom-proxy/src/observability/prefix_drift.rs:
pub struct PrefixDriftDetector {
// Keyed by session_id; stores last-seen prefix hash + timestamp.
cache: Cache<SessionId, (PrefixHash, Instant)>,
}
impl PrefixDriftDetector {
pub fn check(&self, session_id: &str, body: &serde_json::Value) -> DriftCheck;
}
pub enum DriftCheck {
FirstSeen,
Stable,
Drifted { previous_hash: PrefixHash, new_hash: PrefixHash, age: Duration },
}
三态设计值得注意:FirstSeen(会话首轮,无基线可比)与 Stable(哈希不变)都是正常态,只有 Drifted 携带前/后哈希与年龄并触发指标。指标侧在 crates/headroom-proxy/src/observability/prometheus.rs 增加 prefix_drift_detected_total{provider, model} 计数器。
新增测试(crates/headroom-proxy/tests/integration_prefix_drift.rs):
stable_prefix_no_drift—— 前缀不变不触发system_change_detected_as_drift—— system 变更被判定为漂移tools_reorder_detected—— tools 重排同样被判定为漂移(与 E1 的排序语义联动:排序本身是确定性的,故 E1 落地后 tools 重排不应再产生漂移)
验收标准:测试通过;一个会话语中修改 system prompt 的 canary 请求能触发该计数器。
回滚:git revert。损失可观测性,无功能回归。
依赖关系与阶段验收
6 个 PR 的依赖闭包整理如下(「阻塞于」指落地前提):
| PR | 阻塞于 | 阻塞 | 说明 |
|---|---|---|---|
| E1 | B2 | E2 | 排序是后续一切前缀稳定的基础 |
| E2 | E1 | E5 | schema 排序先于检测器接线 |
| E3 | A4、F1、F2 | — | 需认证模式分类与策略门 |
| E4 | F1、F2 | — | 与 E3 成对落地 |
| E5 | E2 | — | 检测器独立于改写路径 |
| E6 | B2 | — | 依赖 live-zone 引擎稳定 |
当 6 个 PR 全部落地后,Phase E 的验收清单为:
- tools 数组字母序排序(确定性、幂等)
- JSON Schema 键递归排序
- PAYG 模式下
cache_control自动放置(4 个断点,非 PAYG 模式零改动) - PAYG 模式下 OpenAI
prompt_cache_key自动注入(确定性派生) - 易变内容检测器 + 客户告警(字节零改写,<1ms 延迟预算)
- 每会话缓存击穿漂移遥测
Phase E 整体退役 bug 清单中的 P3-28 至 P3-32 与 P3-35,使 Rust 代理路径在「前缀字节确定性」这一缓存稳定性的根基上,与 Python 既有实现及工程指南的 Phase 3 要求对齐。
小结:一套"最小干预"的缓存稳定化方法论
Phase E 的六个 PR 共同体现了一条清晰的方法论:缓存稳定化的正确姿势是"把前缀做确定",而不是"把内容做修改"。E1/E2 用确定性排序消除非确定字节;E3/E4 用按认证模式门控的自动断点/键注入补齐提供商侧的缓存钩子(且在 OAuth/订阅模式下一律退让);E5/E6 则彻底放弃改写,只用检测与遥测暴露问题。每个 PR 都有独立分支、明确的风险等级、可执行的测试清单和 git revert 回滚路径——这使 1 周内 6 个 PR 的并行推进在工程上可验证、可回退,也为后续 Phase F 的认证模式策略提供了已就绪的落点。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00