PromptBuilder:系統提示的裝配線
職責
PromptBuilder 是 Cline 在「拿到一個 provider/model 之後」把它對應的系統提示 (system prompt) 實際拼出來的類別。它由 PromptRegistry.get 在每次需要時 new 出來(new PromptBuilder:92),吃三個東西:一個 PromptVariant(決定模板和元件順序)、一個 SystemPromptContext(執行時 context,帶 cwd / mcp / rules / skills 等)、一個 ComponentRegistry(元件函式表)。最終吐出一個字串,這個字串就是 Task 在 attemptApiRequest 裡餵給 ApiHandler.createMessage 的 systemPrompt(getSystemPrompt call:2350).
它和 PromptRegistry 的分工是:Registry 負責按 providerInfo 匹配出該用哪個 variant、並維護元件表;Builder 負責把 variant 的 baseTemplate 和 componentOrder 真的跑一遍,把每個元件函式的結果填進模板佔位符,再做後處理清理空段(build:23). 簡單說 Registry 選模板,Builder 填模板。
設計動機
- 元件化而非大字串:系統提示被拆成
AGENT_ROLE / TOOL_USE / TASK_PROGRESS / MCP / EDITING_FILES / ACT_VS_PLAN / CAPABILITIES / RULES / SYSTEM_INFO / OBJECTIVE / USER_INSTRUCTIONS / SKILLS等元件(components:44),每個元件是一個獨立函式,回傳字串或 undefined,順序由 variant 決定。這樣不同模型家族可以只換其中幾個元件,其他復用。 - placeholder + TemplateEngine:
baseTemplate裡寫/ /這種佔位符,Builder 先把元件結果填進componentSections,再把 standard placeholder(cwd / currentDate / modelFamily)統一塞進去(preparePlaceholders:55),最後交給TemplateEngine.resolve做替換。 - variant matcher 選模板:
PromptRegistry.getModelFamily遍歷所有註冊 variant,調每個 variant 的matcher(context),第一個回傳 true 的勝出(getModelFamily:42),generic variant 兜底。 - runtimePlaceholders 優先級最高:context 上掛的
runtimePlaceholders最後一次Object.assign覆蓋前面所有值(runtime placeholders:79),讓執行時動態計算的欄位能強制覆蓋 variant 預設值。 - 後處理清洗:
postProcess一連串正則把多個連續空行壓成兩個、去掉空 section header##、去掉空=====分隔條(postProcess:86),因為元件動態拼接很容易留下空段,不清理會讓提示看起來很亂。 - 工具提示獨立入口:
getToolsPrompts是 static 方法,專門給 variant 調用來拼「工具 XML schema」段(getToolsPrompts:139),把ClineToolSet.getEnabledToolSpecs的結果轉成<tool_name>...</tool_name>格式的提示文字。
關鍵檔案
PromptBuilder class:12— 持有 variant、context、components、templateEngine 四個依賴。build method:23— 主入口,buildComponents → preparePlaceholders → templateEngine.resolve → postProcess四步。buildComponents:30— 按variant.componentOrder順序調每個元件函式,結果存進 sections。preparePlaceholders:55— variant placeholders + 標準佔位符 + 元件 sections + runtime placeholders 四層覆蓋。postProcess:86— 一連串正則清理空行、空 section、空分隔符,保留 diff 風格的=====不被改。getToolsPrompts:139— static 方法,把啟用的工具轉成提示文字,每個工具一條## tool_name段。tool method:146— 單個工具的提示構造,先過濾 dependencies 和 contextRequirements,再拼 Parameters / Usage 段。buildUsageSection:210— 輸出<tool><param>...</param></tool>的 XML 用法範例。PromptRegistry.get:86— 調getVariant選 variant、附帶算 nativeTools、最後new PromptBuilder(...).build()。getModelFamily:42— 遍歷 variants 調 matcher,第一個匹配的勝出,否則回落 GENERIC。generic components:44— 兜底 variant 的元件順序,基本所有模型預設走這套。SystemPromptContext:94— context 型別定義,帶 providerInfo、cwd、mcpHub、skills、rules、browserSettings 等幾十個欄位。
資料流
Builder 的核心是「按順序調元件函式,把結果收集成 sections,再餵給模板引擎」:
// apps/vscode/src/core/prompts/system-prompt/registry/PromptBuilder.ts
async build(): Promise<string> {
const componentSections = await this.buildComponents()
const placeholderValues = this.preparePlaceholders(componentSections)
const prompt = this.templateEngine.resolve(this.variant.baseTemplate, this.context, placeholderValues)
return this.postProcess(prompt)
}
private async buildComponents(): Promise<Record<string, string>> {
const sections: Record<string, string> = {}
const { componentOrder } = this.variant
// Process components sequentially to maintain order
for (const componentId of componentOrder) {
const componentFn = this.components[componentId]
if (!componentFn) {
Logger.warn(`Warning: Component '${componentId}' not found`)
continue
}
try {
const result = await componentFn(this.variant, this.context)
if (result?.trim()) {
sections[componentId] = result
}
} catch (error) {
Logger.warn(`Warning: Failed to build component '${componentId}':`, error)
}
}
return sections
}注意 buildComponents 是 for await 順序跑的,不是 Promise.all,因為元件之間可能有順序依賴(比如 OBJECTIVE 要引用前面元件拼出的內容),順序跑能保證(sequential for:35). 單個元件拋錯只 warn 不中斷整個構建,缺失元件留空佔位符在後面被替換成空字串。
preparePlaceholders 的覆蓋順序很關鍵,後寫的覆蓋先寫的:
// apps/vscode/src/core/prompts/system-prompt/registry/PromptBuilder.ts
const placeholders: Record<string, unknown> = {}
Object.assign(placeholders, this.variant.placeholders) // 1. variant 默认值
placeholders[STANDARD_PLACEHOLDERS.CWD] = this.context.cwd || process.cwd() // 2. 标准字段
placeholders[STANDARD_PLACEHOLDERS.MODEL_FAMILY] = this.variant.family
placeholders[STANDARD_PLACEHOLDERS.CURRENT_DATE] = new Date().toISOString().split("T")[0]
Object.assign(placeholders, componentSections) // 3. 组件产物
for (const key of STANDARD_PLACEHOLDER_KEYS) {
if (!placeholders[key]) {
placeholders[key] = componentSections[key] || "" // 4. 缺失的标准字段补空
}
}
const runtimePlaceholders = (this.context as any).runtimePlaceholders
if (runtimePlaceholders) {
Object.assign(placeholders, runtimePlaceholders) // 5. 运行时覆盖优先级最高
}
return placeholders這一層套一層的設計,讓「variant 預設值 → 標準欄位 → 元件產物 → runtime 強制覆蓋」優先級清晰,改任何一層都不會意外破壞別的層。
邊界與失敗
- 元件回傳 undefined:
result?.trim()判空,只有非空字串才進 sections(trim check:44),所以元件可以選擇性回傳內容(比如supportsBrowserUse=false時BROWSER段回傳空)。 - 元件拋錯:catch 後只
Logger.warn不重拋,其他元件繼續跑(catch warn:47),保證單個元件 bug 不會讓整個系統提示崩。 - 元件未註冊:
components[componentId]拿不到時 warn 後 continue(not found warn:37),對應佔位符在preparePlaceholders裡補空字串。 - variant 找不到:
PromptRegistry.getVariant在所有 variant 都不匹配且沒有 GENERIC 兜底時直接拋錯(no variant throw:71),不過 GENERIC variant 預設註冊,正常不會到這一步。 - matcher 拋錯:
getModelFamily裡 try/catch 包住v.matcher(context),失敗繼續下一個 variant(matcher catch:51),不讓某個 variant 的 bug 拖垮整個選擇。 - diff 風格
=====誤清理:postProcess在替換=====前後看 50 字元 context,如果是SEARCH / REPLACE / ++++ / ----這類 diff 標記就不動(diff guard:106),避免把replace_in_file工具提示裡的 diff 分隔符誤清理掉。 - nativeTools 重複計算:
PromptRegistry.get裡this.nativeTools = ClineToolSet.getNativeTools(variant, context)是個 side effect(nativeTools side effect:90),註解裡都寫了「Hacky way」,每次取 systemPrompt 都會重算一次。
小結
PromptBuilder 把「系統提示 = 模板 + 元件 + context」三件事拆得很清楚:variant 決定模板和元件順序,元件函式讀 context 回傳字串,placeholder + TemplateEngine 把它們黏起來,postProcess 收尾。這套結構讓 Cline 能給不同模型家族快速定製提示(next-gen / glm / gemini-3 / gpt-5 都有自己的 variant),同時共享同一套元件函式。接下來可以看工具怎麼定義、或者 plan/act 模式怎麼影響提示。
- 工具集與 XML schema:
/prompts/toolset - plan / act 模式差異:
/prompts/mode - 提供者怎麼被選中:
/providers/api-handler - Anthropic handler 怎麼消費 systemPrompt:
/providers/anthropic