Day21 拆完 compaction 的機制,本來要直接量它的影響。但有個很現實的問題:
我跑了 130 次執行,compaction 一次都沒觸發過。
原因是門檻太高。gpt-5.6-luna 的 context 視窗是 272K,預設保留 16,384 給回答,所以要到 255,616 tokens 才會壓縮。而我這些任務裡,單次請求最大的 context 只有 14,050 tokens——連視窗的 6% 都不到。
等一個自然觸發的長任務?那得設計一個要讀幾百個檔案的任務,成本和時間都不划算,而且會同時改變太多變數。
所以這次換個做法:把門檻搬下來,讓現有的任務就會觸發。
Day21 提過,compaction 的兩個參數都能在 .pi/settings.json 裡改。我在受測專案裡放了這個:
{
"compaction": {
"enabled": true,
"reserveTokens": 266000,
"keepRecentTokens": 2500
}
}
翻譯一下:
reserveTokens: 266000 → 觸發門檻變成 272,000 - 266,000 = 6,000 tokens。keepRecentTokens: 2500 → 壓縮時只保留最近 2,500 tokens 的訊息,其餘摘要掉。第二個參數是必要的。如果只降門檻卻保留 20K 近期訊息,切點會一路退到對話開頭,結果「沒有東西可以摘要」——壓縮會變成空轉。兩個參數必須一起調,這是設計這個實驗時踩到的第一個坑。
還有一個細節:專案層級的 .pi/settings.json 預設不會生效,因為 Pi 有「專案信任」機制(Day8 提過)。非互動模式要加 -a 才會載入。為了讓兩組條件只差一個變數,兩組都加了 -a,差別只在有沒有那個設定檔。
| 條件 | 成功 | 壓縮次數 | 成本中位數 | tokens 中位數 | 耗時中位數 |
|---|---|---|---|---|---|
| 不會觸發 | 5/5 | 每次 0 | $0.0078 | 79,237 | 57 秒 |
| 強制觸發 | 5/5 | 每次 1 | $0.0113 | 105,786 | 80 秒 |
壓縮確實被逼出來了,成功率也沒掉。看起來是一個乾淨的實驗。
但它其實沒有測到我要測的東西。
這件事是我在 Day29 寫 session replay 工具的時候發現的:把事件記錄重新 fold 成狀態之後,最終狀態的訊息數是 1。
一則。整場對話只剩一則訊息。
原因很簡單——壓縮事件是檔案裡的最後一筆記錄。五次執行,五次都是。
回頭看 harness 自己吐出來的事件串流,順序寫得清清楚楚:
turn_start × 8 → agent_end → compaction_start → compaction_end
agent_end 在壓縮之前。也就是說:
agent 把任務做完、回報完、結束了,Pi 才開始產生摘要。那份摘要沒有被任何一次模型請求用到。
而且門檻早就跨過了。看 r01 每一輪的 context 大小(input + 快取命中 + output):
| 第幾次請求 | context tokens | 超過 6,000 門檻? |
|---|---|---|
| 1 | 1,653 | 否 |
| 2 | 2,819 | 否 |
| 3 | 4,555 | 否 |
| 4 | 7,573 | 是 |
| 5–8 | 7,899 → 9,505 | 是 |
第四輪就過線了,後面還有四輪,壓縮一次都沒發生。
會不會是門檻還不夠低?我加跑一組:reserveTokens: 271000,門檻變成 1,000 tokens——第一輪就會過線。
| 結果 | |
|---|---|
| 執行次數 | 3(全部成功) |
| 每次的輪數 | 10、8、10 |
| 壓縮時的 context | 12,261 / 11,805 / 10,734 tokens(門檻的 11 倍) |
| 壓縮次數 | 每次 1 |
| 壓縮發生的位置 | 三次都是 agent_end 之後 |
整場對話在門檻十倍以上的地方跑了八到十輪,中間一次都沒壓縮。
所以結論是明確的:
在 Pi 0.84.3 的非互動
-p模式下,這 8 次執行裡,compaction 從來沒有在對話中間發生過——它發生在 agent 結束之後。
Day21 我寫了觸發條件 contextTokens > contextWindow - reserveTokens,但漏掉一個更重要的問題:這個條件什麼時候被檢查? 答案是:不是每一輪檢查。
(我試著在打包過的 bundle 裡追這段邏輯,只找到 shouldCompact() 這個純函式和一個 _runAutoCompaction("threshold") 的呼叫點,沒辦法百分之百確定完整的觸發路徑。所以上面那句話我只敢用量到的方式講:8 次執行、沒有一次在對話中間壓縮。)
量到一件很實際的事:產生一份摘要的價格。
| 中位數 | |
|---|---|
| 摘要那一次呼叫的成本 | $0.0018 |
| 摘要那一次呼叫的耗時 | 15~31 秒 |
| 佔整次執行成本的比例 | 約 16% |
那 80 秒 vs 57 秒的差距(+40%),幾乎就是這一段:agent 早就做完了,程序在那裡多跑十幾二十秒去寫一份沒人會讀的摘要。
更難堪的是,我第一次算出來的成本是 $0.0095,不是 $0.0113。
原因是:摘要那一次模型呼叫的 usage,掛在 compaction 那筆記錄上,不在任何一則 assistant 訊息上。
{"type": "compaction", "tokensBefore": 9505, "firstKeptEntryId": "6b7efc9d",
"usage": {"input": 4952, "output": 506, "totalTokens": 5458,
"cost": {"total": 0.0015976}}}
我的量測程式只走 assistant 訊息來加總 token 和成本,所以漏掉了整個壓縮的開銷——而這正好是這個實驗唯一真正要量的東西。
修好之後(bench/runner/metrics.py 把 compaction 記錄的 usage 也加進去),差距從 +22% 變成:
| 成本中位數 | 相對變化 | 95% CI(bootstrap) | 有把握? | |
|---|---|---|---|---|
| 修正前(漏算摘要) | $0.0095 | +22.0% | −16.6% ~ +53.0% | 否 |
| 修正後 | $0.0113 | +45.7% | +3.9% ~ +76.2% | 是 |
有意思的是對話本身的那 +22%($0.0078 → $0.0095)區間含 0,是雜訊;真正撐得住的那一段,全部來自那筆被我漏掉的摘要費用。
而我不需要重跑任何一次執行就能修正這件事——因為 session 都留著,重算一次就好。這是 Day29 要展開的主題。
這次的數字指向一個很具體的建議:
如果你在 CI、批次腳本、或任何「跑完就結束」的場景用 agent,compaction 對這一次執行毫無幫助,只會在最後多收你一筆摘要費。
它產生的摘要是留給下一次 session 的(--continue / --resume 的時候才有用)。一次性的 job 根本不會有下一次。
以這次的數字算:每個 job 多付約 16% 的成本、15 到 31 秒的時間,換到一份會被丟掉的摘要。在批次場景把 compaction.enabled 關掉,是一個可以直接省錢的設定。
反過來,在互動式的長對話裡,這筆錢是該付的——那份摘要就是下一輪請求的輸入。
-p 非互動模式。 互動模式(對話會繼續下去)很可能是另一回事,我沒有量。Day23 回到工具本身:跑了 130 次實驗之後,我發現我一直缺一把尺——沒辦法跨實驗比較、沒辦法分辨「有效果」和「運氣好」。那天會把量測台補成一個真正的評測框架。而今天這個「少算了一筆錢」的教訓,會在 Day29 變成一個獨立的工具。