TechLead
Lección 7 de 8

Encadenamiento y descomposición de prompts

Dividir tareas complejas en prompts más pequeños para obtener mejores resultados

¿Qué es el encadenamiento de prompts?

El encadenamiento de prompts divide tareas complejas en una secuencia de prompts más simples, donde la salida de un prompt se convierte en la entrada del siguiente. Este enfoque mejora la precisión, habilita flujos de trabajo complejos y facilita la depuración.

Piensa en ello como dividir una función grande en funciones pequeñas y enfocadas: cada prompt hace una cosa bien.

🔗 Por qué usar encadenamiento de prompts

  • Mejor precisión: Las tareas pequeñas son más fáciles de resolver correctamente
  • Depuración más sencilla: Identifica exactamente dónde falla el proceso
  • Flexibilidad: Cambia pasos individuales sin afectar a los demás
  • Eficiencia de tokens: Cada paso incluye solo el contexto relevante

Un solo prompt vs cadena

Un prompt único que intenta resolver todo a la vez suele producir errores u omisiones. Al dividir la tarea en pasos, cada paso recibe solo el contexto necesario y produce resultados más fiables.

// ❌ Single Complex Prompt (error-prone)
"Take this code, identify bugs, fix them, add error handling,
write tests, generate documentation, and suggest performance
improvements. Return everything formatted as markdown."

// ✅ Prompt Chain (reliable)
// Step 1: Analyze code
const analysis = await prompt(`
  Analyze this code for potential issues.
  Return a JSON array of issues found:
  [{ "type": "bug|style|perf", "line": number, "issue": string }]
  
  Code: ${code}
`);

// Step 2: Fix issues based on analysis  
const fixedCode = await prompt(`
  Fix these issues in the code:
  Issues: ${analysis}
  
  Original Code: ${code}
  
  Return only the fixed code.
`);

// Step 3: Add error handling
const robustCode = await prompt(`
  Add comprehensive error handling to this code.
  Include try-catch blocks and input validation.
  
  Code: ${fixedCode}
`);

// Step 4: Generate tests
const tests = await prompt(`
  Write unit tests for this code using Jest.
  Cover: happy path, edge cases, error cases.
  
  Code: ${robustCode}
`);

Patrones comunes de encadenamiento

Existen tres patrones principales: procesamiento secuencial (cada paso alimenta al siguiente), map-reduce (procesamiento en paralelo con combinación final) y ramificación (diferentes caminos según clasificación).

// Pattern 1: Sequential Processing
// Each step builds on the previous
input → [Extract] → data → [Transform] → result → [Format] → output

// Example: Document Processing
const text = await extractTextFromPDF(file);
const entities = await extractEntities(text);
const summary = await summarizeWithEntities(text, entities);
const report = await formatAsReport(summary, entities);

// Pattern 2: Map-Reduce
// Process items in parallel, then combine
items.map(item => process(item)) → [Combine] → result

// Example: Code Review
const files = ['a.ts', 'b.ts', 'c.ts'];
const reviews = await Promise.all(
  files.map(file => reviewFile(file))
);
const summary = await summarizeReviews(reviews);

// Pattern 3: Branching
// Different paths based on classification
input → [Classify] → type → 
  type A → [Process A]
  type B → [Process B]
  type C → [Process C]

// Example: Customer Support
const category = await classifyTicket(ticket);
switch (category) {
  case 'technical': return await handleTechnical(ticket);
  case 'billing': return await handleBilling(ticket);
  case 'feature': return await handleFeatureRequest(ticket);
}

Estrategia de descomposición

La clave está en dividir una tarea compleja en pasos bien definidos: diseño, especificación, implementación y pruebas. Cada paso recibe el contexto acumulado de los anteriores.

// Break down complex task into steps

// Original Complex Task:
"Create a full REST API for a todo app"

// Decomposed:
const steps = [
  // 1. Design
  "Design the data model for a todo app with users, " +
  "lists, and items. Return as TypeScript interfaces.",
  
  // 2. API Specification
  "Based on these types, design REST endpoints. " +
  "Return OpenAPI spec for CRUD operations.",
  
  // 3. Implementation - Routes
  "Implement Express routes for these endpoints. " +
  "Include input validation.",
  
  // 4. Implementation - Controllers  
  "Implement controller functions for each route. " +
  "Include error handling.",
  
  // 5. Database Layer
  "Implement database functions using Prisma " +
  "for these operations.",
  
  // 6. Tests
  "Write integration tests for these endpoints " +
  "using supertest."
];

// Execute sequentially, passing context forward
let context = {};
for (const step of steps) {
  const result = await prompt(step, context);
  context = { ...context, ...result };
}

Cadena de verificación

Incluir pasos de revisión en la cadena permite detectar y corregir errores automáticamente. El patrón de autocrítica genera un borrador, lo evalúa y lo mejora en iteraciones sucesivas.

// Chain includes verification steps
async function generateAndVerify(task) {
  // Step 1: Generate
  const code = await prompt(`
    Generate code for: ${task}
    Return only the code.
  `);
  
  // Step 2: Review
  const review = await prompt(`
    Review this code for bugs and issues.
    Return JSON: { "issues": [], "isValid": boolean }
    
    Code: ${code}
  `);
  
  // Step 3: Fix if needed
  if (!review.isValid) {
    const fixedCode = await prompt(`
      Fix these issues in the code:
      Issues: ${JSON.stringify(review.issues)}
      
      Code: ${code}
    `);
    
    // Step 4: Verify fix
    return await generateAndVerify(task); // Retry
  }
  
  return code;
}

// Self-critique pattern
async function generateWithCritique(task) {
  const draft = await prompt(`Generate: ${task}`);
  
  const critique = await prompt(`
    Critique this output. What could be improved?
    Output: ${draft}
  `);
  
  const final = await prompt(`
    Improve this output based on the critique:
    Original: ${draft}
    Critique: ${critique}
  `);
  
  return final;
}

Ejemplo real: migración de código

La migración de JavaScript a TypeScript es un caso ideal para encadenamiento: se analiza la estructura, se infieren tipos, se convierte el código, se añade strictness y se verifica el resultado.

// Migrating code from JavaScript to TypeScript

async function migrateToTypeScript(jsCode) {
  // Step 1: Analyze structure
  const analysis = await prompt(`
    Analyze this JavaScript code structure.
    Identify: functions, classes, exported items, dependencies.
    Return as JSON.
    
    Code: ${jsCode}
  `);
  
  // Step 2: Infer types
  const types = await prompt(`
    Based on this code analysis, infer TypeScript types.
    Create interfaces for objects, type parameters, return types.
    
    Analysis: ${analysis}
    Original Code: ${jsCode}
  `);
  
  // Step 3: Convert code
  const tsCode = await prompt(`
    Convert this JavaScript to TypeScript.
    Use these inferred types: ${types}
    
    JavaScript: ${jsCode}
  `);
  
  // Step 4: Add strict checks
  const strictCode = await prompt(`
    Make this TypeScript code strict-mode compatible.
    - Add null checks
    - Handle undefined cases
    - Use proper type narrowing
    
    Code: ${tsCode}
  `);
  
  // Step 5: Verify
  const verification = await prompt(`
    Verify this TypeScript code:
    1. Are all types properly defined?
    2. Are there any 'any' types that should be specific?
    3. Does it handle all edge cases?
    
    Return issues as JSON array.
    
    Code: ${strictCode}
  `);
  
  return { code: strictCode, issues: verification };
}

Gestión del contexto

Pasar demasiado contexto entre pasos consume tokens y puede confundir al modelo. Las mejores estrategias pasan solo la información relevante, usan objetos de contexto acumulados o resumen los resultados intermedios.

// Managing context across chain steps

// Strategy 1: Pass relevant context only
async function chainWithContext(input) {
  const step1Result = await prompt(`
    Step 1 task...
    Input: ${input}
  `);
  
  // Only pass what's needed for step 2
  const step2Result = await prompt(`
    Step 2 task...
    Previous: ${step1Result.summary} // Not full output
  `);
  
  return step2Result;
}

// Strategy 2: Accumulated context object
class ChainContext {
  private data: Record<string, any> = {};
  
  set(key: string, value: any) {
    this.data[key] = value;
  }
  
  get(key: string) {
    return this.data[key];
  }
  
  toPromptContext() {
    return Object.entries(this.data)
      .map(([k, v]) => `${k}: ${JSON.stringify(v)}`)
      .join('\n');
  }
}

// Strategy 3: Summarize between steps
async function chainWithSummary(steps) {
  let context = "";
  
  for (const step of steps) {
    const result = await prompt(step + `\nContext: ${context}`);
    
    // Summarize for next step
    context = await prompt(`
      Summarize the key information from this for the next step:
      ${result}
      
      Keep it under 100 words.
    `);
  }
}

✅ Buenas prácticas de encadenamiento

  • • Cada paso debe tener un único propósito claro
  • • Usa salida estructurada (JSON) entre pasos para mayor fiabilidad
  • • Incluye pasos de validación/verificación en tareas críticas
  • • Pasa solo el contexto relevante para mantener el foco
  • • Agrega manejo de errores y lógica de reintentos
  • • Registra resultados intermedios para depurar