Cap 21: Prompt Engineering & Structured Output

Por: Artiko
claude-codeprompt-engineeringfew-shotbatch-apici-cdstructured-output

Este capítulo cubre el Domain 4 del examen Claude Certified Architect – Foundations: técnicas de prompt engineering que producen outputs consistentes, reducen falsos positivos y escalan a producción.


1. Criterios explícitos para reducir falsos positivos

El problema con instrucciones vagas

Los falsos positivos destruyen la confianza. Si Claude marca como problema algo que no lo es, el equipo deja de confiar en las alertas y las ignora todas, incluidas las reales.

MALO: instrucción vaga

Revisa este código y señala cualquier problema de seguridad.

Este prompt produce inconsistencias graves:

  • Una vez marca console.log como “información sensible”
  • Otra vez ignora una inyección SQL real
  • El umbral de “problema” varía entre ejecuciones

BUENO: criterios categóricos específicos

Revisa este código y reporta ÚNICAMENTE los siguientes problemas de seguridad.
Sé conservador: si no estás seguro, NO lo reportes.

Categorías a reportar:
- CRÍTICO: credenciales hardcodeadas (contraseñas, API keys, tokens en literales de string)
- CRÍTICO: inyección SQL (concatenación de strings en queries sin parametrizar)
- CRÍTICO: API keys expuestas en código fuente o logs
- ALTO: deserialización de datos no confiables sin validación
- ALTO: ejecución de comandos del sistema con input de usuario

No reportar:
- Problemas de estilo o convenciones
- Posibles mejoras de performance
- Cualquier cosa que requiera contexto de negocio adicional

Por qué funciona

Los criterios categóricos establecen un contrato claro:

flowchart LR
    A[Input de código] --> B{¿Cumple criterio\nexacto?}
    B -->|Sí: credencial hardcodeada| C[REPORTAR CRÍTICO]
    B -->|Sí: SQL sin parametrizar| D[REPORTAR CRÍTICO]
    B -->|No encaja en ninguna categoría| E[IGNORAR]
    C --> F[Output confiable]
    D --> F
    E --> F

Ejercicio: reescribir instrucción vaga

Vago: “Verifica que el código siga buenas prácticas”

Categórico:

Verifica ÚNICAMENTE:
- [ ] Funciones con más de 50 líneas → reportar nombre y línea
- [ ] Variables sin tipo explícito en TypeScript → reportar nombre
- [ ] try/catch que captura Error genérico sin re-lanzar → reportar ubicación

No reportar convenciones de nombres, comentarios ni estructura de carpetas.

2. Few-shot prompting para consistencia

Qué es few-shot

Few-shot prompting consiste en incluir ejemplos de input/output deseado dentro del prompt. Es la diferencia entre decirle a Claude qué hacer y mostrarle cómo se ve el resultado correcto.

flowchart TD
    A[Zero-shot] --> B["Instrucción → Claude adivina formato"]
    C[Few-shot] --> D["Instrucción + Ejemplos → Claude replica patrón"]
    B --> E[Output inconsistente]
    D --> F[Output uniforme]

Cuándo usar few-shot

  • Zero-shot produce formatos distintos entre llamadas
  • El output requiere estructura muy específica (JSON, tablas, severidades)
  • El dominio es ambiguo y los ejemplos clarifican la intención
  • Se necesita tono o estilo consistente entre múltiples ejecuciones

Estructura de un prompt few-shot

[Instrucción del rol y tarea]

[Ejemplo 1]
Input: <caso de ejemplo>
Output: <resultado exacto esperado>

[Ejemplo 2]
Input: <otro caso>
Output: <otro resultado>

[Query real]
Input: <el input actual>
Output:

Ejemplo: formatear hallazgos de code review

Sin ejemplos (zero-shot) — resultado inconsistente

Prompt:

Revisa este código y reporta los problemas encontrados.

Código: [código aquí]

Posibles outputs inconsistentes:

  • “Hay un problema de seguridad en la línea 42”
  • Bug: SQL injection detectada”
  • “⚠️ Line 42: possible injection”

Con ejemplos (few-shot) — resultado uniforme

import Anthropic from '@anthropic-ai/sdk';

const client = new Anthropic();

const fewShotPrompt = `Eres un revisor de código. Analiza el fragmento y reporta hallazgos
usando EXACTAMENTE el formato de los ejemplos. Si no hay hallazgos, responde "Sin hallazgos."

---
Ejemplo 1
Input:
\`\`\`python
password = "admin123"
db.execute("SELECT * FROM users WHERE id=" + user_id)
\`\`\`
Output:
[CRÍTICO] L1 - Credencial hardcodeada: variable \`password\` contiene valor literal
[CRÍTICO] L2 - SQL Injection: concatenación de \`user_id\` sin parametrizar

---
Ejemplo 2
Input:
\`\`\`python
def calcular_total(items):
    return sum(item.precio for item in items)
\`\`\`
Output:
Sin hallazgos.

---
Ejemplo 3
Input:
\`\`\`python
api_key = os.environ.get("API_KEY")
response = requests.get(url, headers={"Authorization": f"Bearer {api_key}"})
logging.info(f"Llamando API con key: {api_key}")
\`\`\`
Output:
[ALTO] L3 - API key expuesta en logs: \`api_key\` se imprime en logging.info

---
Input:
\`\`\`python
SECRET_TOKEN = "tok_live_abc123xyz"
conn.execute("DELETE FROM orders WHERE user=" + request.args.get("id"))
\`\`\`
Output:`;

async function reviewCode(code: string): Promise<string> {
  const message = await client.messages.create({
    model: 'claude-opus-4-8',
    max_tokens: 1024,
    messages: [
      {
        role: 'user',
        content: fewShotPrompt.replace(
          '```python\nSECRET_TOKEN',
          `\`\`\`python\n${code}`
        ),
      },
    ],
  });

  const content = message.content[0];
  return content.type === 'text' ? content.text : '';
}

Output consistente garantizado:

[CRÍTICO] L1 - Credencial hardcodeada: variable `SECRET_TOKEN` contiene valor literal
[CRÍTICO] L2 - SQL Injection: concatenación de `request.args.get("id")` sin parametrizar

Los ejemplos deben cubrir edge cases

Malo: solo ejemplos de happy path

Ejemplo 1: credencial obvia → reportar
Ejemplo 2: código limpio → sin hallazgos

Bueno: incluir casos borderline

Ejemplo 3: variable con nombre "key" pero valor de variable de entorno → sin hallazgos
Ejemplo 4: API key en comentario (no en código ejecutable) → sin hallazgos
Ejemplo 5: token en test con valor "fake-token-for-testing" → sin hallazgos

Sin ejemplos de edge cases, Claude asume que el patrón es más amplio de lo que es.


3. Reducción de ambigüedad con exemplares

El problema con términos ambiguos

Términos como “escalar”, “borderline”, “apropiado” o “razonable” son ambiguos. Dos personas razonables los interpretarían diferente.

Ambiguo:

Si el cliente hace una solicitud borderline, escálala al equipo humano.

¿Qué es “borderline”? ¿Amenaza velada? ¿Solicitud de reembolso por monto alto? ¿Lenguaje agresivo?

Solución: few-shot examples que demuestran la decisión

const customerSupportPrompt = `Eres un agente de soporte. Decide si RESPONDER directamente
o ESCALAR al equipo humano. Usa exactamente el formato del ejemplo.

---
Caso 1
Cliente: "Mi pedido llegó roto, quiero mi dinero de vuelta"
Decisión: RESPONDER
Motivo: Solicitud estándar de reembolso, dentro de política de 30 días
Respuesta: "Lamentamos el inconveniente. Procesaremos tu reembolso en 3-5 días hábiles..."

---
Caso 2
Cliente: "Llevo 3 semanas esperando y nadie me responde. Voy a publicar esto en redes"
Decisión: ESCALAR
Motivo: Cliente insatisfecho con riesgo de impacto en reputación, requiere atención prioritaria
Respuesta: [ESCALAR: cliente con historial de espera prolongada + amenaza de publicación]

---
Caso 3
Cliente: "Quiero cancelar mi suscripción"
Decisión: RESPONDER
Motivo: Cancelación estándar, proceso automatizable
Respuesta: "Puedo ayudarte con la cancelación. ¿Puedes confirmar el email de tu cuenta?"

---
Caso 4
Cliente: "Saben que tengo abogado ¿verdad? Esto no va a quedar así"
Decisión: ESCALAR
Motivo: Mención de acción legal, requiere revisión del equipo legal y gestión senior
Respuesta: [ESCALAR: mención explícita de acción legal]

---
Caso actual
Cliente: "Pagué por el plan premium hace dos semanas y todavía no tengo acceso a las funciones"
Decisión:`;

Con estos 4 ejemplos, Claude aprende que:

  • Reembolso estándar → responder
  • Riesgo reputacional → escalar
  • Proceso rutinario → responder
  • Mención legal → escalar siempre

4. Diseño de prompts para CI/CD

Requisitos de un prompt de CI/CD

Un prompt en un pipeline de CI/CD debe cumplir:

  1. Determinismo: misma entrada → misma categoría de salida
  2. Bajo false positive: mejor perder un positivo que generar ruido
  3. Formato parseable: el pipeline necesita leer el resultado programáticamente
  4. Fail-safe: si Claude no está seguro, no bloquear el pipeline

Cómo se consigue hoy el determinismo

La vía clásica —temperature: 0, ajustar top_p/top_k y prellenar el turno assistant con { para forzar JSON— ya no existe en los modelos actuales:

  • temperature, top_p y top_k devuelven 400 en Fable 5, Opus 4.8 y Opus 4.7; Sonnet 5 rechaza cualquier valor que no sea el default.
  • El prefill del turno assistant final también devuelve 400 en Fable 5, Opus 4.6/4.7/4.8 y Sonnet 4.6/5.

El comportamiento se dirige hoy por prompting (criterios categóricos, few-shot, reglas explícitas) y por structured outputs, que garantiza la forma del JSON a nivel de API en vez de a nivel de instrucción.

flowchart LR
    A[PR abierto] --> B[CI trigger]
    B --> C[Claude review]
    C --> D{¿Hallazgos CRÍTICOS?}
    D -->|Sí| E[Block PR + comentario]
    D -->|No| F[Approve + comentario opcional]
    E --> G[Developer fix]
    G --> A

Prompt real de review de PR para CI

import Anthropic from '@anthropic-ai/sdk';

interface ReviewResult {
  status: 'BLOCK' | 'APPROVE';
  findings: Finding[];
}

interface Finding {
  severity: 'CRÍTICO' | 'ALTO';
  file: string;
  line: number;
  description: string;
}

async function reviewPullRequest(diff: string): Promise<ReviewResult> {
  const client = new Anthropic();

  const prompt = `Eres un revisor de seguridad en un pipeline de CI/CD.
Analiza el diff de Git y responde ÚNICAMENTE con JSON válido.

REGLAS:
- Sé conservador: en caso de duda, NO reportes
- Solo reporta problemas que BLOQUEEN el merge
- No reportes problemas de estilo, performance ni arquitectura

CRITERIOS DE BLOQUEO:
- Credenciales hardcodeadas (strings con patrones de key/token/password/secret)
- Inyección SQL directa (concatenación en queries)
- API keys o tokens en código fuente (no en .env ni variables de entorno)
- Datos de usuarios expuestos en logs

FORMATO DE RESPUESTA (solo JSON, sin texto adicional):
{
  "status": "BLOCK" | "APPROVE",
  "findings": [
    {
      "severity": "CRÍTICO" | "ALTO",
      "file": "nombre del archivo",
      "line": número de línea,
      "description": "descripción concisa del problema"
    }
  ]
}

Si status es APPROVE, findings debe ser un array vacío [].

DIFF A REVISAR:
${diff}`;

  const message = await client.messages.create({
    model: 'claude-opus-4-8',
    max_tokens: 2048,
    messages: [{ role: 'user', content: prompt }],
  });

  const content = message.content[0];
  if (content.type !== 'text') throw new Error('Unexpected response type');

  return JSON.parse(content.text) as ReviewResult;
}

Structured outputs: dejar de parsear texto a ciegas

El prompt anterior obtiene JSON pidiéndolo por instrucción y parseando el texto con JSON.parse. Es frágil: cualquier preámbulo, bloque de código o coma sobrante rompe el pipeline.

Structured outputs es GA y elimina esa fragilidad: el esquema se declara en la petición y la respuesta se ajusta a él.

const message = await client.messages.create({
  model: 'claude-opus-4-8',
  max_tokens: 2048,
  output_config: {
    format: {
      type: 'json_schema',
      schema: {
        type: 'object',
        properties: {
          status: { type: 'string', enum: ['BLOCK', 'APPROVE'] },
          findings: {
            type: 'array',
            items: {
              type: 'object',
              properties: {
                severity: { type: 'string', enum: ['CRÍTICO', 'ALTO'] },
                file: { type: 'string' },
                line: { type: 'number' },
                description: { type: 'string' },
              },
              required: ['severity', 'file', 'line', 'description'],
              additionalProperties: false,
            },
          },
        },
        required: ['status', 'findings'],
        additionalProperties: false,
      },
    },
  },
  messages: [{ role: 'user', content: prompt }],
});

Puntos a recordar:

  • La forma vigente es output_config.format con {"type": "json_schema", "schema": {...}}. El antiguo campo output_format y la cabecera beta structured-outputs-2025-11-13 solo funcionan durante un periodo de transición.
  • En definiciones de tools, el equivalente es strict: true como campo top-level de la tool, que exige additionalProperties: false y required en el schema de entrada.
  • Con structured outputs, la sección FORMATO DE RESPUESTA del prompt pasa a ser redundante: el prompt se queda con los criterios, y la forma la impone la API.

Control de esfuerzo en prompts de CI

El nivel de esfuerzo es la palanca directa de calidad/coste en revisiones automatizadas. Es GA, sin cabecera beta, y va anidado en output_config:

const message = await client.messages.create({
  model: 'claude-opus-4-8',
  max_tokens: 2048,
  output_config: {
    effort: 'xhigh', // low | medium | high | xhigh | max
    format: { type: 'json_schema', schema: reviewSchema },
  },
  messages: [{ role: 'user', content: prompt }],
});
  • El default es high; xhigh es el nivel recomendado para coding y agentes.
  • Bajar a low/medium es la forma correcta de abaratar un pase de triage masivo, en vez de degradar el prompt.

Sobre thinking: thinking: {type: "enabled", budget_tokens: N} fue eliminado en Fable 5, Opus 4.8/4.7 y Sonnet 5 (devuelve 400). El sustituto es thinking: {type: "adaptive"}, cuyo display por defecto es "omitted" (los bloques de thinking llegan vacíos); para razonamiento visible hay que pedir display: "summarized".

Minimizar false positives: la regla “be conservative”

Incluir explícitamente “en caso de duda, no reportes” reduce los falsos positivos porque Claude entiende que el costo de un falso positivo (bloquear un PR legítimo) es mayor que el costo de un falso negativo en este contexto.

Esto es diferente a un contexto de auditoría de seguridad donde quieres máxima sensibilidad.


5. Multi-instance y multi-pass review

Por qué self-review es limitado

Cuando Claude revisa su propio output, retiene el razonamiento original. Si ese razonamiento tenía un error, es probable que el mismo error pase la revisión.

flowchart TD
    A[Claude genera código] --> B[Claude se auto-revisa]
    B --> C{¿Encuentra error?}
    C -->|Razonamiento incorrecto retenido| D[Error pasa la revisión]
    C -->|Razonamiento correcto| E[Error detectado]
    style D fill:#ff6b6b
    style E fill:#51cf66

La guía de diseño de harnesses de larga duración de Anthropic llega a la misma conclusión y la formula como recomendación de arquitectura: separar generador de evaluador, porque «afinar un evaluador escéptico independiente es mucho más tratable que hacer que un generador critique su propio trabajo». La autoevaluación de un solo agente aparece listada explícitamente como antipatrón.

Multi-pass: dividir por responsabilidad

En lugar de un solo prompt “revisa todo esto”, divide la revisión en passes con responsabilidades distintas:

async function multiPassReview(files: Record<string, string>) {
  const client = new Anthropic();

  // Pass 1: seguridad (alta prioridad, criterios estrictos)
  const securityPass = await client.messages.create({
    model: 'claude-opus-4-8',
    max_tokens: 2048,
    messages: [{
      role: 'user',
      content: `SOLO analiza seguridad. Criterios: [lista exacta]

      Archivos: ${JSON.stringify(files)}`
    }]
  });

  // Pass 2: correctitud lógica (independiente del pass 1)
  const logicPass = await client.messages.create({
    model: 'claude-opus-4-8',
    max_tokens: 2048,
    messages: [{
      role: 'user',
      content: `SOLO analiza lógica de negocio y casos edge.
      No revises seguridad ni estilo.

      Archivos: ${JSON.stringify(files)}`
    }]
  });

  return { security: securityPass, logic: logicPass };
}

Independent instances para verificación cruzada

Para decisiones críticas, usa dos instancias independientes y cruza resultados:

flowchart LR
    A[Código a revisar] --> B[Instancia A\nrevisor de seguridad]
    A --> C[Instancia B\nrevisor independiente]
    B --> D[Hallazgos A]
    C --> E[Hallazgos B]
    D --> F{Intersección}
    E --> F
    F -->|Ambas instancias coinciden| G[Alta confianza]
    F -->|Solo una instancia reporta| H[Revisión manual]
async function crossReview(code: string): Promise<{
  highConfidence: string[];
  needsManualReview: string[];
}> {
  const client = new Anthropic();

  const [reviewA, reviewB] = await Promise.all([
    client.messages.create({
      model: 'claude-opus-4-8',
      max_tokens: 1024,
      messages: [{ role: 'user', content: `Revisor A: ${code}` }],
      system: 'Eres el revisor A. Reporta hallazgos en formato JSON array de strings.',
    }),
    client.messages.create({
      model: 'claude-opus-4-8',
      max_tokens: 1024,
      messages: [{ role: 'user', content: `Revisor B: ${code}` }],
      system: 'Eres el revisor B. Reporta hallazgos en formato JSON array de strings.',
    }),
  ]);

  const findingsA: string[] = JSON.parse(
    reviewA.content[0].type === 'text' ? reviewA.content[0].text : '[]'
  );
  const findingsB: string[] = JSON.parse(
    reviewB.content[0].type === 'text' ? reviewB.content[0].text : '[]'
  );

  // Simplificado: en producción usar similitud semántica
  const highConfidence = findingsA.filter((f) => findingsB.includes(f));
  const needsManualReview = [
    ...findingsA.filter((f) => !findingsB.includes(f)),
    ...findingsB.filter((f) => !findingsA.includes(f)),
  ];

  return { highConfidence, needsManualReview };
}

Advertencia: un revisor siempre encuentra algo

La verificación cruzada tiene un sesgo conocido, documentado en las best practices de Claude Code: «A reviewer prompted to find gaps will usually report some, even when the work is sound… Chasing every finding leads to over-engineering».

Por eso el prompt del revisor no debe pedir «encuentra problemas» a secas, sino acotar el mandato:

Revisa el diff contra el plan declarado.
Marca ÚNICAMENTE gaps que afecten:
- Corrección (el código no hace lo que dice hacer)
- Requisitos declarados en el plan que no están implementados

No marques mejoras de diseño, refactors sugeridos ni cobertura adicional
de tests que no esté en los requisitos.
Si el diff cumple el plan, responde "Sin gaps".

Esto es el mismo principio de la sección 1 —criterios categóricos— aplicado al rol de revisor: sin un criterio explícito de qué cuenta como gap, la instancia revisora fabrica hallazgos para justificar su existencia.

Flujo completo de multi-pass review

sequenceDiagram
    participant CI as CI/CD Pipeline
    participant P1 as Pass 1: Seguridad
    participant P2 as Pass 2: Lógica
    participant P3 as Pass 3: Cross-verify críticos
    participant R as Resultado final

    CI->>P1: diff del PR
    P1-->>CI: hallazgos de seguridad
    CI->>P2: diff del PR (independiente)
    P2-->>CI: hallazgos de lógica
    CI->>P3: solo hallazgos CRÍTICOS de P1+P2
    P3-->>CI: confirmación o descarte
    CI->>R: merge de hallazgos confirmados

6. Batch processing con Message Batches API

Cuándo usar batch vs sincrónico

flowchart TD
    A{¿Necesitas resultado\nen menos de 1 hora?} -->|Sí| B[API sincrónica]
    A -->|No| C{¿Tienes más de\n10 requests similares?}
    C -->|No| B
    C -->|Sí| D[Message Batches API]
    D --> E[50% ahorro de costo]
    D --> F[Ventana de 24 horas]
    B --> G[Resultado inmediato\ncosto completo]

Características clave de Message Batches API

CaracterísticaValor
Ahorro de costo50% vs API sincrónica
Tiempo de procesamientoHasta 24 horas
Tamaño máximo de batch10,000 requests
Soporte multi-turnNo soportado
custom_idRequerido para correlación

Elección de modelo en volumen: claude-opus-4-8 (contexto 1M, salida 128K, $5/$25 por MTok, cutoff enero 2026) es el default recomendado. Para lotes grandes donde el coste manda, claude-sonnet-5 cuesta $3/$15 con precio introductorio de $2/$10 hasta el 31 de agosto de 2026, con la misma ventana de 1M tokens. El 50% de ahorro del batch se aplica sobre esos precios.

Implementación completa

import Anthropic from '@anthropic-ai/sdk';

const client = new Anthropic();

interface CodeFile {
  path: string;
  content: string;
}

// Paso 1: crear el batch
async function createReviewBatch(files: CodeFile[]): Promise<string> {
  const requests = files.map((file) => ({
    custom_id: `review-${file.path.replace(/\//g, '-')}`,
    params: {
      model: 'claude-opus-4-8' as const,
      max_tokens: 1024,
      messages: [
        {
          role: 'user' as const,
          content: `Revisa este archivo con criterios de seguridad estrictos.
Responde solo con JSON: {"findings": [], "status": "CLEAN"|"ISSUES"}

Archivo: ${file.path}
\`\`\`
${file.content}
\`\`\``,
        },
      ],
    },
  }));

  const batch = await client.beta.messages.batches.create({ requests });
  console.log(`Batch creado: ${batch.id}`);
  return batch.id;
}

// Paso 2: verificar estado del batch
async function checkBatchStatus(batchId: string) {
  const batch = await client.beta.messages.batches.retrieve(batchId);
  console.log(`Estado: ${batch.processing_status}`);
  console.log(`Completados: ${batch.request_counts.succeeded}`);
  console.log(`Fallidos: ${batch.request_counts.errored}`);
  return batch;
}

// Paso 3: obtener resultados y resubmitir fallidos
async function processBatchResults(batchId: string): Promise<{
  results: Record<string, unknown>;
  failed: string[];
}> {
  const results: Record<string, unknown> = {};
  const failed: string[] = [];

  for await (const result of await client.beta.messages.batches.results(batchId)) {
    if (result.result.type === 'succeeded') {
      const content = result.result.message.content[0];
      if (content.type === 'text') {
        try {
          results[result.custom_id] = JSON.parse(content.text);
        } catch {
          failed.push(result.custom_id);
        }
      }
    } else {
      // errored o expired
      failed.push(result.custom_id);
      console.warn(`Falló ${result.custom_id}: ${result.result.type}`);
    }
  }

  return { results, failed };
}

// Uso completo
async function batchReviewPipeline(files: CodeFile[]) {
  // Refinamiento: primero revisar una muestra sincrónicamente
  const sample = files.slice(0, 3);
  console.log('Refinando prompt con muestra de 3 archivos...');

  // Validar que el prompt produce resultados parseables
  for (const file of sample) {
    const response = await client.messages.create({
      model: 'claude-opus-4-8',
      max_tokens: 1024,
      messages: [
        {
          role: 'user',
          content: `Revisa: ${file.path}\n${file.content}`,
        },
      ],
    });
    console.log(`Muestra OK: ${file.path}`);
  }

  // Ahora enviar el batch completo
  const batchId = await createReviewBatch(files);

  // Polling simple (en producción usar webhook o job scheduler)
  let batch = await checkBatchStatus(batchId);
  while (batch.processing_status === 'in_progress') {
    await new Promise((resolve) => setTimeout(resolve, 60_000)); // 1 min
    batch = await checkBatchStatus(batchId);
  }

  const { results, failed } = await processBatchResults(batchId);

  // Resubmitir fallidos sincrónicamente
  if (failed.length > 0) {
    console.log(`Resubmitiendo ${failed.length} requests fallidos...`);
    const failedFiles = files.filter((f) =>
      failed.includes(`review-${f.path.replace(/\//g, '-')}`)
    );
    for (const file of failedFiles) {
      const response = await client.messages.create({
        model: 'claude-opus-4-8',
        max_tokens: 1024,
        messages: [{ role: 'user', content: file.content }],
      });
      const content = response.content[0];
      if (content.type === 'text') {
        results[file.path] = JSON.parse(content.text);
      }
    }
  }

  return results;
}

SLA constraints: batch puede no ser adecuado

Si tienes un SLA de respuesta, evalúa la viabilidad de batch:

flowchart TD
    A[SLA requerido] --> B{¿Menos de 4 horas?}
    B -->|Sí| C[API sincrónica\no streaming]
    B -->|No| D{¿Menos de 24 horas?}
    D -->|Sí| E[Batch con monitoreo\n+ fallback sincrónico]
    D -->|No| F[Batch sin restricciones\nmáximo ahorro]
    E --> G{¿Batch terminó\na tiempo?}
    G -->|No| H[Fallback: sincrónico\npara los pendientes]
    G -->|Sí| I[Resultado batch]

Ejemplo: pipeline de auditoría nocturna de 500 archivos

  • Costo sincrónico estimado: $50
  • Costo con batch: $25
  • Tiempo de procesamiento: 2-8 horas (bien dentro de una ventana nocturna)
  • Resultado disponible para el equipo al inicio de la jornada

Contra-ejemplo: notificación en tiempo real al usuario

  • SLA: respuesta en 5 segundos
  • Batch: incorrecto, puede tardar horas
  • Usar: API sincrónica con streaming

Flujo recomendado: sample refinement → batch completo

sequenceDiagram
    participant Dev as Developer
    participant API as Claude API sync
    participant Batch as Batch API

    Dev->>API: 3-5 archivos de muestra
    API-->>Dev: resultados + validación de formato
    Dev->>Dev: ajustar prompt si es necesario
    Dev->>Batch: batch completo (500 archivos)
    Note over Batch: procesamiento async\nhasta 24 horas
    Batch-->>Dev: resultados con custom_id
    Dev->>Dev: procesar resultados\nresubmitir fallidos

El paso de refinamiento con muestra sincrónica es crítico: detecta errores de prompt antes de enviar el batch completo. Un error de parseo en el sample te ahorra horas de espera y el costo del batch entero.


Resumen: decisiones de diseño de prompt

flowchart TD
    A[¿Outputs inconsistentes?] -->|Sí| B[Agregar few-shot examples]
    A -->|No| C[¿Falsos positivos altos?]
    C -->|Sí| D[Agregar criterios categóricos\n+ 'be conservative']
    C -->|No| E[¿Términos ambiguos?]
    E -->|Sí| F[Agregar ejemplos de edge cases\ny decisiones borderline]
    E -->|No| G[¿Pipeline de CI/CD?]
    G -->|Sí| H[Formato JSON parseable\n+ fail-safe]
    G -->|No| I[¿Muchos archivos similares?]
    I -->|Sí| J{¿SLA < 4 horas?}
    J -->|Sí| K[API sincrónica]
    J -->|No| L[Batch API\n50% ahorro]
TécnicaProblema que resuelveCosto
Criterios categóricosFalse positivesPrompt más largo
Few-shot examplesInconsistencia de formatoTokens adicionales por ejemplos
Ejemplos de edge casesAmbigüedad en casos borderlineMás ejemplos en el prompt
Structured outputs (output_config.format)JSON no parseable / prompt frágilDefinir y mantener el schema
output_config.effortCalidad insuficiente o coste excesivoxhigh/max cuestan más tokens
Multi-pass reviewSelf-review sesgadoMúltiples llamadas API
Independent instancesVerificación cruzada2x costo para hallazgos críticos
Batch APICosto en volumenLatencia de hasta 24 horas

Siguiente: Structured Output