PromptBuilder:系统提示的装配线
职责
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 的 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— 上下文类型定义,带 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 字符上下文,如果是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 把「系统提示 = 模板 + 组件 + 上下文」三件事拆得很清楚: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