Skip to content

PromptBuilder:系統提示的裝配線

源码版本v4.0.10

職責

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 的 baseTemplatecomponentOrder 真的跑一遍,把每個元件函式的結果填進模板佔位符,再做後處理清理空段(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,再餵給模板引擎」:

typescript
// 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
}

注意 buildComponentsfor await 順序跑的,不是 Promise.all,因為元件之間可能有順序依賴(比如 OBJECTIVE 要引用前面元件拼出的內容),順序跑能保證(sequential for:35). 單個元件拋錯只 warn 不中斷整個構建,缺失元件留空佔位符在後面被替換成空字串。

preparePlaceholders 的覆蓋順序很關鍵,後寫的覆蓋先寫的:

typescript
// 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=falseBROWSER 段回傳空)。
  • 元件拋錯: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.getthis.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

對照官方資料:Cline 文件 · README