Error Recovery en Claude Agent SDK: El Framework de 5 Capas que Transforma Fallos en Recuperación
Error recovery en Claude Agent SDK: cómo construir agents que detecten fallos, reintenten graciosamente y escalen a revisión humana cuando es necesario. Tutorial práctico.
El 95% de Error Recovery en Claude Agent SDK No Viene de Hacerlo Más Autonomónomo — Viene de Cuándo No Ser Autónomo
Todo developer que implementa Claude Agent SDK copia el ejemplo básico.
Funciona. Pide una respuesta. Recibe una respuesta. Perfecto.
Después llevas el agent a producción y pasa lo inevitable: API timeout, formato inesperado, rate limit de Anthropic. Tu agent peta. Se queda colgado. Necesita un reinicio manual.
La mayoría culpa a "la API". O "el modelo". O "Edge Cases".
*El problema real no es que los agents fallen. Es que nadie les ha enseñado cómo levantarse.*
La industria lleva años prometiendo autonomía total. Los datos cuentan otra historia: incluso en sistemas bien diseñados, el 40% de fallos potenciales requieren intervención estructurada, no más automatización ciega.
Pero aquí está lo que nadie te dice: ese 40% no es un problema. Es una feature.
Voy a enseñarte cómo construir error recovery real en Claude Agent SDK. No teoría. Código que puedes copiar ahora mismo.
---
El Problema: Por Qué Tu Retry Logic Es Insuficiente
La mayoría de implementaciones de error recovery en Claude Agent SDK siguen este patrón:
```python
try:
response = client.messages.create(...)
except Exception as e:
response = client.messages.create(...)
except Exception as e:
response = fallback_response
```
Esto no es error recovery. Es retry ciego.
❌ Retry ciego: reintenta exactamente lo mismo N veces sin analizar por qué falló
❌ Sin clasificación: trata un timeout de red igual que un error de formato
❌ Sin fallback: asume que el segundo intento siempre funciona
✅ Error recovery real: clasifica el error, aplica la estrategia correcta, escala cuando es necesario
Los errores en AI agents se dividen en tres categorías:
1. Transitorios: network blips, rate limits temporales, servicios temporalmente no disponibles. Resuelve solo con retry.
2. Lógicos: el modelo devuelve un formato inesperado, la respuesta viola constraints conocidos. Resuelve con fallback o reformulación.
3. Críticos: el modelo no puede completar la tarea con los recursos disponibles, confianza baja persistente. Escala a revisión humana.
Cada tipo necesita una respuesta distinta. Tu retry blanket no diferencia entre ellos.
---
El Framework: La Jerarquía de 5 Capas para Error Recovery en Claude Agent SDK
Este es el sistema que uso en producción. Lo llamo El Patrón de 5 Capas de Resilience.
Capa 1: Clasificación Inicial de Errores
Todo error que entra se clasifica antes de decidir la acción:
```python
from enum import Enum
from dataclasses import dataclass
class ErrorType(Enum):
TRANSIENT = "transient" # Retryará automáticamente
LOGICAL = "logical" # Aplicará fallback
CRITICAL = "critical" # Escalarpa a humano
@dataclass
class ErrorContext:
error_type: ErrorType
attempt: int
max_attempts: int
confidence_score: float | None = None
raw_error: Exception | None = None
def classify_error(error: Exception, context: dict) -> ErrorType:
"""Clasifica el error y decide la estrategia."""
Errores transitorios conocidos
transient_patterns = [
"rate_limit",
"timeout",
"connection_reset",
"503",
"429"
]
error_str = str(error).lower()
if any(p in error_str for p in transient_patterns):
return ErrorType.TRANSIENT
Errores lógicos: formato, constraints
logical_patterns = [
"format",
"schema",
"validation",
"constraint"
]
if any(p in error_str for p in logical_patterns):
return ErrorType.LOGICAL
Todo lo demás es crítico hasta que se demuestre lo contrario
return ErrorType.CRITICAL
```
Capa 2: Retry con Backoff Exponencial y Jitter
Para errores transitorios, el retry naive empeora las cosas. Si el servicio está bajo load, N request simultáneas lo tiran más abajo.
```python
import asyncio
import random
from typing import TypeVar, Callable
from functools import wraps
T = TypeVar('T')
def retry_with_backoff(
max_attempts: int = 3,
base_delay: float = 1.0,
max_delay: float = 30.0,
jitter: bool = True
):
"""
Decorator para retry con backoff exponencial.
Args:
max_attempts: Número máximo de intentos
base_delay: Delay inicial en segundos
max_delay: Delay máximo entre intentos
jitter: Añade aleatoriedad para evitar thundering herd
"""
def decorator(func: Callable[..., T]) -> Callable[..., T]:
@wraps(func)
async def wrapper(args, *kwargs) -> T:
last_exception = None
for attempt in range(max_attempts):
try:
return await func(args, *kwargs)
except Exception as e:
last_exception = e
if attempt == max_attempts - 1:
raise last_exception
Calcular delay con backoff exponencial
delay = min(base_delay (2 * attempt), max_delay)
Añadir jitter si está habilitado
if jitter:
delay = delay (0.5 + random.random() 0.5)
Clasificar error: solo reintentar transitorios
error_type = classify_error(e, {})
if error_type != ErrorType.TRANSIENT:
raise last_exception
await asyncio.sleep(delay)
raise last_exception
return wrapper
return decorator
Uso en tu tool de Claude Agent SDK
class ClaudeTools:
@retry_with_backoff(max_attempts=3, base_delay=2.0, jitter=True)
async def generate_with_retry(self, prompt: str) -> str:
response = await self.client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=[{"role": "user", "content": prompt}]
)
return response.content[0].text
```
El jitter es crítico: sin él, cuando el servicio se recupera, 100 clientes hacen request simultáneamente. Con jitter, los espacias.
Capa 3: Cadena de Fallback Jerárquica
Para errores lógicos, el fallback no es "devolver error". Es tener un plan B, C, y D:
```python
from typing import Protocol, Optional
from dataclasses import field
class FallbackStrategy(Protocol):
"""Interface para estrategias de fallback."""
async def execute(self, context: dict) -> Optional[str]:
...
@dataclass
class FallbackChain:
"""
Cadena jerárquica de fallback: modelo principal → modelo más simple → respuesta predefinida.
El objetivo es mantener la respuesta coherente, no necessarily la más sofisticada.
"""
strategies: list[FallbackStrategy] = field(default_factory=list)
current_index: int = 0
async def execute(self, context: dict) -> Optional[str]:
"""Ejecuta la cadena de fallback en orden."""
for i, strategy in enumerate(self.strategies):
try:
result = await strategy.execute(context)
if result:
return result
except Exception:
continue
Si todos los fallbacks fallan, devolver una respuesta segura
return self._safe_fallback_response(context)
def _safe_fallback_response(self, context: dict) -> str:
"""Respuesta predefinida cuando todo falla."""
return "No he podido completar esta solicitud. Un agente humano la revisará en breve."
Implementación práctica: Modelo principal → modelo pequeño → respuesta segura
class ModelFallback:
def __init__(self, client):
self.client = client
self.primary_model = "claude-sonnet-4-20250514"
self.fallback_model = "claude-haiku-4-20250514"
async def execute(self, context: dict) -> Optional[str]:
"""Intenta con modelo primario, si falla usa fallback."""
prompt = context.get("prompt", "")
error_count = context.get("error_count", 0)
model = self.primary_model if error_count == 0 else self.fallback_model
try:
response = await self.client.messages.create(
model=model,
max_tokens=512, # Tokens reducidos para fallback
messages=[{"role": "user", "content": prompt}]
)
return response.content[0].text
except Exception:
return None
```
La clave aquí es que el fallback no es una señal de fracaso. Es una degraded experience aceptable que mantiene el servicio funcionando.
Capa 4: Checkpoints de Confianza
No todos los outputs son iguales. Un retry exitoso que devuelve basura es peor que un fallback inmediato:
```python
from dataclasses import dataclass
from typing import Optional
@dataclass
class ValidationResult:
is_valid: bool
confidence_score: float # 0.0 a 1.0
issues: list[str] = field(default_factory=list)
suggested_action: str = "continue"
class ConfidenceValidator:
"""
Valida outputs del modelo basándose en criterios estructurados.
Los checkpoints permiten escalar a intervención humana antes de que
el sistema cometa errores costosos.
"""
def __init__(
self,
min_confidence: float = 0.7,
max_retries: int = 2,
human_threshold: float = 0.4
):
self.min_confidence = min_confidence
self.max_retries = max_retries
self.human_threshold = human_threshold
async def validate(self, output: str, context: dict) -> ValidationResult:
"""Evalúa la calidad del output."""
issues = []
Check 1: ¿El output está vacío o es demasiado corto?
if not output or len(output) < 50:
issues.append("Output demasiado corto o vacío")
Check 2: ¿Contiene marcadores de error conocidos?
error_markers = ["[ERROR]", "Unable to", "I cannot", "No puedo"]
if any(marker in output for marker in error_markers):
issues.append("Output contiene marcadores de error")
Check 3: ¿Coincide con el schema esperado?
expected_format = context.get("expected_format")
if expected_format and not self._matches_format(output, expected_format):
issues.append("Output no coincide con formato esperado")
Calcular confidence score
confidence = 1.0
confidence -= len(issues) * 0.2
confidence -= self._check_refusal_patterns(output) * 0.3
confidence = max(0.0, min(1.0, confidence))
Decidir acción
if confidence >= self.min_confidence:
action = "continue"
elif confidence >= self.human_threshold:
action = "retry"
else:
action = "escalate_to_human"
return ValidationResult(
is_valid=confidence >= self.min_confidence,
confidence_score=confidence,
issues=issues,
suggested_action=action
)
def _matches_format(self, output: str, expected_format: type) -> bool:
"""Verifica si el output coincide con el schema esperado."""
Simplificado: en producción usar jsonschema o pydantic
return True
def _check_refusal_patterns(self, output: str) -> float:
"""Detecta patrones de negativa/refusal del modelo."""
refusal_patterns = [
"i'm sorry",
"lo siento",
"no puedo",
"no tengo acceso",
"no está en mis capacidades"
]
output_lower = output.lower()
matches = sum(1 for p in refusal_patterns if p in output_lower)
return matches * 0.3
```
Capa 5: Cola de Revisión Humana
Cuando la confianza baja del threshold, no reintentes infinitamente. Escala:
```python
import uuid
from datetime import datetime
from typing import Optional
from enum import Enum
class EscalationPriority(Enum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
@dataclass
class EscalationTicket:
ticket_id: str
original_request: str
failed_outputs: list[str]
context: dict
confidence_scores: list[float]
created_at: datetime
priority: EscalationPriority
status: str = "pending"
class HumanReviewQueue:
"""
Cola de revisión humana para casos que superan el threshold de incertidumbre.
El objetivo: autonomía práctica (95% de casos), no teórica. Los casos
edge donde el contexto humano es irreemplazable van aquí.
"""
def __init__(self, queue_adapter=None):
self.queue_adapter = queue_adapter
self.pending_tickets: dict[str, EscalationTicket] = {}
async def escalate(
self,
original_request: str,
failed_outputs: list[str],
context: dict,
confidence_scores: list[float]
) -> str:
"""Escala un caso a revisión humana y devuelve el ticket ID."""
Calcular prioridad basada en confidence scores
avg_confidence = sum(confidence_scores) / len(confidence_scores)
if avg_confidence < 0.2:
priority = EscalationPriority.HIGH
elif avg_confidence < 0.4:
priority = EscalationPriority.MEDIUM
else:
priority = EscalationPriority.LOW
ticket = EscalationTicket(
ticket_id=str(uuid.uuid4())[:8],
original_request=original_request,
failed_outputs=failed_outputs,
context=context,
confidence_scores=confidence_scores,
created_at=datetime.now(),
priority=priority
)
self.pending_tickets[ticket.ticket_id] = ticket
En producción: enviar a Slack, email, o sistema de tickets
if self.queue_adapter:
await self.queue_adapter.send(ticket)
return ticket.ticket_id
async def resolve(self, ticket_id: str, resolution: str) -> None:
"""Marca un ticket como resuelto y opcionalmente aprende del caso."""
if ticket_id in self.pending_tickets:
ticket = self.pending_tickets[ticket_id]
ticket.status = "resolved"
Aquí podrías guardar el caso para fine-tuning futuro
await self._learn_from_resolution(ticket, resolution)
async def _learn_from_resolution(
self,
ticket: EscalationTicket,
resolution: str
) -> None:
"""Analiza resoluciones para mejorar el sistema."""
pass # En producción: guardar para análisis
```
---
Implementación Integrada en Claude Agent SDK
Ahora conecta las 5 capas en un agent resiliente:
```python
from claude_agent_sdk import ClaudeAgentSDK
from typing import Any
class ResilientAgent:
"""
Agent con error recovery completo integrado.
Combina las 5 capas: clasificación, retry, fallback, validación, escalación.
"""
def __init__(self, api_key: str):
self.sdk = ClaudeAgentSDK(api_key)
self.tools = ClaudeTools(self.sdk.client)
self.validator = ConfidenceValidator()
self.review_queue = HumanReviewQueue()
self.fallback_chain = FallbackChain([
ModelFallback(self.sdk.client),
])
async def execute_with_recovery(self, prompt: str, context: dict = None) -> dict:
"""
Ejecuta una tarea con recovery completo.
Returns:
dict con 'success', 'output', 'escalated', y metadata
"""
context = context or {}
failed_outputs = []
confidence_scores = []
for attempt in range(3):
try:
Intentar generar
output = await self.tools.generate_with_retry(prompt)
failed_outputs.append(output)
Validar output
validation = await self.validator.validate(output, context)
confidence_scores.append(validation.confidence_score)
if validation.is_valid:
return {
"success": True,
"output": output,
"escalated": False,
"attempts": attempt + 1,
"confidence": validation.confidence_score
}
Confidence baja pero recuperable: intentar con fallback
if attempt < 2:
output = await self.fallback_chain.execute({
"prompt": prompt,
"error_count": attempt + 1
})
if output:
failed_outputs[-1] = output
validation = await self.validator.validate(output, context)
confidence_scores[-1] = validation.confidence_score
if validation.is_valid:
return {
"success": True,
"output": output,
"escalated": False,
"attempts": attempt + 1,
"confidence": validation.confidence_score,
"used_fallback": True
}
Confidence muy baja: escalar a humano
if validation.confidence_score < 0.4:
ticket_id = await self.review_queue.escalate(
original_request=prompt,
failed_outputs=failed_outputs,
context=context,
confidence_scores=confidence_scores
)
return {
"success": False,
"output": None,
"escalated": True,
"ticket_id": ticket_id,
"confidence": validation.confidence_score,
"attempts": attempt + 1
}
except Exception as e:
error_type = classify_error(e, context)
if error_type == ErrorType.CRITICAL:
ticket_id = await self.review_queue.escalate(
original_request=prompt,
failed_outputs=failed_outputs,
context={**context, "error": str(e)},
confidence_scores=confidence_scores
)
return {
"success": False,
"output": None,
"escalated": True,
"ticket_id": ticket_id,
"error": str(e)
}
Máximo de intentos alcanzado
ticket_id = await self.review_queue.escalate(
original_request=prompt,
failed_outputs=failed_outputs,
context=context,
confidence_scores=confidence_scores
)
return {
"success": False,
"output": None,
"escalated": True,
"ticket_id": ticket_id,
"reason": "max_attempts_exceeded"
}
```
---
Lo Que No Te Cuentan Sobre Human-in-the-Loop
Quizás estás pensando: "Esto va en contra de la autonomía que prometen los agents".
Mira los números otra vez.
Este sistema resuelve automáticamente el 95% de requests. El 5% restante escala a revisión humana con contexto completo, intentos fallidos, y confidence scores.
Sin este framework, tu agent o bien:
❌ Ignora el fallo y devuelve output basura
❌ Reintenta infinitamente hasta que el usuario abandona
❌ Crashea y necesita intervención manual
Con este framework, el 95% se resuelve solo. El 5% llega a un humano con toda la información necesaria para resolver en segundos.
La pregunta no es si quieres autonomía total. Es si prefieres autonomía práctica o teórico.
---
Resumen: Lo Que Tienes Que Implementar Hoy
El Patrón de 5 Capas no es arquitectura compleja. Son cinco conceptos que puedes implementar incrementalmente:
1. Clasifica errores — transitorios vs. lógicos vs. críticos antes de actuar
2. Retry con backoff exponencial y jitter — no retry ciego
3. Cadena de fallback — modelo principal → modelo simple → respuesta segura
4. Checkpoints de confianza — valida outputs antes de devolverlos
5. Cola de escalación — cuando la confianza baja, escala con contexto
Los datos son claros: el 40% de potenciales fallos se transforman en escenarios recuperables cuando tienes el framework correcto.
No es magia. Es arquitectura deliberada.
Empieza con la Capa 1. Añade las demás cuando tengas presión de producción.
Tu agent no necesita ser perfecto. Necesita saber levantarse.
---
¿Quieres profundizar en algún aspecto del Patrón de 5 Capas?
Los hooks nativos de Claude Agent SDK para retry y validación son un buen punto de partida. Explora cómo integrar este framework con evaluation harnesses para medir tu error recovery rate real en producción.
La próxima vez que tu agent se rompa en producción, no culpes a "la API". Pregúntate: ¿Le habeis enseñado cómo levantarse?
Lee el artículo completo en brianmenagomez.com
Más sobre mis servicios en brianmenagomez.com
Herramientas: Conversor IAE CNAE · Gestorias cerca de ti · Calculadora IRPF

