Skip to content

PromptBuilder: Die Montagelinie des Systemprompts ​

源码版本v4.0.10

Verantwortung ​

PromptBuilder ist Clines Klasse, die nach dem «Erhalten eines provider/model» den zugehörigen Systemprompt (system prompt) tatsächlich zusammenbaut. Er wird bei jedem Bedarf von PromptRegistry.get neu instanziiert (new PromptBuilder:92) und nimmt drei Dinge: ein PromptVariant (bestimmt Vorlage und Komponentenreihenfolge), einen SystemPromptContext (Laufzeit-Kontext mit cwd / mcp / rules / skills etc.) und eine ComponentRegistry (Komponentenfunktions-Tabelle). Am Ende wird ein String ausgegeben, den der Task in attemptApiRequest als systemPrompt an ApiHandler.createMessage übergibt (getSystemPrompt call:2350).

Die Aufgabenteilung mit PromptRegistry ist: Die Registry ist dafür zuständig, anhand von providerInfo die richtige variant zu matchen und die Komponententabelle zu pflegen; der Builder ist dafür zuständig, die baseTemplate und componentOrder der variant wirklich abzuarbeiten, das Ergebnis jeder Komponentenfunktion in den Vorlagen-Platzhalter einzutragen und dann eine Nachverarbeitung zum Aufräumen leerer Abschnitte durchzuführen (build:23). Kurz gesagt: Registry wählt die Vorlage, Builder füllt sie.

Entwurfsmotivation ​

  • Komponenten statt großem String: Der Systemprompt wird in Komponenten wie AGENT_ROLE / TOOL_USE / TASK_PROGRESS / MCP / EDITING_FILES / ACT_VS_PLAN / CAPABILITIES / RULES / SYSTEM_INFO / OBJECTIVE / USER_INSTRUCTIONS / SKILLS zerlegt (components:44); jede Komponente ist eine unabhängige Funktion, die einen String oder undefined zurückgibt; die Reihenfolge wird von der variant bestimmt. So können verschiedene Modellfamilien nur einige Komponenten austauschen und die anderen wiederverwenden.
  • placeholder + TemplateEngine: In der baseTemplate stehen Platzhalter wie / / ; der Builder füllt zuerst die Komponenten-Ergebnisse in componentSections, dann die Standard-Platzhalter (cwd / currentDate / modelFamily) einheitlich ein (preparePlaceholders:55) und übergibt es schließlich an TemplateEngine.resolve zur Ersetzung.
  • variant-Matcher wählt Vorlage: PromptRegistry.getModelFamily iteriert über alle registrierten Varianten, ruft für jede matcher(context) auf; die erste, die true zurückgibt, gewinnt (getModelFamily:42), die generic variant dient als Rückfall.
  • runtimePlaceholders mit höchster Priorität: runtimePlaceholders auf dem context überschreibt mit dem letzten Object.assign alle vorherigen Werte (runtime placeholders:79), sodass Felder, die zur Laufzeit dynamisch berechnet werden, die variant-Defaults zwangsweise überschreiben können.
  • Postprozess-Bereinigung: postProcess nutzt eine Reihe von Regex, um mehrere aufeinanderfolgende Leerzeilen auf zwei zu komprimieren, leere Section-Header ## zu entfernen und leere =====-Trennzeilen zu entfernen (postProcess:86), da dynamisches Zusammensetzen von Komponenten leicht leere Abschnitte hinterlässt, die den Prompt unordentlich wirken ließen.
  • Werkzeug-Prompt eigener Einstieg: getToolsPrompts ist eine statische Methode, die variant-spezifisch aufgerufen wird, um den «Werkzeug-XML-schema»-Abschnitt zu bauen (getToolsPrompts:139), der die Ergebnisse von ClineToolSet.getEnabledToolSpecs in <tool_name>...</tool_name>-formatierte Prompt-Texte umwandelt.

Schlüsseldateien ​

  • PromptBuilder class:12 — hält die vier Abhängigkeiten variant, context, components, templateEngine.
  • build method:23 — Haupteinstieg, vier Schritte: buildComponents → preparePlaceholders → templateEngine.resolve → postProcess.
  • buildComponents:30 — ruft jede Komponentenfunktion in der Reihenfolge von variant.componentOrder auf und speichert die Ergebnisse in sections.
  • preparePlaceholders:55 — vier Schichten: variant placeholders + Standard-Platzhalter + Komponenten sections + runtime placeholders.
  • postProcess:86 — eine Reihe von Regex zum Aufräumen von Leerzeilen, leeren Sections, leeren Trennzeilen; behält diff-artige ===== unverändert.
  • getToolsPrompts:139 — statische Methode, wandelt aktivierte Werkzeuge in Prompt-Texte um, pro Werkzeug ein ## tool_name-Abschnitt.
  • tool method:146 — Prompt-Konstruktion für ein einzelnes Werkzeug; filtert zuerst dependencies und contextRequirements, baut dann Parameters / Usage-Abschnitte.
  • buildUsageSection:210 — gibt ein XML-Verwendungsbeispiel <tool><param>...</param></tool> aus.
  • PromptRegistry.get:86 — ruft getVariant auf, um die variant zu wählen, berechnet nativeTools und ruft zuletzt new PromptBuilder(...).build() auf.
  • getModelFamily:42 — iteriert über variants und ruft matcher auf; der erste Treffer gewinnt, sonst Rückfall auf GENERIC.
  • generic components:44 — Komponentenreihenfolge der Rückfall-variant; für fast alle Modelle defaultmäßig aktiv.
  • SystemPromptContext:94 — Typdefinition des Kontexts mit Dutzenden Feldern wie providerInfo, cwd, mcpHub, skills, rules, browserSettings etc.

Datenfluss ​

Der Kern des Builders ist «Komponentenfunktionen in Reihenfolge aufrufen, Ergebnisse in sections sammeln und dann an die Template-Engine übergeben»:

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
}

Beachte, dass buildComponents mit for await sequenziell läuft, nicht mit Promise.all, da es zwischen Komponenten Abhängigkeiten geben kann (z. B. wenn OBJECTIVE auf Inhalte verweist, die von vorigen Komponenten gebaut wurden); sequentielles Abarbeiten stellt das sicher (sequential for:35). Wenn eine einzelne Komponente einen Fehler wirft, wird nur gewarnt und der gesamte Build nicht abgebrochen; fehlende Komponenten hinterlassen leere Platzhalter, die später durch leere Strings ersetzt werden.

Die Überschreibungsreihenfolge in preparePlaceholders ist kritisch; später Geschriebenes überschreibt früher Geschriebenes:

typescript
// apps/vscode/src/core/prompts/system-prompt/registry/PromptBuilder.ts
const placeholders: Record<string, unknown> = {}
Object.assign(placeholders, this.variant.placeholders)                  // 1. variant-Defaults
placeholders[STANDARD_PLACEHOLDERS.CWD] = this.context.cwd || process.cwd()  // 2. Standard-Felder
placeholders[STANDARD_PLACEHOLDERS.MODEL_FAMILY] = this.variant.family
placeholders[STANDARD_PLACEHOLDERS.CURRENT_DATE] = new Date().toISOString().split("T")[0]
Object.assign(placeholders, componentSections)                          // 3. Komponenten-Ergebnisse
for (const key of STANDARD_PLACEHOLDER_KEYS) {
	if (!placeholders[key]) {
		placeholders[key] = componentSections[key] || ""               // 4. fehlende Standard-Felder auffüllen
	}
}
const runtimePlaceholders = (this.context as any).runtimePlaceholders
if (runtimePlaceholders) {
	Object.assign(placeholders, runtimePlaceholders)                    // 5. Runtime-Überschreibung mit höchster Priorität
}
return placeholders

Dieses Schicht-für-Schicht-Design macht die Priorität «variant-Defaults → Standard-Felder → Komponenten-Ergebnisse → Runtime-Zwangsüberschreibung» klar; das Ändern einer Schicht beschädigt nicht versehentlich eine andere.

Grenzen und Fehler ​

  • Komponente gibt undefined zurück: result?.trim() prüft auf Leer; nur nicht-leere Strings gelangen in sections (trim check:44), sodass Komponenten selektiv Inhalte zurückgeben können (z. B. gibt der BROWSER-Abschnitt leer zurück, wenn supportsBrowserUse=false).
  • Komponente wirft Fehler: Nach catch nur Logger.warn, kein Re-throw; andere Komponenten laufen weiter (catch warn:47), sodass ein Bug in einer einzelnen Komponente nicht den gesamten Systemprompt abstürzen lässt.
  • Komponente nicht registriert: Wenn components[componentId] nicht geholt werden kann, wird gewarnt und mit continue fortgefahren (not found warn:37); der zugehörige Platzhalter wird in preparePlaceholders mit leerem String aufgefüllt.
  • variant nicht gefunden: PromptRegistry.getVariant wirft direkt, wenn keine variant matched und es kein GENERIC-Fallback gibt (no variant throw:71); die GENERIC-variant ist aber standardmäßig registriert, daher kommt es normalerweise nicht so weit.
  • matcher wirft Fehler: In getModelFamily wird v.matcher(context) von try/catch umschlossen; bei Fehlern wird mit der nächsten variant fortgefahren (matcher catch:51), sodass ein Bug in einer variant nicht die gesamte Auswahl behindert.
  • Diff-artige ===== fälschlich bereinigt: postProcess prüft vor und nach dem Ersetzen von ===== den Kontext von 50 Zeichen; wenn es diff-Markierungen wie SEARCH / REPLACE / ++++ / ---- sind, wird nichts geändert (diff guard:106), um zu vermeiden, dass die diff-Trennzeilen im replace_in_file-Werkzeugprompt fälschlich bereinigt werden.
  • nativeTools mehrfach berechnet: this.nativeTools = ClineToolSet.getNativeTools(variant, context) in PromptRegistry.get ist eine Side-Effect (nativeTools side effect:90); im Kommentar steht «Hacky way»; bei jedem Abruf des systemPrompts wird es einmal neu berechnet.

Zusammenfassung ​

PromptBuilder trennt die drei Aspekte «Systemprompt = Vorlage + Komponenten + Kontext» sauber: Die variant bestimmt Vorlage und Komponentenreihenfolge, die Komponentenfunktionen lesen den context und geben Strings zurück, placeholder + TemplateEngine kleben sie zusammen, postProcess schließt ab. Diese Struktur erlaubt es Cline, verschiedenen Modellfamilien schnell angepasste Prompts zu geben (next-gen / glm / gemini-3 / gpt-5 haben je ihre eigene variant), während sie denselben Satz Komponentenfunktionen teilen. Als Nächstes kannst du ansehen, wie Werkzeuge definiert sind oder wie der plan/act-Modus den Prompt beeinflusst.

  • Werkzeugsatz und XML-Schema: /prompts/toolset
  • Unterschiede im plan / act-Modus: /prompts/mode
  • Wie ein Provider ausgewählt wird: /providers/api-handler
  • Wie der Anthropic-Handler den systemPrompt verarbeitet: /providers/anthropic

Siehe offizielle Dokumentation: Cline-Dokumentation · README