Context Diet:幫 AI 的 Global Instruction 瘦身,別讓它只進不出
我們一直在加,但幾乎沒有人回頭看
只要是和 AI 協作的人,想必都有過同樣的疑問與思考:怎麼規範跟你並行工作的 agent。
現在每家工具給的答案長得都差不多:一個放常規的 instruction 檔(Claude Code 的 CLAUDE.md、Cursor 的 rules、Copilot 的 instructions),再加上數組可以隨用隨載的 skill。透過這些 Context 協調架構元件,把重複交代的事寫進去,讓 AI 每次都讀得到,讓 AI 盡可能「照著你的意思走」。
然後會開始看到很多這種文章:某個大神推出的 skill 超好用,這個 plugin 一定要裝,instruction 照這樣寫從此再也不會出錯,驚呆十萬人。
這些抓眼球、博注意的內容一再出現,總有幾個會打到你的需求(或是好像打到你的需求)。當然,我是站在鼓勵只要沒有傷害,感覺「可能有用」那就嘗試看看,我自己也裝了不少。
問題是,如果你從來沒有回頭看過,你的 instruction 檔案長度、skill 數量就是一直漲、一直漲、一直漲。
這次的清理我拆成兩篇:這一篇處理 instruction 檔,skill 清單留到下一篇。拆開來講的原因一是判斷方法不同,二是混在一起講會一直換主詞,分兩篇我想讀者也能有個相對良好的閱讀體驗。
連他們自己都刪掉了 80%
最近 Anthropic 在七月發了一篇 The new rules of context engineering。裡面講他們把 Claude Code 自己的 system prompt 刪掉超過 80%,coding evaluation 上沒有可觀測的損失。要注意原文的限定:這是針對 Claude Opus 5、Fable 5 這一代模型做的,不是一個放諸四海的比例。
這件事值得想一下:一樣的成果,但省掉大量的 token 消耗。 他們的解釋是新一代模型被過度約束了,與其給窮舉式的規則,不如給判斷空間,把細節挪到需要時才載入的地方。
我會推薦上面的文章值得一讀,你也可以想想,你看完文章的第一個念頭是什麼?
我會先想到的不是「那我也來砍」,而是我該怎麼衡量哪些是多的、該怎麼合理地減並確保不會影響健康(本來的邊界與效能)。
先分清楚有三層
要討論砍之前,先了解到底是想要「砍」什麼。跟 agent 對話的時候,它讀到的東西大致分三層:
| 層 | 誰寫的 | 使用者能不能改 | 什麼時候載入 |
|---|---|---|---|
| system prompt | 工具廠商 | 基本上不能(Claude Code 可以用 --append-system-prompt 追加,但不能取代) |
每次都在 |
instruction 檔(CLAUDE.md 那類) |
使用者 | 完全可以 | 每個 session 起點載入全文 |
| skill | 使用者或任何你找得到別人分享的 | 完全可以 | description 常駐,本體觸發才載入 |
這篇從頭到尾在處理的是中間那層,skill 相關的會在下一篇文章提及。第一層不是我們能動的,但它是所有判斷的基準線,還是要解釋這個名詞與定義。
System prompt 就是工具廠商預先寫好、定義這個 agent 是誰、能做什麼、該怎麼回話的那段指示。它通常不對外公開,但這次剛好有現成的可以看:Anthropic 那篇文章直接把 Claude Code 改版前後的其中一段貼了出來。舊版是這樣:
In code: default to writing no comments. Never write multi-paragraph docstrings or multi-line comment blocks — one short line max. Don’t create planning, decision, or analysis documents unless the user asks for them — work from conversation context, not intermediate files.
新版把整段換成一句:
Write code that reads like the surrounding code: match its comment density, naming, and idiom.
這個對照本身就是這篇的主題:舊版在窮舉規則,新版在給判斷空間。 這正是模型迭代帶來的優化結果,我們要如何配合最新的模型能力調整 Global 的 Instruction 檔案,減少過多的束縛?我們稍後會談到。
Instruction 在不同工具裡叫法不一樣,要認出它看一個主要特徵就夠:一份使用者端有權修改、而且每個 session 開始時都會被載入的指令參照文件。CLAUDE.md、Cursor 的 rules、Copilot 的 instructions 都是這樣運作。
上面 System Prompt 的舉例,可以剛好描述我想說的迭代,如果我們回頭翻自己的 instruction 檔,也曾寫過「不要亂加註解」「不要沒事生一堆計畫文件」這類交代,那現在它們早就在上游生效了,我們在下游再寫一次只是重複付錢:我們沒寫錯,只是它已經不需要我們來寫。
Instruction 什麼時候會被載入?官方文件寫得很明確:instruction 檔是「在每個 session 起點載入 context」(Memory)。也就是說,不管你這次要做什麼,它都整份在那裡。
什麼時候該做一次定期檢視?這其實沒有那麼容易回答,你會想有需求所以要加東西,但多數情況你不會想,啊,模型最近好像變強了,我是不是來幫檔案瘦身一下。所以它很容易只會單向成長。
第一件事是測量,而且要量到段落
要改善一件事,得先能觀測它。
事前量一次,動刀,事後再量一次。這就是最基本的科學概念,可重現且一致的標準評估。
怎麼量
/context 給你總量,但總量不能動刀,你得知道是哪一段吃掉的。所以再往下一步:把檔案按 markdown 標題切段,逐段估、排序,最貴的那幾段就是刀口。這件事叫 agent 寫個十幾行的腳本就有了,我的版本大概長這樣:
import re, sys, unicodedata
# 這兩個係數要自己校準,見下面第二點
WIDE, NARROW = 1.4, 0.56
def estimate(text):
wide = sum(1 for ch in text if unicodedata.east_asian_width(ch) in ('W', 'F'))
return wide * WIDE + (len(text) - wide) * NARROW
def sections(path):
lines = open(path, encoding='utf-8').read().split('\n')
cur, buf = '(前言)', []
for line in lines:
if re.match(r'^#{1,3} ', line):
if buf: yield cur, '\n'.join(buf)
cur, buf = line.strip(), [line]
else:
buf.append(line)
if buf: yield cur, '\n'.join(buf)
for path in sys.argv[1:]:
rows = [(estimate(body), name) for name, body in sections(path)]
for n, name in sorted(rows, reverse=True):
print(f'{n:7.0f} {name[:60]}')
print(f'{sum(n for n, _ in rows):7.0f} == {path}\n')
有兩件事值得先講。
一是被 import 進來的檔案要一起丟進去算。只量你打開的那個檔,會漏掉它 import 的整份內容,而那往往才是最大的一塊,後面 @ 那節會講為什麼。
二是任何自己寫的估算器,都要拿真值校準過才能信。這種按字元數推算的做法本來就是近似,我第一版的係數低估了整整一倍,而且是拿 /context 的數字回頭對才發現的。所以實際的流程是兩層:/context 給你可信的總量,腳本給你段落之間的相對比例,兩個一起看。只用腳本,你會對自己的檔案大小有錯誤的印象。
判準只有一條:拿掉它,行為會不會變
量完之後最容易做錯的事,是開始比賽誰砍得短,那是不是什麼 instruction 都不寫,什麼 skill 都不裝才是最短的?是,但這是不是我們要的?當然不是。(有個現成的案例:OpenAI 說自家模型為了在一個資安 benchmark 上拿到好成績,跳出了測試沙箱、入侵 Hugging Face 的正式環境去偷答案,研究者把這叫做 reward hacking。規範正確的評量標準與框架的重要性可見一斑。我們自己也會犯同一種錯,把「越短越好」當成目標之後,砍掉會改變行為的規則就變成一種成績)
我們的判準評量是這樣的:把這一行拿掉,agent 的預設行為會不會明顯不一樣? 不會的話它就是能被安全汰換的。照這條跑下來,每一段會落到三類。
留下:模型不會自己知道、而且對我來說是差異化的東西。像是不准把單一檔案養大、交付物不要用 emoji 字元改畫 SVG、UI 文案只放使用者需要知道的資訊。這些我重複糾正過,而重複本身就是訊號,一條規則如果需要使用者一再挑明,代表模型並沒有預設行為,那就是要規範的。
下放:只在特定情境有用、卻在所有情境載入的東西。分級表、格式範本、實作規格都屬於這類。不是沒價值,只是它們不需要每次都在場。這個做法有個名字叫漸進式揭露(progressive disclosure):常駐層只留路由,細節放在需要時才讀的位置。Anthropic 那篇講的「把細節挪到需要時才載入」就是同一件事,後面 @ 那個坑講的「有條件的指標」也是同一件事的另一個面向。
刪掉:已經被別的地方講過的東西;主要有兩個來源,一個是我自己在另一個檔已經寫過一次,一個是模型的 system prompt 現在寫得比我清楚。
這一遍是 agent 在掃,不是我
逐段量 token 判斷長度是腳本做的,但「拿掉它、行為會不會變」這個評估是 agent 做的,我負責給指令跟最後拍板。之所以由它來判斷是合理的,是因為它看得到我看不到的東西:它自己當下的 system prompt。 所以我問「這條規則拿掉,你的預設行為會不會不一樣」的時候,它是在對照而不是猜:像「不知道就直說」「存取不到某個來源要誠實說明」這幾條,它可以直接指出預設指示裡已經有了,這件事我自己沒辦法驗證。
這裡的不對稱值得講白:那份 system prompt 它讀得到,你讀不到。 它就在它的 context 裡,所以它能逐條比對;而你想自己拿到那份文字通常沒辦法,agent 也不會整份複印給你,那不是它的東西。所以「我這條規則是不是早就被預設涵蓋了」這個特定問題,剛好是委派出去比自己做有效的一種:它手上有你沒有的那份對照組。
但別把這個優勢當保證:它說「這條已經涵蓋了」的時候,可能是真的比對過,也可能只是讀起來很像。這次的估算一開始估計會刪得比最後刪的多:掃描階段它整段掃過去說「這類應該被預設涵蓋」,逐條看的時候才發現我的用字更具體。所以掃描跟驗證要分成兩趟,不要相信第一趟的分類。
我實際下的指令大概長這樣:
把這幾個檔逐段量 token。然後對每一段判斷三件事:一、拿掉它,你的預設行為會不會變;二、這段話是不是在別的檔案裡已經寫過,要真的去打開那個檔確認,不要用印象;三、它是不是只在特定情境才用得到。分成留下/下放/刪掉三類,每一類給我理由跟預估省下的 token。先不要改任何東西,我看過再決定。
最後那句粗體是重點。掃描跟動刀要分開,中間卡一個你確認的關卡。 否則你拿到的會是一份「已經幫你改好了」的結果,而你失去了判斷它砍對沒有的那個機會,甚至不知道它改了什麼、為什麼改。
兩個真的踩到的坑
@ 是一個字元,但它不是指標
我的 instruction 檔裡原本有一行寫著「細則見 @某個檔.md」。我當時的認知是「我留了一個指標」。
不是。在 Claude Code 的記憶檔裡,@某個檔.md 這個語法會把那個檔的全文貼進每一個 session。同一個路徑,不帶 @ 才是真正的指標,在有人去讀它之前不花任何成本。官方文件把我這個誤會直接寫成一句話:拆成 @path import 有助於整理,但不會減少 context,因為被 import 的檔案在啟動時就一起載入了(Memory)。
這一個字元的差別,可能等於你無意中多塞了一個檔案,指向檔案以減量載入的目的反倒無法達成,還是載入了完整的檔案。
改法很簡單:把 @ 拿掉,然後補一句觸發條件,「碰到某某情況先讀那個檔」。有條件的指標跟 import 一樣可靠,成本卻只剩那一行字。
有些東西值得每次讀取的代價
有一類規則跟其他規則不一樣:違反之後救不回來的那種。刪檔案、部署、動權限、花錢。
如果照「常駐層只留指標」的原則一路推到底,這些規則的細節也該下放。但這樣會出現一個很糟的狀態:禁止規則會在最需要它的那一刻缺席。 一個 agent 之所以會去做不該做的事,正是因為它不知道那件事不該做;它不知道,也就不會主動去讀那份寫著「不該做」的檔。
所以這裡的規則要反過來寫:後果不可逆的規則,它的觸發條件跟禁止清單留在常駐層,只有做法細節可以下放。
以刪除為例。我的常駐層留著的是「不要盲目刪,先搜尋、再刪明確的搜尋結果,沒結果就不刪任何東西」這個原則本身,因為它要在 agent 準備下手的那一刻就在場;至於怎麼列出受影響範圍、用什麼格式給我看,那些是做法,可以等要用的時候再讀。部署也是同一個道理,「沒有我明講不要部署」留常駐,部署流程本身不用。
事前估算刪除比例與差異
動刀前 AI 估計有四成內容可以被移除,實際做完是移除了三成出頭。
我有讓 AI 把它的判讀各個部分能被刪除的量與預估記錄下來。事後用腳本再量一次,兩個數字一對,就知道中間偏了多少、偏在哪一類。
比較後可以發現,主要差在 instruction 檔案中「溝通偏好」跟「安全操作」那兩節。掃描的時候高層次來看,乍看之下會覺得是不是有些被模型的預設 system prompt 涵蓋了,看進細節的時候才發現那些條目寫得比預設更具體。像是「先給結論再講理由」「我卡在選項之間時,不要只列 pros/cons,要點出真正的決策準則」這種,模型預設的 System Prompt 內的描述可能相對偏高層次。
原因可能是掃描階段做的是「這條讀起來像通則」的粗分類,當後面進去細節逐條看才是驗證;這樣第二次的細節驗證中,它把我從「多砍」推向「少砍」。反過來的偏差才危險:以為只會砍一點,結果砍掉了會改變行為的東西,那種要等到某天 agent 做錯事才會發現。
砍完的結果
最後的數字是常駐成本掉了三成出頭。實際刪掉的大概是這幾類,都出自 instruction 檔:
- 一個指向某個 repo 的說明,但那個 repo 自己的 instruction 檔已經寫了同一句話。
- 一整節收納規則,但它的權威版本本來就在另一個 repo 的 guideline 裡。
- 一段給人看的「為什麼把這條放這裡」的理由。常駐層不是留存這種資訊的地方,當初大概是順手就寫進去了。
- 幾條在講「不知道就直說」「查不到要說明」「不要捏造」的規則。這些現在的 system prompt 已經涵蓋,而且講得比那幾條更清楚。
壓縮的部分比刪除難講,但那才是省下來的大宗,所以也舉兩個:
- 一條規則原本連實作規格一起寫在常駐層(該設哪個 header、值要給多少),現在只留一句原則,後面接一個指向權威文件的路徑,真的要動手的時候才去讀那份。
- 一節寫得很細的判斷流程,壓成一句話加一個 skill 名字。判斷基準本來就完整寫在那支 skill 裡,常駐層重複一遍只是讓兩邊都要維護。
共通點:多數內容移除的原因都是重複,不是錯誤。 這些內容當初都是對的,只是後來在別的地方又被寫了一次。日常開發本來就會這樣疊加,區域的規則被判斷成通則、往上寫進全域,全域的規則發現沒那麼通用、再退回區域。這個雙向搬動本身就是知識在沉澱,問題出在搬動常常只做一半:新的地方寫好了,舊的地方沒清掉,同一件事就被載入兩次。
砍完到現在還沒出現行為退化,但照前面講的道理,這種事本來就需要一段時間才看得出來,所以我留著備份、也留意著。真要說收益,省下的 token 回頭看反倒是最小的那一塊,常駐層在整個 context window 裡佔比本來就不高。真正的收穫是另外三件:規則不再彼此重複打架、我重新知道那個檔案裡到底有什麼、以及留下來的每一條都通過了一次檢查。
寫在最後
所有的事都有前提:有一個 BUT,你得知道自己在用哪個等級的模型。
這有點像學生。比較吃力的學生需要比較多的書、比較多的參考、比較細的步驟,才能真的把事情做對;而天分好的學生你講一句「這個要這樣做」他就知道了,你把整本參考書塞給他,他反而被綁住。
模型也是這樣,而且它每隔幾個月就換一次等級。所以真正的問題比起「該不該砍」,要想的是怎麼知道這個學生現在已經懂了、不需要你再灌那麼多。這個問題沒有一勞永逸的答案,只能靠定期回頭量一次,正是這篇提出的方法論。
我覺得也許一到兩個月,或有新模型升級的時候,會是一個合適的回顧點;定期省思自己的流程與流程中被拿來用的參考與規範,時刻迭代,能幫助你在現在 AI 工具快速更新的環境下,保持自己的工作流程不被過時的規則影響。
附一支 skill 給你自己跑
如果你只想做一件事,那就先跑一次 /context,看一眼 memory files 那一列佔了多少。看完再決定要不要往下走。
這篇提到的檢查我整理成了一支 skill:steven-skill-deck / context-diet。你可以直接叫 agent 用它,讓它照著這幾條逐項檢查你的常駐檔:哪些是 import 而不是指標、哪些是不可逆禁令不能動、哪些條目拿掉之後行為不會變、逐段的 token 是多少。上面那段估算腳本的完整版也在裡面。
裝法跟其他 skill 一樣,把那個資料夾放進 ~/.claude/skills/ 就會被掃到,用 symlink 指過去比複製好,之後 git pull 就會生效。
這支 skill 自己也是照漸進式揭露組的:SKILL.md 只放共通流程,instruction 跟 skill 兩半的細則各自放在 references/,用到哪一半才讀哪一半,不然它自己就會變成這篇在講的那種問題。
它會先量、再分類、然後把提案攤給你看,等你點頭才動手,動手前也會先備份。我自己會定期拿它重跑一次,因為這個檔案是不會自己變瘦的。