首页
/ Headroom Phase E 深度解析:LLM 代理的缓存稳定化体系——确定性排序、自动缓存断点与漂移遥测

Headroom Phase E 深度解析:LLM 代理的缓存稳定化体系——确定性排序、自动缓存断点与漂移遥测

2026-09-05 22:25:58作者:蔡丛锟

本文围绕 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(())
}

验收标准:测试通过,且 Rust 与 Python 两个实现输出的 tools[] 字节级相等(跨语言 parity)。仓库中 Python 侧的既有回归可作参照:tests/test_proxy_handler_helpers.pytests/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 中所有对象节点的键。覆盖范围包括嵌套的 propertiesdefinitionsoneOfanyOfallOfif/then/elseadditionalProperties 等任意深度。

风险说明值得展开:递归排序理论上可能破坏 schema 语义——如果存在依赖顺序的 if/then/elseoneOf 结构。文档的判断是「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 断点

  1. system prompt 末尾(1 个)
  2. tools[] 末尾(1 个)
  3. 最后一条「稳定」对话历史边界之后(1 个,"稳定"的判定阈值可配置)
  4. 最新一条用户消息之前(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/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_timestampdetects_uuid_v4detects_jwt_shapedetects_build_hash,以及关键的负例 no_false_positives_on_normal_prose(普通散文不得误报);
  • 代理集成测试(crates/headroom-proxy/tests/integration_volatile.rs):warning_logged_on_volatile_system_prompt(触发告警日志)、system_prompt_bytes_unchangedsystem 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 的认证模式策略提供了已就绪的落点。

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