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