Skip to content

PromptBuilder:系统提示的装配线

源码版本v4.0.10

职责

PromptBuilder 是 Cline 在「拿到一个 provider/model 之后」把它对应的系统提示 (system prompt) 实际拼出来的类。它由 PromptRegistry.get 在每次需要时 new 出来(new PromptBuilder:92),吃三个东西:一个 PromptVariant(决定模板和组件顺序)、一个 SystemPromptContext(运行时上下文,带 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 — 上下文类型定义,带 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 字符上下文,如果是 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 把「系统提示 = 模板 + 组件 + 上下文」三件事拆得很清楚: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