Skip to content

PromptBuilder : la chaîne d'assemblage du system prompt

源码版本v4.0.10

Responsabilités

PromptBuilder est la classe qui, une fois Cline dispose d'un provider/model, construit concrètement le system prompt (invite système) correspondant. Elle est instanciée par PromptRegistry.get à chaque besoin (new PromptBuilder:92), et consomme trois entrées : un PromptVariant (qui détermine le modèle et l'ordre des composants), un SystemPromptContext (contexte runtime portant cwd / mcp / rules / skills, etc.), et un ComponentRegistry (table de fonctions de composants). Elle produit in fine une chaîne, qui est le systemPrompt que la Task passe à ApiHandler.createMessage dans attemptApiRequest (getSystemPrompt call:2350).

La répartition avec PromptRegistry est la suivante : la Registry se charge de déterminer, en fonction de providerInfo, quelle variante utiliser et maintient la table de composants ; le Builder, lui, exécute réellement le baseTemplate et le componentOrder de la variante, remplit les placeholders du modèle avec le résultat de chaque fonction de composant, puis applique un post-traitement qui nettoie les sections vides (build:23). En bref : la Registry choisit le modèle, le Builder le remplit.

Motivation de conception

  • Composants plutôt qu'une grosse chaîne : le system prompt est éclaté en composants AGENT_ROLE / TOOL_USE / TASK_PROGRESS / MCP / EDITING_FILES / ACT_VS_PLAN / CAPABILITIES / RULES / SYSTEM_INFO / OBJECTIVE / USER_INSTRUCTIONS / SKILLS (components:44). Chaque composant est une fonction indépendante qui renvoie une chaîne ou undefined, l'ordre étant fixé par la variante. Ainsi, différentes familles de modèles ne remplacent que quelques composants et réutilisent le reste.
  • placeholder + TemplateEngine : baseTemplate contient des placeholders comme / / . Le Builder remplit d'abord componentSections avec les résultats des composants, puis injecte les placeholders standards (cwd / currentDate / modelFamily) de façon uniforme (preparePlaceholders:55), avant de confier la substitution à TemplateEngine.resolve.
  • variant matcher pour choisir le modèle : PromptRegistry.getModelFamily parcourt toutes les variantes enregistrées et appelle leur matcher(context) ; la première à renvoyer true l'emporte (getModelFamily:42), la variante generic servant de filet de secours.
  • runtimePlaceholders en priorité maximale : le runtimePlaceholders accroché au context fait l'objet d'un dernier Object.assign qui écrase toutes les valeurs précédentes (runtime placeholders:79), afin que les champs calculés dynamiquement au runtime puissent forcer la valeur par défaut de la variante.
  • nettoyage en post-traitement : postProcess enchaîne plusieurs regex pour compresser plusieurs lignes vides consécutives en deux, retirer les ## de sections vides et les séparateurs ===== vides (postProcess:86), car l'assemblage dynamique laisse facilement des sections vides qui rendraient le prompt brouillon si elles n'étaient pas nettoyées.
  • entrée dédiée pour les prompts d'outils : getToolsPrompts est une méthode static, spécifiquement appelée par les variantes pour assembler la section « XML schema des outils » (getToolsPrompts:139), convertissant le résultat de ClineToolSet.getEnabledToolSpecs en texte de prompt au format <tool_name>...</tool_name>.

Fichiers clés

  • PromptBuilder class:12 — détient quatre dépendances : variant, context, components, templateEngine.
  • build method:23 — entrée principale ; enchaîne buildComponents → preparePlaceholders → templateEngine.resolve → postProcess.
  • buildComponents:30 — appelle chaque fonction de composant dans l'ordre fixé par variant.componentOrder et stocke les résultats dans sections.
  • preparePlaceholders:55 — quatre couches : placeholders de variante + champs standards + sections de composants + runtime placeholders.
  • postProcess:86 — série de regex nettoyant lignes vides, sections vides et séparateurs vides, tout en préservant les ===== de style diff.
  • getToolsPrompts:139 — méthode static qui transforme les outils activés en texte de prompt, un bloc ## tool_name par outil.
  • tool method:146 — construction du prompt d'un seul outil : filtre d'abord dependencies et contextRequirements, puis assemble les sections Parameters / Usage.
  • buildUsageSection:210 — produit l'exemple XML d'usage <tool><param>...</param></tool>.
  • PromptRegistry.get:86 — appelle getVariant pour sélectionner la variante, calcule nativeTools, puis new PromptBuilder(...).build().
  • getModelFamily:42 — parcourt les variantes en appelant matcher, la première correspondance l'emporte, sinon repli sur GENERIC.
  • generic components:44 — ordre des composants de la variante de repli ; presque tous les modèles utilisent ce parcours par défaut.
  • SystemPromptContext:94 — définition du type de contexte, avec providerInfo, cwd, mcpHub, skills, rules, browserSettings, etc., plusieurs dizaines de champs.

Flux de données

Le cœur du Builder consiste à « appeler les fonctions de composants dans l'ordre, collecter leurs résultats en sections, puis les passer au moteur de template » :

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
}

À noter : buildComponents s'exécute en for await séquentiel et non en Promise.all, car des dépendances d'ordre peuvent exister entre composants (par ex. OBJECTIVE peut référencer le contenu produit par les composants précédents) ; l'exécution séquentielle garantit cela (sequential for:35). Une erreur dans un composant ne fait que warn et n'interrompt pas l'ensemble de la construction ; les placeholders manquants seront remplacés par une chaîne vide plus tard.

L'ordre de priorité dans preparePlaceholders est crucial ; les écritures ultérieures écrasent les précédentes :

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

Cet empilement de couches rend limpide la priorité « valeur par défaut de la variante → champ standard → produit des composants → surcharge runtime », et modifier une couche ne risque pas de casser accidentellement les autres.

Limites et échecs

  • composant renvoyant undefined : le test result?.trim() ne laisse entrer dans sections que les chaînes non vides (trim check:44). Un composant peut donc renvoyer du contenu de façon sélective (par ex. la section BROWSER renvoie vide quand supportsBrowserUse=false).
  • composant qui lève : le catch se contente d'un Logger.warn sans rethrow, et les autres composants continuent (catch warn:47), de sorte qu'un bug dans un composant ne fasse pas s'effondrer tout le system prompt.
  • composant non enregistré : si components[componentId] n'existe pas, on warn puis on continue (not found warn:37), le placeholder correspondant étant rempli par une chaîne vide dans preparePlaceholders.
  • variante introuvable : PromptRegistry.getVariant lève directement si aucune variante ne correspond et qu'aucune variante GENERIC de repli n'est enregistrée (no variant throw:71). La variante GENERIC étant enregistrée par défaut, ce cas ne se produit pas en pratique.
  • matcher qui lève : getModelFamily enveloppe v.matcher(context) dans un try/catch ; en cas d'échec, on passe à la variante suivante (matcher catch:51), pour qu'un bug dans une variante ne casse pas toute la sélection.
  • nettoyage intempestif des ===== de diff : postProcess examine 50 caractères de contexte avant et après un ===== ; s'il s'agit de marqueurs diff (SEARCH / REPLACE / ++++ / ----), ils ne sont pas touchés (diff guard:106), évitant d'effacer par erreur les séparateurs diff présents dans le prompt de l'outil replace_in_file.
  • nativeTools recalculé : PromptRegistry.get réalise this.nativeTools = ClineToolSet.getNativeTools(variant, context) en side effect (nativeTools side effect:90), avec un commentaire explicite « Hacky way » ; à chaque récupération du systemPrompt, le calcul est refait.

Résumé

PromptBuilder décompose proprement les trois faces du system prompt — template + composants + contexte — : la variante décide du modèle et de l'ordre des composants, les fonctions de composants lisent le context et renvoient des chaînes, les placeholders + TemplateEngine les agrègent, et postProcess fait le ménage. Cette architecture permet à Cline de personnaliser rapidement le prompt pour différentes familles de modèles (next-gen / glm / gemini-3 / gpt-5 ont chacun leur variante), tout en mutualisant le même ensemble de fonctions de composants. Pour la suite : comment les outils sont définis, ou comment le mode plan/act influe sur le prompt.

  • Toolset et XML schema : /prompts/toolset
  • Différences entre modes plan / act : /prompts/mode
  • Comment un provider est sélectionné : /providers/api-handler
  • Comment le handler Anthropic consomme le systemPrompt : /providers/anthropic

Voir la documentation officielle : documentation Cline · README