Skip to content

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.

全大寫的 ALWAYSNEVER 看起來很粗魯,但它們在 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」

Claude Code 架構設計深度分析