在 Claude Code 執行 npm test 或 pytest,有時候難處不是測試失敗,而是那個失敗被幾百行 PASS、編譯訊息和警告蓋掉。Agent 可能為了找錯誤再跑一次指令,或重新讀整份日誌。只留下最後幾十行也未必安全,因為真正的例外可能出現在前面。

我在 Hacker News 的 Show HN 看到 Context Diet。它透過 Claude Code 的 Mod Hook,把很長的工具輸出改成較短的摘要,同時留一個完整原文的檔案路徑。我原本很喜歡這個方向:給 Agent 一份可回讀的索引,總比讓它一直消耗上下文好。

但看到 README 寫著「every failure」時,我有點猶豫。摘要既然有字元預算,遇到很多彼此不同的錯誤,怎麼可能保證一個都不漏?因此我把版本固定在 v0.3.0、commit 689284a,查看 Hook、壓縮器與測試原始碼,再用 Node.js 對通用日誌壓縮邏輯做人工合成測試。

先講清楚證據範圍:我沒有在 Claude Code 裡安裝執行 Context Diet Mod,也沒有做 Claude 模型 A/B 測試。 文中的數字只證明從原始碼改寫的壓縮演算法,在刻意設計的輸入下會如何取捨,不能當作外掛在真實專案的錯誤遺漏率或 Token 節省率。

它縮短的是模型看到的工具回傳

固定版本的 session.append Hook 會處理符合條件的 Bash、Grep、Glob 及 JSON 類 MCP 輸出。Read、Edit、Write、WebFetch 和非 JSON 的 MCP 文字不走這條裁剪路徑。預設觸發門檻是 8,000 個字元,不是 8,000 Token。

它不是對所有輸出一律執行 tail。識別 Jest、Vitest、pytest、Go test、Cargo、TypeScript 檢查器等格式時,會優先摘要失敗訊息、位置與上下文,折疊通過的測試;JSON 留結構骨架,Diff、Grep 和檔案清單也有各自的處理方式。這些都能在固定原始碼讀到,不代表我已經在 Claude Code 實際跑過每一種框架。

我認為更重要的設計是保存原文。Hook 會先嘗試把完整輸出寫到 .context-diet/out-NNN.txt,再把可回讀的路徑附在摘要上。檔名使用 300 個輪替槽位;儲存失敗時會直接放行原始結果,不會給 Agent 一個其實不存在的檔案。

但「有存到本機」不等於「永久保存」。舊槽位會被覆寫,日誌裡的金鑰、環境變數和專案路徑也可能留在磁碟。.gitignore 可以降低誤提交風險,並不是加密或備份方案。重要的正式環境日誌仍應另外保存。

錯誤優先,不表示所有錯誤都放得下

我直接看了diet.ts 的通用日誌壓縮器。它先折疊重複的行,替開頭與結尾預留空間,再從後面往前找錯誤區塊,盡量帶上附近的上下文。

關鍵是這段預算判斷:

const cost = window.reduce((n, i) => n + (folded[i]?.text.length ?? 0) + 1, 0)
if (used + cost > target) continue

當某段錯誤和上下文放不進剩餘預算,就跳過。只有一個錯誤時,排序通常很實用;如果同時有幾十種不同的失敗,還帶著長堆疊,摘要自然無法裝下全部。

Jest、pytest 等明確識別的工具會走另一套專用解析邏輯,同樣受到預算限制。下面的測試只針對未識別格式的通用日誌回退路徑,沒有測專用 Jest/pytest 解析器的實際錯誤保留率。 不應把通用日誌的比例套到真實的 Jest 測試。

所以 README 的「every failure」比較可靠的工程理解,是「盡可能讓失敗資訊優先顯示,而且能回讀原文」,而不是「無論多少錯誤,都保證完整顯示在一份有限長度的摘要」。

人工造一個錯誤,再造 145 個

這次沒有為了一個長度預算去安裝整套 Claude Code。我把固定原始碼中的通用 dietLog 與相關文字函式手動轉成不依賴第三方套件的 JavaScript,透過隔離環境的 Node.js v22.16.0 執行。較完整的合成測試 4/4 通過,退出碼為 0;這不是官方 Mod 整合測試。

人工測試輸入原文長度摘要長度實際觀察
長日誌中間只有一條關鍵錯誤32,087 字元1,175 字元1/1 個錯誤標記保留
145 條不同錯誤,摘要預算 1,450 字元23,146 字元1,419 字元7/145 條原始錯誤行可見
很短的輸出45 字元45 字元原樣保留
PASS errorHandler.test.ts 分類對照52 字元52 字元通過訊息未被誤判成錯誤

第二組裡有 138 條原始錯誤行沒有出現在摘要中。以下是實際保存的人工合成摘要片段,不是真實專案發生過的錯誤:

… [225 lines cut] …
ERROR CASEei unhandled category_ts in /app/modules/amy/workflow.ts:40 expected stable result bci
ERROR CASEej unhandled category_tt in /app/modules/amz/workflow.ts:40 expected stable result bcj
ERROR CASEek unhandled category_tu in /app/modules/ana/workflow.ts:40 expected stable result bck
…
… [79 lines cut] …

這裡還有個容易看錯的數字:壓縮器的說明寫「5 of 145 error lines with context」,但數整份摘要裡實際可見的原始錯誤行,是 7 條。因為開頭或結尾保留下來的內容,本來就可能含有錯誤。特別挑選的「錯誤視窗數」不等於「最後可見的錯誤行數」,不能混為一談。

1,450 字元是刻意縮小的實驗預算,並不是外掛預設值。 在固定的 v0.3.0 裡,預設觸發門檻為 8,000 字元,普通長輸出的摘要目標是 Math.max(2_000, Math.round(threshold / 2)),也就是 4,000 字元;如果 Claude Code 已經把輸出存成檔案,摘要目標則是 3,000 字元。預算增加通常能容納更多內容,但不能據此算出真實外掛的錯誤遺漏率。

為了方便獨立重跑,我也做了一份只用 Node.js 標準函式庫的兩項測試。它使用比較長的合成錯誤行,同樣設定 1,450 字元預算,保留的是 8/145 條,而不是前面較完整適配測試的七條。兩項測試 2/2 通過;兩組夾具的輸入不同,不能把它們當成效能比較。

以下是已核對的完整最小重放程式。儲存成 budget-replay.test.mjs,執行 node --test budget-replay.test.mjs:

/**
 * First-party negative control for Context Diet v0.3.0's generic dietLog budget path.
 * Pin: drkokorev/context-diet @ 689284a893f1aa509b9c6c1c06b91ef1ca52279c.
 * Only the error-window budget logic is mirrored, for controlled ASCII log lines.
 * This is NOT an upstream Mod invocation, a benchmark, or a production loss rate.
 * The full adapted dietLog fixture and observed digests are in the separate evidence bundle.
 */
import assert from 'node:assert/strict';
import test from 'node:test';

const id = i => { let s='',n=i+1; while(n){ n--;s=String.fromCharCode(97+n%26)+s;n=Math.floor(n/26); } return s; };
const noise = n => Array.from({length:n},(_,i)=>'step '+id(i)+' completed '+id(i+1200)+' '+i);
const critical = line => /^ERROR\b|^\s+(Expected|Received):/.test(line);
function select(raw,target){
  const folded=raw.split('\n').map(text=>({text,level:critical(text)?2:0}));
  const errorHits=folded.map((l,i)=>l.level===2?i:-1).filter(i=>i>=0);
  const keep=new Set();let used=0;
  const headBudget=target*(errorHits.length?0.1:0.2);
  const tailBudget=target*(errorHits.length?0.25:0.35);
  for(let i=0;i<folded.length&&used<headBudget;i++){keep.add(i);used+=folded[i].text.length+1;}
  let tailUsed=0;
  for(let i=folded.length-1;i>=0&&tailUsed<tailBudget;i--){
    if(!keep.has(i)){keep.add(i);tailUsed+=folded[i].text.length+1;}
  }
  used+=tailUsed;
  let windowsShown=0;
  for(let h=errorHits.length-1;h>=0;h--){
    const at=errorHits[h];const window=[];
    for(let i=at-2;i<=at+3;i++)if(i>=0&&i<folded.length&&!keep.has(i))window.push(i);
    const cost=window.reduce((n,i)=>n+folded[i].text.length+1,0);
    if(used+cost>target)continue; // EXACT budget guard from upstream dietLog.take()
    window.forEach(i=>keep.add(i));used+=cost;windowsShown++;
  }
  return {visible:folded.filter((_,i)=>keep.has(i)).map(l=>l.text).join('\n'),totalCritical:errorHits.length,windowsShown};
}
test('single late/central failure remains visible',()=>{
  const text=[...noise(310),'ERROR ECASEONE impossible transaction transition','  Expected: completed','  Received: pending',...noise(310)].join('\n');
  const out=select(text,2000);
  assert.ok(out.visible.includes('ECASEONE'));
});
test('many distinct failures cannot all fit finite digest budget',()=>{
  const failures=Array.from({length:145},(_,i)=>'ERROR CASE'+id(i)+' unhandled category_'+id(i+400)+' in /app/modules/'+id(i+900)+'/workflow.ts:40 expected stable result '+id(i+1300));
  const raw=[...noise(90),...failures,...noise(90)].join('\n');
  const out=select(raw,1450);
  const retained=failures.filter(line=>out.visible.includes(line)).length;
  assert.ok(retained>0&&retained<failures.length);
  process.stdout.write(JSON.stringify({failure_lines:failures.length,retained,missing:failures.length-retained,digest_budget_chars:1450,mode:'source-budget-window-control-not-plugin-runtime'})+'\n');
});

這個小程式只重現通用日誌的預算選擇分支,沒有呼叫 Claude Code 的 Mod API。測試斷言成功,不代表原外掛已完成任何真實執行或能讓 Agent 正確診斷。

這個反例沒有讓我否定壓縮;反而提醒我,摘要最好始終是通往原始證據的索引,而不是證據本身。

先試 dry,capture 要另外評估

如果你使用支援 Mod 的 Claude Code,可以先讀原作者固定版本的安裝說明,再比對目前用戶端文件。當時 README 要求 Claude Code 2.1.287+;我沒有在真實用戶端驗證安裝。

/plugin marketplace add drkokorev/context-diet
/plugin install context-diet@context-diet

比較穩妥的起點是先開 /diet dry,跑平常容易產生大量輸出的測試,再用 /diet report 觀察原本可能裁掉多少。dry 的設計是只統計、不替換模型看見的原始輸出。若某個指令的完整日誌必須保留供稽核,可使用 /diet keep <命令片段>,或 /diet off。

/diet capture on 則是另一件事。Claude Code 本身可能先縮短過長的結果,讓 Hook 沒看到全部文字。capture 選項預設關閉;對部分簡單的測試或建置指令,它會包裝 Bash 指令,把 stdout/stderr 合併寫入 .context-diet/run-NN.log,再回讀並嘗試還原退出碼。

包裝指令可能讓原本 Bash(pytest:*) 之類的權限白名單無法直接匹配,因此跳出新的授權提示;合併輸出流也可能改變閱讀順序。管線、多個指令、原本就有重定向,以及 watch/background 模式,都不在自動 capture 處理範圍。我不建議在高度依賴權限規則或即時終端輸出的工作流程中直接打開;用完可以 /diet capture off。

作者 README 中的數字是:七道問答題在兩組都答對,工具呼叫總數由 13 降到 8;另外共 18 次 Agent 修復執行中,平均呼叫次數是 8.0(未使用)/6.0(使用)/5.8(加 capture)。這些是作者自己的小樣本彙總,不是我重新執行並確認的模型成本或正確率。

如果你的工作階段常被大量通過記錄塞滿,Context Diet 值得評估。但遇到事故鑑識、複雜並行錯誤或自動合併 PR,摘要只能拿來定位,做結論前還是應回讀完整日誌。至於 Agent 會不會真的回讀、還是只修畫面上看得到的幾個錯誤就提交,那需要另外做行為測試,不能由這次合成實驗推論。

之前我也寫過 CLAUDE.md / AGENTS.md / Skills 的指令分層和 Agent 編排的狀態交接。那兩篇在處理「該載入什麼指令」與「誰負責任務」;這次更小,但可能更直接:模型面前這份工具回傳,究竟是完整證據,還是一張附了原文路徑的便條?