Agent Prefix Cache 驗證最容易犯的錯,不是公式寫錯,而是 simulator 看起來能跑、數字也很漂亮,卻沒有真的實作被比較的政策。2026 年 9 月,一個以 393 個 Claude Code sessions、68,266 筆 model requests 做 corpus characterization,再取前 40 sessions、4,751 筆 requests 比較 LRU、LFU、TTL 與 session-aware eviction 的開源實驗登上 Hacker News 討論;我們把它固定在 commit 73e7e468,從 cold checkout 跑完預設流程,再逐行追 metric 與 eviction path。結果最值得學的不是「LRU 贏了」,而是兩個足以推翻排行榜的 harness 問題:TTL 類別沒有任何過期邏輯,而且所謂 recompute 把第一次出現的新 suffix 也算成淘汰後重算。
這篇會帶你從零分清單次推論 KV、跨請求 Prefix Cache 與 provider TTL,下載並核對 68,266-request trace,跑 baseline、寫出會失敗的 TTL/pinning 測試,再把容量掃描、分母與不確定性補齊。你不需要 GPU;目前的 repo 是離散事件模擬器。若你先前讀過 Agentic Workload Prefix Cache trace 教學或跨 Worker KV Cache 維運,本文刻意不重複概念,而是專攻「怎麼證明一個快取 benchmark 值得信」。
先說結論:能重跑,不等於通過 Agent Prefix Cache 驗證
🧭 記憶把手:可信的 Agent Prefix Cache 驗證=固定資料 × 正確政策 × 正確分母 × 會抓錯的 oracle。
- 可以保留:公開資料確實有 393 個 traces、68,266 筆 requests;固定 commit 後,
make setup data repro可以跑完既定腳本。 - 必須限縮:Mooncake 的 saturation shape 相似,但六個容量點的絕對 hit rate 仍高出論文表格 4.1–6.1 個百分點,不能稱為完整對齊。
- 必須否決:
TTL-300s只是改名字的 LRU;33.1% 與 17.5% 是 prefix-miss token 的 gap 分布,不是 eviction-caused recomputation。 - 最後判決:目前程式只顯示「這個特定 harness 的 LRU 輸出最高」,沒有證明 LRU 普遍優於 LFU、TTL、session-aware policy 或相關研究系統。
這個轉折正是好教材:一個 null result 只有在 baseline、反例測試與 metric 都過關時,才有資格變成工程結論。想先補齊整體 Agent 系統的控制面,可以搭配 AI Agent Harness 白話指南與如何建立 Agent Harness。
第一步:先分清 KV Cache、Prefix Cache、TTL
三個名字常被混在同一句話裡,實際回答的是不同問題:
- 單次推論 KV state:同一個 generation 內,decode 新 token 時重用前面 token 的 key/value,避免每一步把整段 prompt 再算一次。生命週期通常跟著這次 inference。
- 跨請求 Prefix Cache:下一個 request 若有相容且連續的共同前綴,就重用先前保留的 KV blocks。容量不足時才需要 admission/eviction policy。
- Provider TTL:一段前綴即使還有空間,也可能因時間條件失效。TTL 與容量淘汰是兩個軸,實驗不該把其中一個冒充另一個。

以 Claude API 為例,Anthropic Prompt Caching 官方文件把它描述成跨請求重用完全相容的 prompt prefix;順序是 tools → system → messages,usage 會分成 cache_creation_input_tokens、cache_read_input_tokens與未快取的 input_tokens。官方文件列出預設五分鐘的最低生命週期、讀取會刷新它,另有一小時選項;截至 2026 年 9 月 16 日,這份文件未說明內部容量、admission、router affinity 或究竟採用哪種 eviction policy。
因此,API 回傳 cache_read_input_tokens = 0只能證明這次沒有讀到可用 entry,不能單靠一次 miss 推論「Anthropic 使用 LRU」。原因還可能是第一次請求、前綴或設定變動、低於模型門檻、TTL、路由或其他不可見條件。支援的環境可再用 Cache Diagnostics找第一個 model/system/tools/messages divergence;它仍不能揭露 provider 的容量淘汰演算法。
第二步:固定 68,266-request trace 的真正邊界
AgentX 資料集固定版本包含 393 個 traces、28,444 個 main turns、1,697 個 subagent groups 與 39,822 筆 subagent inner requests,合計 68,266 筆 model requests。輸入以 64-token block hash 表示,而且 hash scope 是 session-local;它能保存同一 session 內的前綴關係,卻刻意看不到跨 session 共用的 system/tool prefix。
這也不是「所有 Claude Code 使用者」的隨機樣本。資料卡寫明使用 top sampling、每個 session 至少 20 筆 Anthropic requests、排除圖片與 classifier calls,並移除超過 256k 上限的個別請求。它適合問「這批長而活躍的 sessions 在某個 simulator 裡會怎樣」,不適合外推整體使用者分布。
39,822 筆 subagent request 不能悄悄攤平
原 repo 的 experiments/prep.py會遞迴取出巢狀 subagent requests,全部塞進同一個 list,再依 timestamp 排序。這個處理讓 simulator 比較簡單,卻改變了 workload 語意:39,822 筆 subagent calls 佔全部 requests 的 58.3%,其中不少原本是重疊執行。相對地,AIPerf 官方 WEKA replay 文件保留 nested topology,透過 SPAWN/SPAWN_JOIN 把 subagent 當成獨立且可並行的 child sessions。
所以腳本在排除 gap ≤ 0後印出的 positive、non-overlapping inter-request gap 中位數 2.1 秒,只能當這個 flattened event stream 的條件描述值,不能直接翻譯成「大多數 miss 都來自兩秒工具迴圈」。你必須先標記 main turn、subagent child、overlap 與負 gap,才能判斷是 sequential tool loop 還是 concurrent branch。
第三步:從 cold checkout 跑完,但先別相信排行榜
先固定 repo 的完整 SHA,不要只追 main。截至本文查核時,repository 只有兩個 commits、沒有 tag、release、CI 或 test suite;資料下載腳本又指向可變動的 GitHub main與 Hugging Face resolve/main。真正可比較的 run manifest 至少要記 code SHA、每個 source object 的 SHA-256、Python 版本、session subset、seed、arrival window 與容量單位。
git clone https://github.com/gauravapiscean/agentic-kv-cache.git
cd agentic-kv-cache
git checkout --detach 73e7e46807d007726e69c636deb2792439850757
make setup
make data
make repro
本次 cold checkout 下載並整理資料後,make data寫出 393 sessions、68,266 requests。make repro接著做四件事:跑 Mooncake baseline、用全部 393 sessions 做描述統計、取資料順序前 120 sessions 做 gap 分析,再取前 40 sessions(4,751 requests)跑三個容量點的 policy ablation。後兩項不是 68,266 筆全量 policy comparison,也沒有 holdout、bootstrap 或多個 seeds。
預設 ablation 把 8,000、20,000、50,000 個 blocks 分別當成 512k、1.28M、3.2M token capacity,印出的 LRU hit rate 是 83.48%、93.92%、95.76%。這些數字只描述固定 seed、兩小時 arrival window、session-local hash 與目前程式碼下的輸出;接下來五道 gate 決定它們能不能變成結論。
Agent Prefix Cache 驗證的五道 Gate
Gate 1|Data:資料數量對了,事件拓撲也要對
驗收不能只 assert len(requests) == 68266。另外保存每個 request 的 parent、child session、arrival、finish、overlap group 與原始 dataset row;preprocessing 後逐項核對。若研究問題是「session-aware policy」,把所有 child calls 歸進 outer session 會直接改變 policy 的資訊來源。
Gate 2|Baseline:無限容量也對不上,就不是 eviction 問題
repo 把 Mooncake arXiv 表格的 LRU curve 當 baseline。cold run 的六個點都與 checked-in output 相同,但 measured hit rate 比表格高 4.1–6.1 個百分點;連近似 infinite-capacity 也高 4.3 個百分點。無限容量不會因 LRU victim selection 產生差異,所以這個 offset 應先追 trace version、block construction 或 denominator,而不是調 policy。
Mooncake 的 FAST’25 trace 說明也把 updated release traces 與 historical arXiv trace 分開;本次從公開 historical trace 算得平均 input length 為 8,589.96 tokens,而arXiv v4描述的是 7,590 tokens,相差約 13%。這證明 trace distribution 不完全一致,但不能單憑這項差異認定它就是 hit-rate offset 的原因。安全說法是「saturation trend 類似,但有未解的系統性 offset」,不是「完整對齊」。
Gate 3|Policy:先用 301 秒的兩筆 request 打臉 TTL
目前 TTL類別的完整實作只有繼承 LRULeaf、保存 self.ttl並改顯示名稱;沒有任何 lookup、timer 或 expiry path 讀取這個值。難怪 TTL-300s和 LRU byte-identical:它們目前就是同一種行為。
修政策前先寫最小 failing test。兩筆完全相同的 prefix 相隔 301 秒,容量又足夠;真正的 300 秒 TTL 應該兩次都 miss,因此整體 hit ratio 必須是 0。現行程式會得到 0.5:
# 以 PYTHONPATH=src 執行
from sim import Sim, TTL
events = [
(0.0, 0, ["a", "b"], 128, 0.0),
(301.0, 0, ["a", "b"], 128, 0.0),
]
r = Sim(events, 10, lambda s: TTL(s, 300), 64).run()
assert r["hit"] == 0.0, r # 現行 commit 會在這裡失敗
正確模型要把 TTL expiry 與 capacity eviction 分開記原因;如果要模擬「讀取刷新 TTL」,也要把 refresh 寫進事件規格。完成後至少掃 299、300、301 秒,並在容量很大與很小的兩個 regime 各跑一次。
Gate 4|Metric:cold/new suffix 不是 eviction recompute
gap 腳本用 (len(chain) - longest_prefix_hit) × 64計算「recompute」。這會把第一次 request、剛新增的 user/tool suffix、真正被淘汰的舊 blocks 全混在一起;而且 cache 明確使用 LRU,所以也不是 policy-independent。
最簡單的修法是同時跑 finite 與 capacity-infinite control。對同一個 request,infinite cache 能命中的最長前綴代表「歷史上曾出現且仍相容」;finite cache 比它少命中的部分,才是容量造成的 prefix deficit:
finite_k = finite_cache.lp(chain)
infinite_k = infinite_cache.lp(chain)
eviction_recompute_blocks = max(0, infinite_k - finite_k)
cold_or_new_suffix_blocks = len(chain) - infinite_k
此外要把 first request、負 gap/overlap 與一般正 gap分開。原圖六個 buckets 只占總 denominator 約 78%,因為未分類項仍留在分母。修正後同時報 request hit、token-weighted prefix hit、eviction-recompute tokens、cold/new tokens、occupancy 與 write churn,讀者才知道政策究竟改善哪一種成本。
Gate 5|Oracle:會跑的 Belady,不等於 exact OPT
repo README 提到早期 harness 曾讓 Belady 輸給 LRU,原因是插入長 chain 時把正在建構的 prefix 自己淘汰。現行 Cache.insert()確實在一次同步 insertion transaction 內把整條 chain 放進 pinned,能阻止這種 self-eviction;但 repository 沒保留 bug 前版本或對應測試,因此我們不能宣稱重新觀察到那次歷史失敗。
更重要的是,現行 pin 在 insert 結束就清掉;api_time沒有讓仍在執行的 request 持續持有 refcount。遇到重疊 subagents,下一個 arrival 仍可能淘汰上一個尚未 finish 的 blocks。Belady 類別也只從最多 256 個 LRU-tail leaves 挑候選,且沒有出現在任何 checked-in experiment;它是 heuristic reference,不是 variable-size radix cache 的完整 OPT 證明。
比較新 policy 前,先建立一組答案已知的 tiny traces,逐項驗證:active refcount 大於 0 的 block 不可被淘汰、hit 必須 prefix-contiguous、無限容量時 capacity eviction 為 0、TTL boundary 正確、每個 victim 都是當下合法 leaf。這種 oracle test 的價值,高過再多跑一個 68k trace。

第四步:把 policy sweep 改成真正可比較的實驗
修完五道 gate,再比較政策。不要只把名稱塞進同一個 loop;每個 arm 都要有操作定義:
- LRU:在合法 radix leaves 中,以最後一次可觀測存取時間選 victim。
- LFU with aging:明確定義計數範圍、aging/decay、evict 後是否保留歷史;現行程式只 touch deepest hit,會低估日後變成 leaf 的熱門 ancestors,不能代表一般 LFU。
- TTL+LRU:TTL 負責時間失效,LRU 負責容量壓力,miss reason 分開記帳。
- Admission+eviction:例如 frequency sketch 先決定新 block 是否值得進 cache,再由 LRU/LFU 選 victim,避免把兩個決策混成一個分數。
- Session-aware:具體寫成 active-request refcount、soft priority、inactive branch aging 或已知 workflow step;「懂 session」不是一個標準演算法。
容量不要只寫 blocks
AgentX 的一個 block 是 64 tokens,Mooncake 是 512 tokens;直接把「50,000 blocks」放在同一張圖會誤導。至少同時報 token capacity,若要談部署成本,再用模型層數、KV heads、head dimension、dtype 與 allocator overhead 換成實際 bytes。容量 sweep 要一路延伸到三種區域:嚴重 capacity-bound、轉折區、容量寬鬆到 TTL 有機會成為限制。
切分、seed 與不確定性
- 以完整 session 做 train/tune/holdout,不能用前 40 個 sessions 調 policy 又報同一批。
- 至少跑多個 arrival seeds 與 window,保存每個 seed 的結果,而不是只給平均。
- 同時報 request-weighted、token-weighted 與每-session 等權分布,再用 session-cluster bootstrap 建 confidence interval、做 leave-one-session-out influence check;如此才能看見一個 3,551-request 長 session 是否主宰 point estimate。
- 保留原始 nested topology;另做 flattened ablation,量出 preprocessing 本身改了多少。
- 先用 simulator 篩選,再到實際 serving engine 測 TTFT、queueing、JCT、throughput 與 GPU memory;hit rate 只代表 prefill reuse,不等於端到端速度。
若你接下來要把 simulator 連到真實 serving,先閱讀 KV Cache 隨機淘汰評測理解 within-request 與 cross-request 指標邊界,再回到跨 Worker routing/tiering 教學補上多 worker placement。想控制 API prompt reuse 成本,則可搭配Claude 省 token 方法,但不要把 provider usage fields 當成內部 LRU 的直接觀測窗。
原 repo 的數字,現在可以怎麼讀?
- 68,266/393:是來源 corpus 與 characterization 的規模;policy ablation 實際是前 40 sessions、4,751 requests。
- positive-gap 中位數 2.1 秒:腳本先排除
gap ≤ 0,再對 flatten 後 event stream 計算;它不足以判定 sequential tool loop 或 eviction cause。 - LRU 83.48% → 95.76%:是三個指定容量下、這個 harness 的 token-block hit rate,適合當回歸快照,不是 production SLA。
- TTL 與 LRU 完全相同:目前只證明兩者的執行路徑相同,不能證明容量一定比 TTL 更早淘汰。
- LFU 大幅落後:只適用於這個不 aging、只 touch deepest hit、跨 eviction 保留歷史 count 的 LFU variant。
- 33.1%/17.5%:最多只能稱為目前 LRU simulator 的 total prefix-miss tokens 依 preceding gap 分桶;修正 compulsory miss 與漏列 bucket 前,不用它支持容量/TTL 因果。
這種寫法不是把研究「全部否定」,而是把每個數字綁回它真正量到的東西。透明地公開 code 與 trace 已經讓錯誤可被找到;下一步是把這些反例變成 tests,讓未來的 policy 不會再靠名稱通過。
新手版實作清單:一個週末完成最小可信版本
- 先鎖版本:code SHA、trace revision、object SHA-256、Python 與所有參數寫進 run manifest。
- 先寫五個 tests:TTL boundary、active pin、prefix contiguity、infinite capacity、合法 leaf victim;紅燈時不准跑排行榜。
- 重做 metric ledger:cold/new、capacity eviction、TTL expiry、admission reject、prompt invalidation 各自有 counter。
- 保存 topology:main/subagent parent-child、arrival/finish 與 overlap 都留著;flatten 只能當一個明示的 ablation。
- 掃完整 regime:多個容量、seeds、arrival windows、session holdout,報分布而不是單點。
- 最後才接 GPU:只把通過 simulator oracle 的 policies 帶到引擎 A/B,量 prefill、TTFT、JCT、throughput 與實際 memory。
如果你正在建立完整 Agent 開發能力,可以把這個 cache audit 當成一個 eval module,接進 AlphaLab AI 課程與實作路線裡的 harness、observability 與 deployment 練習;關鍵不是背「LRU 還是 LFU」,而是讓錯誤 policy 在小測試就被攔下。
常見問題 FAQ
1. Prompt Cache 跟 KV Cache 是不同技術嗎?
不是互斥關係。一般 KV state 描述一次 inference 內的 attention 狀態;provider 的 Prompt/Prefix Cache 則把相容 prefix 的 KV 表示保留到後續 requests 使用。差別主要在生命週期、比對規則與控制面。
2. Claude API 的 cache read 為 0,能證明被 LRU 淘汰嗎?
不能。0 read 也可能是 cold request、prefix/request setting 改變、低於門檻、TTL 或其他不可見原因;截至 2026 年 9 月 16 日,Anthropic Prompt Caching 官方文件未說明內部 eviction policy。
3. 為什麼一定要跑 infinite-capacity control?
它把「以前從未出現的新 suffix」與「曾存在但因容量消失」分開。若無限容量 baseline 自己就對不上公開數字,問題通常在 trace、hash、block construction 或 denominator,不在 victim policy。
4. TTL 跟 LRU 應該二選一嗎?
不應該。TTL 管時間失效,LRU 管容量壓力;常見設計是 TTL+LRU。實驗需要分別記錄是哪一個原因讓 entry 失效。
5. Hit rate 越高,使用者延遲一定越低嗎?
不一定。Prefix hit 主要省 prefill;decode 長度、queueing、KV 搬運、offload、network 與 tool latency 仍可能主導 TTFT 或整體 JCT。
6. 要怎麼測 in-flight pinning?
建立兩個重疊 requests:A 尚未 finish 時讓 B 觸發容量壓力,assert A 的 refcount blocks 不在 victim set;A finish 後再確認它們恢復可淘汰。只在同步 insert 期間 pin 不足以覆蓋這個情境。
7. Belady 輸給 LRU,就一定是 harness 壞掉嗎?
先看你實作的是不是同一目標下的 exact oracle。variable-size、shared-prefix radix cache 的全域 optimum 不是簡單的 flat-object Belady;若只抽 256 個候選,更是 heuristic。最安全的做法是在 tiny trace 上用 exhaustive oracle 比對。
8. 這次 Agent Prefix Cache 驗證能得出 LRU 最好嗎?
不能。它能得出的強結論是:原始 headline 所需的 TTL 與 recompute 證據目前沒有通過 gate;修正後仍要在多容量、多 seed、保留 topology 的 holdout trace 與真實 engine 上比較,才能說哪個 policy 適合哪個 regime。
接著閱讀
左右滑動查看更多推薦
結語:最好的 benchmark,先證明自己會失敗
68,266 筆真實 requests 很珍貴,但資料量不會自動修好政策或分母。這次最實用的產物,是把一句「LRU 很難打敗」改造成五道可執行的 gate:trace topology、baseline、TTL、metric、oracle。先讓 301 秒 TTL 測試失敗、讓 cold miss 離開 eviction ledger、讓 active request 真正被 pin,再掃容量與不確定性;到那時,不論最後是 LRU、LFU、admission 或 session-aware policy 勝出,結論才知道自己為什麼成立。






