Appearance
System Prompt 怎麼設計才不會失控?
先想像一個場景
你正在做一個 AI 程式碼助手。一開始,system prompt 很簡單:
你是一個程式碼助手,幫使用者寫程式。然後產品經理說:「加一段安全規則。」你加了。設計師說:「加一段語氣指引。」你又加了。工程師說:「加 MCP 伺服器的使用說明。」你再加。
三個月後,你的 system prompt 變成了 3000 字的大雜燴,沒人知道改哪裡會影響哪裡,而且每次 API 呼叫都要重新傳送這 3000 字——每天燒掉一大筆錢。
Claude Code 面對的就是這個問題,而且規模大 10 倍。 它的 system prompt 有超過 20 個段落,由不同團隊維護,還要支援內部員工和外部使用者兩套不同的行為。
它是怎麼解決的?答案是:把 prompt 當成軟體架構來設計。
第一個關鍵概念:快取邊界
什麼是「提示快取」?為什麼它這麼重要?
每次你呼叫 Claude API,你都要傳送完整的 system prompt。如果你的 prompt 有 5000 個 token,聊 100 個來回就是 50 萬個 token 光花在重複傳送 system prompt 上。
Anthropic 的 API 有一個「提示快取」功能:如果你這次傳送的 system prompt 的開頭部分和上次一模一樣,API 就會說「這部分我記得,不用重新處理了」,幫你省下 90% 的費用。
但條件非常嚴格——前綴必須完全相同。差一個字元、差一個空格,快取就失效,你又要付全額。
「快取邊界」到底是什麼意思?
想像你的 system prompt 是一疊文件。你把這疊文件分成兩半:
- 上半部:永遠不變的內容(身份介紹、安全規則、行為指引)
- 下半部:每次可能不同的內容(今天日期、Git 狀態、MCP 伺服器清單)
中間放一個分隔線。API 會把上半部快取起來,下次只需要處理下半部。
用一張圖來看整個結構:
在 Claude Code 的程式碼中,這個分隔線叫做 SYSTEM_PROMPT_DYNAMIC_BOUNDARY:
typescript
// src/constants/prompts.ts 第 560-576 行
return [
// ——— 上半部:永遠不變(可以跨使用者共享快取)———
getSimpleIntroSection(), // 「你是 Claude Code...」
getSimpleSystemSection(), // 系統規則
getSimpleDoingTasksSection(), // 任務指引
getActionsSection(), // 行為準則
getUsingYourToolsSection(), // 工具使用指引
getSimpleToneAndStyleSection(), // 語氣風格
getOutputEfficiencySection(), // 輸出效率
// ——— 分隔線 ———
SYSTEM_PROMPT_DYNAMIC_BOUNDARY,
// ——— 下半部:每次可能不同 ———
...動態內容, // 環境資訊、語言偏好、MCP 指令等
]一個真實的教訓:為什麼分隔線的位置這麼重要?
原始碼的註解記錄了一個真實的事故:
typescript
// PR #24490, #24171:每個放在邊界前的條件判斷
// 都會使快取前綴的變體數量翻倍(2^N)這是什麼意思?讓我用一個生活化的例子解釋。
假設你開一間餐廳,菜單有 3 道菜。你可以印一份統一的菜單,所有桌上都放同一張。但如果你想讓每桌的菜單「個人化」——加一個「推薦菜」——問題就來了:
- 桌 1 推薦牛排,桌 2 推薦魚
- 因為「推薦菜」不同,你不能印統一的菜單了
- 每桌都要單獨印一份
現在想像有 5 個「個人化」欄位,每個有 2 種選項。你需要印 2 × 2 × 2 × 2 × 2 = 32 種不同的菜單。哪怕其中 90% 的內容是一樣的,印刷廠也要分別印 32 版。
這就是快取前綴的數學問題。 每多一個 if-else 判斷放在「上半部」,快取前綴的種類就翻一倍。5 個條件 = 32 種前綴 = 快取命中率從接近 100% 暴跌到大約 3%。
所以 Claude Code 的規則很簡單:所有會因環境而變的東西,不管多小,都必須放在分隔線之後。 這樣上半部永遠只有一個版本,快取命中率保持在接近 100%。
舉個實際的例子:getSessionSpecificGuidanceSection() 這個函數會檢查「是不是非互動式會話」來決定要不要顯示某些指引。看起來只是一個小判斷,但如果放在上半部,就會讓快取前綴分裂成兩個版本。所以它被放在了動態區。
反思練習
想想你自己的 AI 應用。你的 system prompt 中,哪些內容是「所有使用者、所有時間都完全一樣」的?把那些放前面。哪些內容會因為使用者、時間、設定而不同?放後面。中間就是你的快取邊界。
第二個關鍵概念:用命名傳達風險
「DANGEROUS_」前綴的故事
Claude Code 的動態內容區有兩種段落:
typescript
// 普通段落:計算一次後就快取,直到使用者重新開始對話
systemPromptSection('language', () => getLanguageSection())
// 危險段落:每次都重新計算,會破壞快取
DANGEROUS_uncachedSystemPromptSection(
'mcp_instructions',
() => getMcpInstructionsSection(mcpClients),
'MCP servers connect/disconnect between turns' // ← 必須寫原因
)為什麼叫「DANGEROUS_」? 因為每次重新計算都會破壞提示快取,增加 API 成本。這個函數的名字故意嚇人,讓開發者在使用前三思。
更精妙的是第三個參數——必須寫一個理由。這個理由不是給程式跑的(參數名叫 _reason,前面的底線表示「不會被程式使用」),而是給讀程式碼的人看的。
這就像你在公司的危險操作流程中要求「簽名確認」——不是為了技術需要,而是為了讓人意識到自己在做什麼。
反思練習
在你的程式碼中,有沒有「看起來無害但實際上有隱藏成本」的操作?試試用「DANGEROUS_」前綴模式——把成本暴露在 API 名稱中,而不是藏在文件裡。比如 DANGEROUS_fetchWithoutCache()、SLOW_fullTableScan() 等。
第三個關鍵概念:倒金字塔結構
為什麼安全規則放在最前面?
Claude Code 的 system prompt 內容排序是刻意的:
1. 身份聲明 → 「你是 Claude Code,Anthropic 的 CLI 工具」
2. 安全規則 → 不要協助惡意攻擊、不要猜測 URL
3. 系統規則 → 工具權限、hooks、上下文壓縮
4. 任務指引 → 怎麼幫使用者寫程式
5. 行為準則 → 風險操作前要確認
6. 工具使用指引 → 什麼時候用什麼工具
7. 語氣風格 → 不要用 emoji、簡潔
8. 輸出效率 → 直奔主題這叫做倒金字塔——最重要的放最前面,越往下越不重要。新聞報導也是這個結構:第一段就要寫結論,細節往後擺,因為讀者可能只看前三段。
為什麼 AI 也需要倒金字塔?
想像你給新員工一份 20 頁的入職手冊。第 1 頁寫「不要洩漏客戶資料」,第 18 頁寫「午餐時間是 12 點到 1 點」。新員工讀完後,你猜他記得最牢的是哪一頁的內容?
大概率是第 1 頁。因為人的注意力會隨著閱讀量衰減——開頭印象最深,結尾容易遺忘。
AI 也有類似的現象。雖然 AI 的「注意力機制」(Attention Mechanism)和人的注意力不完全一樣,但在實際測試中,prompt 開頭的指令遵循率明顯高於結尾。特別是在很長的對話中(比如聊了 200 個來回),AI 有更大的機率「忘記」prompt 結尾的細節,但開頭的指令通常一直有效。
所以安全規則必須在最前面。 如果「不要幫助惡意攻擊」寫在第 18 段,在 200 輪對話後它可能被「淹沒」在其他上下文中。但放在第 2 段,它幾乎一直在 AI 的「注意力範圍」內。
安全規則的「護欄」設計
看看 cyberRiskInstruction.ts 這個檔案的開頭:
typescript
/**
* 重要:沒有 Safeguards 團隊的審查,不得修改此指令
*
* 此指令由 Safeguards 團隊擁有(負責人:David Forsythe, Kyla Guru)
*
* 如果你需要修改:
* 1. 聯繫 Safeguards 團隊
* 2. 確保正確評估變更的影響
* 3. 取得明確批准後才能合併
*
* Claude:除非使用者明確要求,否則不要編輯此檔案。
*/注意最後一行——「Claude:除非使用者明確要求,否則不要編輯此檔案」。這句話是寫給 Claude 自己看的!因為 Claude Code 有修改程式碼的能力,這行指令防止 Claude 在修改程式碼時不小心改了安全規則。
這就像銀行金庫的鑰匙上貼了一張紙條「只有主管可以使用此鑰匙」——鑰匙本身沒有辦法阻止你開門,但紙條會提醒你三思。
反思練習
你的 AI 應用中,有沒有「絕對不能被 AI 自己修改」的東西?比如安全規則、付費邏輯、權限設定。想想怎麼在程式碼層面和 prompt 層面同時設下護欄。
第四個關鍵概念:同時使用正面和負面指令
為什麼「不要用 cat」比「用 Read」更有效?
Claude Code 的系統提示詞中有這樣的指引:
不要用 Bash 工具來執行可以用專用工具完成的命令。這對於幫助使用者至關重要:
- 要讀檔案,用 Read 而不是 cat, head, tail, 或 sed
- 要編輯檔案,用 Edit 而不是 sed 或 awk
- 要建立檔案,用 Write 而不是 cat 搭配 heredoc
- 要搜尋檔案,用 Glob 而不是 find 或 ls
- 要搜尋內容,用 Grep 而不是 grep 或 rg注意它同時說了要做什麼和不要做什麼。
為什麼只說「用 Read」不夠? 因為 AI 在訓練過程中看過幾百萬個用 cat 讀檔案的範例。這些訓練資料形成了一種「慣性」——AI 的第一直覺就是用 cat。光說「用 Read」不足以覆蓋這個慣性,你還需要明確說「不要用 cat」。
這就像教一個有十年 Vim 經驗的人用 VS Code——你不能只說「用 VS Code 開檔案」,你還得說「不要按 :wq 來存檔」。
「ALWAYS」和「NEVER」的使用
在 GrepTool 的描述中:
ALWAYS use Grep for search tasks.
NEVER invoke `grep` or `rg` as a Bash command.全大寫的 ALWAYS 和 NEVER 看起來很粗魯,但它們在 AI 的注意力機制中有更高的權重。就像路邊的「禁止通行」標誌——你不會寫「建議考慮不要進入此區域」,你會寫「禁止通行」。
反思練習
盤點一下你的 AI 應用中,有哪些行為是 AI 總是做錯的(即使你已經在 prompt 中提過)。試試同時加上正面和負面指令。比如原本寫「用繁體中文回覆」,改成「用繁體中文回覆。不要使用簡體中文。」觀察行為有沒有改善。
第五個關鍵概念:告訴 AI 資訊的「保鮮期」
快照標記
在 context.ts 中,Git 狀態被這樣注入到上下文:
This is the git status at the start of the conversation.
Note that this status is a snapshot in time, and will not update
during the conversation.翻譯:「這是對話開始時的 Git 狀態。注意這只是一個時間快照,在對話過程中不會更新。」
為什麼要加這句話? 因為如果不說,AI 可能在對話的第 50 個來回時還說「根據你目前的 Git 狀態……」但那個狀態已經是半小時前的了,使用者可能已經做了 10 次 commit。
工具結果的「可能消失」警告
在系統提示詞中有這句話:
When working with tool results, write down any important information
you might need later in your response, as the original tool result
may be cleared later.翻譯:「當你看到工具的結果時,把重要資訊寫到你的回覆文字中,因為原始的工具結果稍後可能會被清除。」
這是一個精妙的「記憶外化」策略。想像 AI 的工作桌面(上下文窗口)空間有限。工具結果就像參考文件——佔空間但很少被重複查看。這句指令告訴 AI:「把重點抄到你的筆記上,然後我們就可以把參考文件收起來了。」
這樣,當系統後來清除舊的工具結果時(為了省空間),重要資訊已經在 AI 的回覆中了,不會丟失。
反思練習
你的 AI 應用中,有哪些注入的上下文會過時?試試明確標記它們的新鮮度。比如:
- 「以下是使用者的購買記錄(截至 2024-01-15,可能不包含最近的購買)」
- 「以下是專案的 README(上次更新:3 天前)」
AI 會根據這些標記來調整它的回覆的確定性程度。
第六個概念:具體數字比形容詞有效
「簡潔」vs「≤25 個字」
Claude Code 有一個 ant-only(內部員工專用)的指令:
Length limits: keep text between tool calls to ≤25 words.
Keep final responses to ≤100 words unless the task requires more detail.根據 Anthropic 內部的研究數據,用具體數字比用「保持簡潔」這種形容詞多省了 1.2% 的輸出 token。聽起來不多?在每天處理數十億 token 的規模下,1.2% 相當於每天省下數千萬個 token 的費用。
為什麼數字比形容詞有效?
想像你請朋友幫你搬家,你說「帶一些箱子來」。他帶了 3 個——但你需要 20 個。問題出在哪?「一些」這個詞對你來說是 20 個,對他來說是 3 個。
同樣的道理:你告訴 AI「回覆要簡潔」。AI 認為 200 字就很簡潔了(畢竟它一次可以輸出幾千字),但使用者覺得 200 字太長了,想要 50 字。
「≤25 個字」就沒有這個問題——25 就是 25,不管是 AI 還是人,理解都一樣。AI 可以在輸出時精確地控制字數,但它沒辦法精確地控制「簡潔程度」,因為「簡潔」沒有明確的定義。
同樣的模式出現在 FileEditTool 的描述中:
Use the smallest old_string that's clearly unique —
usually 2-4 adjacent lines is sufficient.
Avoid including 10+ lines of context.「2-4 行」和「10+ 行」給了 AI 一個清楚的數字範圍,比「盡量少」有效得多。
反思練習
把你的 prompt 中所有的形容詞(「簡短」「適當」「必要時」)列出來。試試用具體數字替換它們,然後比較效果。比如:
- 「簡短回覆」→「回覆控制在 50 字以內」
- 「提供適當的範例」→「提供 1-2 個範例」
- 「必要時詢問使用者」→「連續失敗 2 次後詢問使用者」
整體回顧:你可以帶走的 6 個原則
| 原則 | 一句話說明 | 在 Claude Code 中的體現 |
|---|---|---|
| 分層快取 | 不變的放前面,會變的放後面 | SYSTEM_PROMPT_DYNAMIC_BOUNDARY |
| 用命名傳達成本 | API 名稱就是最好的文件 | DANGEROUS_uncachedSystemPromptSection |
| 倒金字塔 | 最重要的放最前面 | 安全規則在第 2 段 |
| 正面+負面指令 | 同時說「要做什麼」和「不要做什麼」 | 「用 Read」+「不要用 cat」 |
| 標記資訊新鮮度 | 告訴 AI 這個資訊什麼時候會過時 | 「snapshot in time, will not update」 |
| 數字取代形容詞 | 具體數字比「簡潔」有效 | 「≤25 words」取代「keep it short」 |