DEV M4 · Production Engineering, Evals & Security — Resumen de estudio
Curso 4 de 5 · 22 pantallas
Cómo probar que una feature aguanta tráfico real: evals, tests y tracing, manejo de fallos, selección de modelo, costo y orquestación, y seguridad. Cada capa cierra una forma en que el desarrollo esconde lo que producción revela.
1. Evals y jueces
- Un eval es un conjunto de casos de entrada + comportamiento esperado + calificación → un puntaje sobre un holdout set. Convierte "ya está listo" de una impresión en un número.
- Escríbelo antes: definir el comportamiento esperado te obliga a decidir qué es el éxito mientras el diseño todavía puede cambiar.
- Que el primer intento saque 2–3/10 es normal. Lo que importa es que suba al cambiar el prompt, las tools o el modelo, una cosa a la vez.
El grader según la forma del output:
| Tarea | Método | Detecta | Falla en |
|---|---|---|---|
| Una sola respuesta correcta | Exact/string match | Respuesta incorrecta, costo casi cero | Cualquier paráfrasis o reordenamiento |
| Output estructurado o código | Code-graded (parsear JSON o Python, rangos, campos requeridos) | Formato y sintaxis | No dice si el contenido es bueno |
| Calidad abierta | LLM-as-judge | Fidelidad, seguimiento de instrucciones, completitud, tono | Ruidoso y caro; el número no significa nada hasta que lo calibras |
- El juez: pídele strengths, weaknesses y reasoning antes del score. Sin eso, el puntaje tiende a un "6" seguro.
- Calibración: corre el juez sobre casos ya etiquetados por humanos y mide el acuerdo. Si es bajo → ajusta la rúbrica (define qué significa cada puntaje, agrega ejemplos buenos y malos) y vuelve a medir.
- Costo: code checks en cada commit; el juez (una llamada por caso) en una pasada periódica programada.
- Cobertura > perfección: 20 casos con edge inputs detectan más que 3 perfectos. Claude puede generar casos a partir de un set etiquetado; revísalos por muestreo.
- Loop: meta → prompt → eval → leer los fallos → un cambio → re-eval. Mira el desglose por caso: un promedio estable puede esconder 3 arreglados y 3 rotos.
- Categoría del fallo:
- Formato → instrucciones de salida del prompt.
- Dato incorrecto en contenido recuperado → retrieval.
- Solo falla con inputs largos → manejo de contexto.
⚠️ Watch out: "I placed my order on March 3 but did not receive it until April 12" → extrajo April 12. La validación confirma la forma, no el valor correcto. No había un set calificado con mensajes de dos fechas. Pide al modelo que enumere edge cases (dos fechas, ninguna fecha, "next Tuesday") y conviértelos en casos calificados.
Checkpoint (eval de resúmenes):
- Esperado en la transcripción de reunión: las 3 action items con sus responsables.
- Esperado en el bug report: el bug + los pasos de reproducción, sin el comentario ajeno.
- Bandas del juez: 1–3 = omite el contenido requerido · 4–7 = parcial · 8–10 = completo y fiel.
2. Testing y tracing
| Nivel | Aísla | No detecta |
|---|---|---|
| Unit | Una función (parser, wrapper de una tool) | Cómo encajan las piezas |
| Functional | Que una llamada a Claude devuelva la forma esperada | El sistema alrededor |
| Integration | El punto de unión entre dos componentes (retrieval → modelo). Aquí se esconden la mayoría de los fallos silenciosos | El comportamiento de todo el flujo |
| End-to-end | El flujo completo como lo usa un usuario | Dónde está el fallo |
- Trace: el prompt, las tool calls, los outputs intermedios y los tiempos de cada paso. Convierte "falló el caso" en "paso 4: el parser lanzó
KeyError: amount". Es la diferencia entre un fix de 5 minutos y un día de investigación. - Router de retrieval: una clasificación barata decide entre "lookup" →
fetch_oncey "multi_step" →agentic_search. Úsalo solo si el tráfico es mixto.
⚠️ Watch out: el unit y el functional pasaban y el e2e fallaba.
retrieve()devolvía[{"content": ...}]ybuild_prompt()esperaba un string, así que el modelo respondía de memoria. Nunca se definió el contrato de formato en esa unión.
Checkpoint: C. Extraer c["content"], alinear la unión y añadir un integration test sobre retrieve() → build_prompt().
3. Manejo de fallos
La primera pregunta: ¿esperar y reintentar podría funcionar?
RETRIABLE = {429, 529, 500, 502, 503, 504} # rate limit, overload, transitorios
TERMINAL = {400, 401, 403, 404} # bad request, auth, permisos, no existe
- Un timeout suele ser retriable, pero si se repite en requests caros, arregla el request.
- Ante la duda, trátalo como terminal: falla visiblemente y se arregla. Un falso "retriable" satura el servicio y esconde el problema.
- El SDK ya reintenta errores transitorios con backoff. No envuelvas un retry en otro retry: decide qué capa es dueña de los reintentos.
- Header
retry-after(en 429 y 529): es el tiempo de espera autoritativo. El backoff exponencial es solo el fallback. - Errores de tools: devuélvelos con
is_error: truey el mensaje. Nunca un resultado vacío: el modelo lo toma como dato válido y da una respuesta segura pero equivocada. - Refusal: llega como HTTP 200 con
stop_reason: "refusal". Tu clasificador por status no lo detecta. No lo reintentes: súbelo al llamador y regístralo.
| Error | Tipo | Backoff | Fallback |
|---|---|---|---|
| 429 rate limit | Retriable | Exponencial + jitter, respeta retry-after, intentos con tope | Error limpio o resultado cacheado o más simple |
| 529 overloaded | Retriable | Backoff (carga del lado de Anthropic, no es rate limit) | Ruta alternativa o error elegante |
| 400 | Falla rápido | Ninguno | Arreglar o rechazar el input |
| Error de tool | Depende | Solo si la causa es transitoria | is_error de vuelta a Claude |
| Refusal (200) | Falla rápido | Ninguno | Subirlo y registrarlo |
⚠️ Watch out: un loop sin manejo de errores cayó con el primer 429. Luego se pusieron retries inmediatos, que agravaron el rate limit.
Checkpoint (reparar el retry): el defecto es time.sleep(0) (sin backoff) y que reintenta cualquier excepción. Fix:
RateLimitError→ usarretry-afteromin(cap, 2**attempt) + jitter.APIStatusErrorterminal →raise(fallar rápido).- Retriable → backoff con tope.
- Al final,
RetryBudgetExhausted.
4. Selección de modelo en producción
- Fable (lo más exigente) > Opus > Sonnet (default) > Haiku (velocidad y costo). El mismo prompt corre en cualquiera.
- Subir de tier = más calidad, más costo y normalmente más latencia. Un modelo superior puede salir más barato si termina en menos tokens. Incluye el costo de un error en la cuenta.
- Error más común y más caro: usar el modelo más capaz por defecto.
- Routing: un modelo default + override según una señal barata (tipo de tarea, longitud, dificultad).
- Sube de tier si un eval muestra fallos en los casos más difíciles y el error es caro. Baja si un eval muestra que el modelo barato mantiene la calidad.
Checkpoint:
- Millones de mensajes cortos y Haiku cumple en el eval → Haiku (costo a volumen).
- Refactor de varios pasos, un error temprano es caro y Sonnet falla los casos difíciles → Opus (calidad cuando equivocarse cuesta mucho).
- Tráfico mixto → router: Sonnet o Haiku por defecto + override a Opus en los requests complejos.
5. Costo y orquestación
- Instrumenta cada llamada desde el inicio: tokens de input y output, latencia y error rate. Un flujo que parece caro de forma uniforme suele tener un paso con el 90% del gasto.
- Palancas:
- Modelo: el capaz solo donde hace falta.
- Tamaño de prompt y contexto: recorta outputs de tools innecesarios.
- Número de tool calls.
- Streaming: mejora la latencia percibida (primer token a los ~300 ms).
- Batch.
- Caching.
- Streaming con tools: acumula
input_json_deltapor índice y parsea el JSON solo cuando el stream cierra. Un stream que se corta es transitorio: reintenta el request completo. - Prompt caching:
- Escribir cuesta 1.25× (TTL 5 min) o 2× (TTL 1 h); leer cuesta 0.1×. Solo conviene si hay más lecturas que escrituras.
- Modo automático (un flag en la raíz del request; recomendado para empezar) o breakpoints explícitos con
cache_control. - 3 condiciones: contenido idéntico (agregar "please" lo invalida), que se repita pronto (5 min, se renueva en cada hit) y longitud mínima (varía por modelo).
- Riesgo: ventana de inconsistencia si el prefijo refleja datos que cambian.
- Message Batches API: ~50% más barato, asíncrono. Para trabajo no urgente y programado. Batch + caching se acumulan.
- Orchestrator-worker: el lead planea, los subagents corren en paralelo y el lead sintetiza.
- ~15× los tokens de un chat normal (Anthropic: Opus 4 como lead + Sonnet 4 como subagents superó claramente a un solo Opus 4 en sus evals internos).
- La varianza de rendimiento se explica sobre todo por el uso de tokens.
- Solo sirve si la tarea se divide en partes independientes (investigación amplia). Malo para trabajo acoplado como programar.
- Piensa en él como una contratación: cinco investigadores terminan antes, pero pagas cinco sueldos.
- Usa un lead capaz y workers baratos.
- Multiplica los puntos de fallo: cada subagent necesita su propio retry y backoff.
- Piso de confiabilidad: primero fija el presupuesto de reintentos y el techo de latencia; después optimiza el costo por encima de ese piso. (El costo siempre presiona más fuerte.) Un eval con baseline fijo hace cumplible el piso.
⚠️ Watch out: un fan-out a subagents en una tarea secuencial → la factura se triplicó y la calidad casi no cambió. Los subagents se esperaban entre sí.
Checkpoint: lookup puntual en un corpus estable = C (single agent, modelo chico, fetch once) · investigación amplia divisible = A (orchestrator-worker) · respuesta que debe sentirse instantánea = D (streaming) · batch no urgente y sensible al costo = B (Batches + caching).
6. Seguridad
Prompt injection
- El modelo lee todo su contexto como un solo flujo de tokens. No hay una frontera interna entre instrucciones confiables y datos no confiables.
- Instrucciones escondidas en páginas, documentos, emails, registros de base de datos o outputs de tools pueden tratarse como órdenes. Pueden ser indirectas (plantadas para después) o estar escondidas (texto blanco, imágenes).
- Defensa: trata el contenido traído de fuera como datos, no como instrucciones.
- Los delimitadores ayudan, pero son una frontera blanda.
- Anthropic entrena al modelo y aplica clasificadores, pero ningún agente que lee contenido no confiable es inmune.
- La frontera fiable no está en el texto, sino en lo que el agente tiene permitido hacer.
- Jailbreak (saltarse la seguridad del modelo) ≠ injection (secuestrar las instrucciones de tu app). Se defienden igual: validar la entrada y limitar la acción.
Identidad y acceso
- Least privilege: el permiso mínimo que permite hacer el trabajo. Es el control que se mantiene aunque todo lo demás falle: una inyección exitosa queda en "acción denegada + entrada en el log".
- Secretos en variables de entorno o un secret manager, nunca en config commiteada (queda en el historial; no se puede rotar si está en el código).
- Protege la configuración de auth: lo que puede ampliar los permisos puede quitar el control.
Hooks como guardrails
- PreToolUse que deniega
write_filefuera de/workspace/output(permissionDecision: "deny") y registra en el audit log tanto lo bloqueado como lo permitido. - Precedencia: deny > ask > allow.
- Una regla en un prompt no es un control de seguridad; un hook sí.
Revisión regulada
3 preguntas tempranas: ¿dónde se procesan los datos?, ¿cómo se registra el acceso? (audit log del hook), ¿se puede controlar la configuración centralmente? (managed config).
- ZDR: la elegibilidad varía por modelo y por plataforma. Los modelos más nuevos pueden no tenerla confirmada. Verifica en el Trust Center al hacer el scoping.
Sandbox a nivel de SO (el control residual)
Aislamiento de filesystem (solo el directorio de trabajo) y de red (solo endpoints permitidos), impuesto por el sistema operativo. Se mantiene aunque falte un hook o esté mal configurado. Es lo primero que pregunta un revisor de seguridad. Se configura en los settings de Claude Code.
| Amenaza | Entra por | Control | Log |
|---|---|---|---|
| Prompt injection | Páginas, docs o tool results | Contenido como datos + hook que rechaza acciones disparadas por input no confiable | La fuente, la acción intentada y el bloqueo |
| Jailbreak | El prompt del usuario | Validar la entrada + limitar lo que el modelo puede hacer | El prompt marcado y el rechazo |
| Acceso demasiado amplio | Identidad con más permisos de los necesarios | Least privilege, secret manager, config de auth bloqueada | Cada acción privilegiada y su identidad |
| Escape del sandbox | Acceso de red o archivos fuera del límite | Sandbox de SO | Cada intento denegado |
⚠️ Watch out: "los usuarios son internos, no validamos las páginas". La instrucción venía de la página, no del usuario, y el agente escribió un archivo que nadie pidió. Fix: contenido como datos + hook delante de la tool de escritura.
Checkpoint (configuración mínima segura):
- Hook PreToolUse que deniega escrituras fuera de
/workspace/output. - Deny paths explícitos (
/etc,/secrets,~/.aws). - Secreto desde
os.environ. - Audit log en cada acción privilegiada.
Tarea acumulativa (3 defectos, 3 capas)
- Eval:
answer()no tiene un set calificado → ninguna regresión se detecta al cambiar el prompt o el modelo. Fix: holdout set calificado en cada cambio. - Errores y costo: loop de 5 intentos con
time.sleep(0)que reintenta todo, incluidos los 400. Fix:call_with_retrycon backoff,retry-aftery fallo rápido en terminales. - Seguridad:
write_file(page.suggested_path, ...)usa una ruta sacada del contenido no confiable. Fix: ruta fija/workspace/output/summary.txt+ hook que deniega y audita.
Para recordar
- Fija el estándar (eval) antes de construir; el grader según el output; calibra al juez.
- El test según el fallo (integration para los puntos de unión) y el trace para saber dónde ocurrió.
- Clasifica cada fallo (retriable vs terminal), backoff con tope,
is_errory fallbacks con nombre. - Mide costo y latencia por llamada; fan-out solo si la tarea de verdad se divide (~15×).
- Contenido traído de fuera = datos; least privilege; hooks que bloquean y registran.