問題根因回顧
headroom 的 ccr_retrieve 工具讓模型能主動召回被壓縮的上下文。
問題在於,原設計在每個請求都把這個工具注入 tools 陣列——
即使這次請求根本沒做任何壓縮,工具也會在那裡。
tools 陣列是 prefix cache 最前面的一段。多了 1 個工具,整段前綴就不同了, API 端對跨 process 流量的部分命中容錯機制就失效了。 多數 session 不需要壓縮,卻全數付出了「tools 前綴變動」的代價。
設計移動:決定從 building block 上移到 orchestration
關鍵洞察是:register_ccr_tool 本身沒問題,問題是「何時呼叫它」。
原本這個決定在 building block 層(register 永遠被呼叫);
M8 把它往上移到 orchestration 層,讓 compress 的結果決定要不要 register。
新的 pipeline 順序:stabilize → compress → (有壓縮才)register。
「有沒有壓縮」的訊號用 identity 傳遞——Python compress 若沒有壓任何東西,
回傳原物件本身(result is original);
Rust 版本回傳 Cow::Borrowed(不可變借用)。
orchestration 層只看這個訊號,不需要 compress 函式知道 register 的存在。
工具放尾端,前綴保留更多
如果 compress 觸發了,tool 還是要掛。這時掛在 tools 陣列的哪個位置, 對快取影響截然不同。
Prefix cache 是逐 byte 前綴比對,從 {"tools":[ 這個位置開始算。
如果把 ccr_retrieve 插到前面(例如排序到最前),
從 tools 第一個元素就開始分岔——後面所有 bytes 都在不同前綴上。
改放尾端:client 既有的 tools 維持 byte-identical,
divergence point 推到最後,保住更多前綴。
工具插哪裡,決定了快取能保住多少。
補齊 Python orchestration 層
M8 前,Python 沒有統一的 pipeline 函式—— 步驟順序只內聯在 parity 測試腳本的 shell 命令裡。 這意味著 M7 proxy 要呼叫 Python pipeline 時,需要知道每個步驟的名字和順序。
M8 新增 src/headroom_lite/pipeline.py::process_request,
讓 Python 有了對稱於 Rust 的 pipeline::process_request 的 orchestration 入口。
M7 proxy 的 proxy.rs:84 透過這個入口接線,lazy registration 自動繼承進去,
不需要再動 proxy 層的代碼。
TDD:Python 先行,Rust 跟進,parity 把關
Python 7 個測試案例(涵蓋:不壓縮不掛工具、壓縮才掛、工具位置在尾端、
多次呼叫冪等性等),先寫測試、確認 RED(ModuleNotFoundError),
再寫實作讓它變 GREEN。全套 37 個測試通過。
Rust 對應的 4 個 pipeline 測試案例,含新增的 lazy-skip 情境。 全套 41 個測試通過,零 warning。
最有說服力的是 parity gate:5 個 fixtures 逐 byte 比對 Python 和 Rust 的輸出。
其中 02_sacred_markers 這個 fixture:舊版會把輸入從 436 bytes 變成 775 bytes
(無條件塞了 ccr 工具),新版從 436 → 436(乾淨穿透,沒有壓縮就沒有任何變化)。
那個 436 → 436,就是 bug 修掉的最直接證據。
單 process 多輪實測:理論與真實對齊
理論是:「確定性轉換在同一個 process 內不會破壞快取」。 M7 proxy 跑起來後,餵 2 輪 Claude Code session: Turn 1 請模型記住數字 42,Turn 2 問那個數字是什麼。
Turn 1:cache_read: 0、cache_creation: 63719(建立快取)。
Turn 2:cache_read: 63719、cache_creation: 412(100% 前綴命中)。
proxy 觀測線的兩輪 bytes 計數完全一致(in == out),確認 proxy 是 byte-faithful 穿透。
成本 $0.088、耗時 6 秒。
這也帶出一個副洞察:真實 Claude Code 流量,引擎多半是 no-op passthrough—— Claude Code 本身就送規範化請求、自帶 cache_control, stabilize 和 compress 步驟都不觸發。 M8 的 lazy 設計確保這種情況下工具不被掛上去,proxy 完全透明。
關鍵教訓
決定往上推一層,building block 就能保持純粹:register_ccr_tool 本身無罪,有罪的是「永遠呼叫它」的決定。把「何時呼叫」上移到 orchestration,building block 的單元測試不用動,proxy 層也不用知道細節。
工具放尾端,divergence point 往後推:prefix cache 從頭比,插前面從第一個 byte 就分岔。尾端插入讓現有工具維持 byte-identical,快取保住更多。
parity gate 用數字說話:436 → 436(不 436 → 775)這個數字對比,比任何文字描述都更直接說明 bug 修掉了。測試的輸出設計成可閱讀的數字差異,code review 時一眼就看懂。
in-process 多輪 vs cross-process 是兩個威脅模型:前者靠確定性轉換保快取,後者靠不動 tools 保前綴容錯。分開驗、分開設計,不要用同一個實驗同時證兩件事。