Skip to content

PromptBuilder:システムプロンプトの組み立てライン

源码版本v4.0.10

役割

PromptBuilder は、Cline が「provider/model を取り出した後」にそのシステムプロンプト (system prompt) を実際に組み立てるクラスである。PromptRegistry.get が必要なタイミングで new して使う (new PromptBuilder:92)。3 つの入力を取る。1 つは PromptVariant (テンプレートとコンポーネント順序を決定)、1 つは SystemPromptContext (実行時コンテキスト、cwd / mcp / rules / skills 等を保持)、1 つは ComponentRegistry (コンポーネント関数テーブル)。最終的に 1 つの文字列を吐き出し、これが Task の attemptApiRequestApiHandler.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 に詰め、次に標準 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 は一連の正規表現で連続する空行を 2 つに圧縮し、空 section ヘッダ ## や空 ===== 区切り線を取り除く (postProcess:86)。コンポーネントを動的に組み立てると空セクションが残りやすく、放置するとプロンプトが乱雑になるため。
  • ツールプロンプトの独立エントリ:getToolsPrompts は static メソッドで、variant が「ツール XML schema」セクションを組み立てるために専用で呼ぶ (getToolsPrompts:139)。ClineToolSet.getEnabledToolSpecs の結果を <tool_name>...</tool_name> 形式のプロンプトテキストに変換する。

主要ファイル

  • PromptBuilder class:12 — variant、context、components、templateEngine の 4 つの依存を持つ。
  • build method:23 — 主エントリ、buildComponents → preparePlaceholders → templateEngine.resolve → postProcess の 4 ステップ。
  • buildComponents:30variant.componentOrder の順に各コンポーネント関数を呼び、結果を sections に格納。
  • preparePlaceholders:55 — variant placeholders + 標準 placeholder + コンポーネント sections + runtime placeholders の 4 層上書き。
  • 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:86getVariant で 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=false のとき BROWSER セクションは空を返す)。
  • コンポーネントが例外を投げる:catch して Logger.warn だけで再送出せず、他のコンポーネントは継続実行 (catch warn:47)。単一コンポーネントのバグでシステムプロンプト全体が崩れるのを防ぐ。
  • コンポーネント未登録:components[componentId] が取れないときは warn 後 continue する (not found warn:37)。対応するプレースホルダは preparePlaceholders で空文字列に補完される。
  • variant が見つからない:PromptRegistry.getVariant は全 variant がマッチせず GENERIC フォールバックもない場合に直接例外を投げる (no variant throw:71)。ただし GENERIC variant はデフォルトで登録されるため、通常ここには到達しない。
  • matcher が例外を投げる:getModelFamilyv.matcher(context) を try/catch で囲み、失敗すれば次の variant に進む (matcher catch:51)。ある variant のバグが選択全体を引きずり下ろさないようにする。
  • 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 は「システムプロンプト = テンプレート + コンポーネント + コンテキスト」という 3 つを明確に切り分ける。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