Cap 21: Prompt Engineering & Structured 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.logcomo “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-5',
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:
- Determinismo: misma entrada → misma categoría de salida
- Bajo false positive: mejor perder un positivo que generar ruido
- Formato parseable: el pipeline necesita leer el resultado programáticamente
- Fail-safe: si Claude no está seguro, no bloquear el pipeline
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-5',
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;
}
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
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-5',
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-5',
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-5',
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-5',
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 };
}
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ística | Valor |
|---|---|
| Ahorro de costo | 50% vs API sincrónica |
| Tiempo de procesamiento | Hasta 24 horas |
| Tamaño máximo de batch | 10,000 requests |
| Soporte multi-turn | No soportado |
| custom_id | Requerido para correlación |
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-5' 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-5',
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-5',
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écnica | Problema que resuelve | Costo |
|---|---|---|
| Criterios categóricos | False positives | Prompt más largo |
| Few-shot examples | Inconsistencia de formato | Tokens adicionales por ejemplos |
| Ejemplos de edge cases | Ambigüedad en casos borderline | Más ejemplos en el prompt |
| Multi-pass review | Self-review sesgado | Múltiples llamadas API |
| Independent instances | Verificación cruzada | 2x costo para hallazgos críticos |
| Batch API | Costo en volumen | Latencia de hasta 24 horas |
Siguiente: Structured Output