Skip to content

PromptBuilder: la cadena de montaje del system prompt

源码版本v4.0.10

Responsabilidades

PromptBuilder es la clase con la que Cline, «una vez obtiene un provider/model», materializa el system prompt correspondiente. Lo instancia PromptRegistry.get cada vez que hace falta (new PromptBuilder:92) y se alimenta de tres cosas: un PromptVariant (que decide plantilla y orden de componentes), un SystemPromptContext (contexto de runtime con cwd / mcp / rules / skills, etc.) y un ComponentRegistry (tabla de funciones de componentes). Devuelve una cadena que es justo el systemPrompt que Task pasa a ApiHandler.createMessage dentro de attemptApiRequest (getSystemPrompt call:2350).

La división de responsabilidades con PromptRegistry es: Registry se encarga de, según providerInfo, emparejar qué variant usar y de mantener la tabla de componentes; Builder se encarga de recorrer el baseTemplate y el componentOrder del variant, rellenar los placeholders con el resultado de cada función componente y hacer el postprocesado que limpia los segmentos vacíos (build:23). En pocas palabras: Registry elige la plantilla, Builder la rellena.

Motivación de diseño

  • Componentización en vez de cadenas gigantes: el system prompt se descompone en componentes AGENT_ROLE / TOOL_USE / TASK_PROGRESS / MCP / EDITING_FILES / ACT_VS_PLAN / CAPABILITIES / RULES / SYSTEM_INFO / OBJECTIVE / USER_INSTRUCTIONS / SKILLS (components:44); cada uno es una función independiente que devuelve una cadena o undefined, y el orden lo fija el variant. Así, distintas familias de modelos pueden reemplazar solo algunos componentes y reutilizar el resto.
  • placeholder + TemplateEngine: en baseTemplate se escriben placeholders como / / ; Builder primero vuelca los resultados de componentes en componentSections, luego inyecta los placeholders estándar (cwd / currentDate / modelFamily) (preparePlaceholders:55) y finalmente delega a TemplateEngine.resolve la sustitución.
  • variant matcher elige la plantilla: PromptRegistry.getModelFamily recorre todas las variantes registradas, llama a matcher(context) de cada una y gana la primera que devuelva true (getModelFamily:42); el variant generic actúa como retroceso.
  • runtimePlaceholders tiene la máxima prioridad: el runtimePlaceholders del context se aplica al final con un Object.assign que sobrescribe todos los valores previos (runtime placeholders:79), de modo que los campos calculados dinámicamente en runtime pueden forzar la sobreescritura de los valores por defecto del variant.
  • Postprocesado de limpieza: postProcess aplica una serie de expresiones regulares que colapsan varias líneas vacías consecutivas en dos, eliminan cabeceras de sección vacías ## y separadores ===== vacíos (postProcess:86), ya que el ensamblaje dinámico de componentes deja con facilidad segmentos vacíos y el prompt se vería desordenado sin esta limpieza.
  • Entrada independiente para los prompts de herramientas: getToolsPrompts es un método static destinado a que el variant lo llame para ensamblar el segmento «XML schema de herramientas» (getToolsPrompts:139), convirtiendo el resultado de ClineToolSet.getEnabledToolSpecs en bloques de texto con forma <tool_name>...</tool_name>.

Archivos clave

  • PromptBuilder class:12 — mantiene cuatro dependencias: variant, context, components y templateEngine.
  • build method:23 — entrada principal; cuatro pasos: buildComponents → preparePlaceholders → templateEngine.resolve → postProcess.
  • buildComponents:30 — recorre las funciones de componente según variant.componentOrder y vuelca los resultados en sections.
  • preparePlaceholders:55 — cuatro capas de sobreescritura: placeholders del variant + campos estándar + sections de componentes + runtime placeholders.
  • postProcess:86 — serie de regex que limpia líneas vacías, secciones vacías y separadores vacíos, preservando intactos los ===== de estilo diff.
  • getToolsPrompts:139 — método static que convierte las herramientas habilitadas en texto de prompt, una sección ## tool_name por herramienta.
  • tool method:146 — constructor del prompt de una sola herramienta; primero filtra dependencies y contextRequirements, luego ensambla las secciones Parameters / Usage.
  • buildUsageSection:210 — genera el ejemplo XML <tool><param>...</param></tool>.
  • PromptRegistry.get:86 — llama a getVariant para elegir variant, calcula nativeTools y por último ejecuta new PromptBuilder(...).build().
  • getModelFamily:42 — recorre las variantes llamando a matcher; gana la primera que coincida; si ninguna, retrocede a GENERIC.
  • generic components:44 — orden de componentes del variant de retroceso; prácticamente todos los modelos van por aquí por defecto.
  • SystemPromptContext:94 — definición del tipo de contexto, con providerInfo, cwd, mcpHub, skills, rules, browserSettings y decenas de campos más.

Flujo de datos

El núcleo de Builder es «llamar a las funciones de componente en orden, recoger los resultados en sections y pasarlos al motor de plantillas»:

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
}

Observa que buildComponents se recorre con un for await secuencial, no con Promise.all, porque los componentes pueden tener dependencias de orden (por ejemplo, OBJECTIVE necesita referenciar contenido producido por componentes previos) y el recorrido secuencial lo garantiza (sequential for:35). Si un componente lanza una excepción, solo se hace warn y no se interrumpe la construcción; los componentes faltantes dejan un placeholder vacío que luego se sustituye por una cadena vacía.

El orden de sobreescritura en preparePlaceholders es crítico: lo que se escribe después pisa a lo previo:

typescript
// apps/vscode/src/core/prompts/system-prompt/registry/PromptBuilder.ts
const placeholders: Record<string, unknown> = {}
Object.assign(placeholders, this.variant.placeholders)                  // 1. valores por defecto del variant
placeholders[STANDARD_PLACEHOLDERS.CWD] = this.context.cwd || process.cwd()  // 2. campos estándar
placeholders[STANDARD_PLACEHOLDERS.MODEL_FAMILY] = this.variant.family
placeholders[STANDARD_PLACEHOLDERS.CURRENT_DATE] = new Date().toISOString().split("T")[0]
Object.assign(placeholders, componentSections)                          // 3. salida de componentes
for (const key of STANDARD_PLACEHOLDER_KEYS) {
	if (!placeholders[key]) {
		placeholders[key] = componentSections[key] || ""               // 4. rellenar con vacío los campos estándar ausentes
	}
}
const runtimePlaceholders = (this.context as any).runtimePlaceholders
if (runtimePlaceholders) {
	Object.assign(placeholders, runtimePlaceholders)                    // 5. sobreescritura de runtime, máxima prioridad
}
return placeholders

Este diseño en capas deja clara la precedencia «valores por defecto del variant → campos estándar → salida de componentes → sobreescritura forzada de runtime»; modificar una capa no rompe accidentalmente otra.

Límites y fallos

  • Componente devuelve undefined: el filtro result?.trim() deja entrar en sections solo las cadenas no vacías (trim check:44), de modo que un componente puede devolver contenido de forma selectiva (por ejemplo, el segmento BROWSER devuelve vacío cuando supportsBrowserUse=false).
  • Excepción en un componente: tras el catch solo se hace Logger.warn, no se relanza; los demás componentes siguen ejecutándose (catch warn:47), de modo que un bug en un componente único no tira todo el system prompt.
  • Componente no registrado: si components[componentId] no existe, se hace warn y se continúa (not found warn:37); el placeholder correspondiente se rellena con una cadena vacía en preparePlaceholders.
  • variant no encontrado: si PromptRegistry.getVariant no empareja ninguna variante y no hay retroceso GENERIC, lanza un error directamente (no variant throw:71), aunque el variant GENERIC está registrado por defecto y normalmente no se llega a este punto.
  • Excepción en matcher: getModelFamily envuelve v.matcher(context) en try/catch; si falla, continúa con la siguiente variante (matcher catch:51), evitando que el bug de una variante arrastre toda la selección.
  • Limpieza indebida de ===== de estilo diff: postProcess mira 50 caracteres de contexto antes y después al sustituir =====; si es un marcador diff del tipo SEARCH / REPLACE / ++++ / ----, lo deja intacto (diff guard:106), evitando borrar por error los separadores diff del prompt de la herramienta replace_in_file.
  • Recálculo repetido de nativeTools: en PromptRegistry.get, this.nativeTools = ClineToolSet.getNativeTools(variant, context) es un side effect (nativeTools side effect:90) marcado con el comentario «Hacky way»; cada vez que se obtiene el systemPrompt se recalcula una vez.

Resumen

PromptBuilder descompone con claridad la ecuación «system prompt = plantilla + componentes + contexto»: el variant fija la plantilla y el orden de componentes; las funciones de componente leen el context y devuelven una cadena; placeholder + TemplateEngine los ensamblan; postProcess cierra la limpieza. Esta estructura permite a Cline personalizar rápidamente el prompt para distintas familias de modelos (next-gen / glm / gemini-3 / gpt-5 tienen cada una su variant) compartiendo el mismo conjunto de funciones de componente. A continuación puedes ver cómo se definen las herramientas o cómo los modos plan/act influyen en el prompt.

  • Toolset y XML schema: /prompts/toolset
  • Diferencias entre modos plan / act: /prompts/mode
  • Cómo se selecciona el provider: /providers/api-handler
  • Cómo Anthropic handler consume el systemPrompt: /providers/anthropic

Véase la documentación oficial: Cline 文档 · README