AI in Production -- Parte 2
Diseñar para el Fallo: Qué Pasa Cuando la IA No Funciona
El Problema
Aquí va un escenario que pasa más a menudo de lo que la gente admite.
Integras una funcionalidad de IA. Funciona genial en las pruebas. Lo despliegas. Tres semanas después, a las 11 de la noche un miércoles, el proveedor de IA tiene una incidencia. Su API empieza a devolver 503. Tu funcionalidad se rompe. Pero no solo la funcionalidad — tu manejo de errores no estaba preparado para esto, así que la excepción sube hasta arriba, y ahora toda tu aplicación está devolviendo 500 a todos los usuarios. No solo a los que usaban la funcionalidad de IA. A todos.
Te llaman. Pasas una hora depurando. Añades un try-catch alrededor de la llamada a la IA. Despliegas un hotfix a medianoche. El proveedor se recupera. Todo vuelve a la normalidad. Lo achacas a mala suerte.
Y vuelve a pasar.
El problema no es el proveedor. Las incidencias pasan con cualquier API — bases de datos, sistemas de pago, servicios de terceros. El problema es que diseñaste el camino feliz y asumiste que siempre funcionaría. Con APIs tradicionales, ya has aprendido a manejar fallos. Con APIs de IA, la mayoría aún no lo ha hecho.
Las APIs de IA tienen modos de fallo específicos que son diferentes a lo que estás acostumbrado:
- Timeout: El modelo está pensando. Mucho rato. Una petición que normalmente tarda 2 segundos lleva 45 segundos y sigue.
- Rate limiting: Estás enviando demasiadas peticiones. La API devuelve 429. Tu lógica de reintentos lo empeora.
- Calidad degradada: La API devuelve 200. La respuesta parece válida. Pero la respuesta es incorrecta. Tu monitorización no lo pilla porque todo parece sano.
- Cambios en el modelo: El proveedor actualiza el modelo. El comportamiento cambia de forma sutil. Tus prompts producen resultados diferentes. No se lanza ningún error.

Cada uno necesita una respuesta diferente. Vamos a construirlo.
La Solución
Los patrones de aquí no son nuevos. Los ingenieros de sistemas distribuidos llevan décadas lidiando con dependencias no fiables. La diferencia es aplicarlos a un componente de IA, que tiene algunas particularidades que vale la pena entender.
Las cuatro capas de una integración de IA resiliente:
1. Timeout — Pon un límite duro de cuánto tiempo vas a esperar. Si la IA no ha respondido en N segundos, abandona. No dejes que una petición lenta bloquee un hilo para siempre.
2. Retry con backoff — Para fallos transitorios (un corte de red, una sobrecarga breve), reintenta automáticamente. Pero usa backoff exponencial — no machaques una API que ya está sufriendo. Y cuidado: reintentar una petición que devolvió una respuesta incorrecta no va a ayudar. Reintenta solo con códigos de error específicos (429, 503), no con respuestas malas.
3. Circuit breaker — Después de un cierto número de fallos en una ventana de tiempo, deja de llamar a la IA por completo. No sigas intentándolo. Abre el circuito. Espera. Luego vuelve a intentar despacio. Esto protege tanto a tu sistema como al proveedor.

4. Fallback — Cuando el circuito está abierto, ¿qué haces? Esta es la decisión arquitectónica que la mayoría de equipos se saltan. Opciones: devolver una respuesta cacheada anterior, devolver un valor por defecto estático, mostrar un mensaje de “esta funcionalidad no está disponible temporalmente”, o redirigir a un proveedor secundario. La elección correcta depende de la funcionalidad.
Juntos, estos te dan modo degradado: la funcionalidad de IA no está disponible, pero el resto de la aplicación sigue funcionando, y los usuarios tienen una experiencia razonable en lugar de una página de error.
Ejecución
Vamos a usar .NET con Polly, la librería de resiliencia estándar para .NET. Polly tiene una API ResiliencePipeline que te permite componer estas estrategias de forma limpia.
Primero, añade los paquetes:
dotnet add package Microsoft.Extensions.Http.Resilience
dotnet add package Polly.Extensions
La interfaz del servicio de IA
Empieza con una abstracción limpia. Al resto de tu aplicación no le debería importar si la IA está disponible o no — simplemente pide un resultado.
public interface IAiSummaryService
{
Task<string?> SummarizeAsync(string text, CancellationToken cancellationToken = default);
}
string? importa aquí. Null significa “la IA no estaba disponible, gestiona esto más arriba.” No lances excepciones — lanzar excepciones obliga a cada llamador a añadir try-catch. Devuelve null y deja que el llamador decida qué mostrar.
El pipeline de resiliencia
using Polly;
using Polly.CircuitBreaker;
using Polly.Retry;
using Polly.Timeout;
public static class ResiliencePipelines
{
public static ResiliencePipeline<string?> BuildAiPipeline() =>
new ResiliencePipelineBuilder<string?>()
// 1. Timeout: give up after 10 seconds
.AddTimeout(new TimeoutStrategyOptions
{
Timeout = TimeSpan.FromSeconds(10)
})
// 2. Retry: up to 2 retries on 429/503, with exponential backoff
.AddRetry(new RetryStrategyOptions<string?>
{
MaxRetryAttempts = 2,
Delay = TimeSpan.FromSeconds(1),
BackoffType = DelayBackoffType.Exponential,
ShouldHandle = new PredicateBuilder<string?>()
.Handle<HttpRequestException>(ex =>
ex.StatusCode is System.Net.HttpStatusCode.TooManyRequests
or System.Net.HttpStatusCode.ServiceUnavailable)
})
// 3. Circuit breaker: open after 5 failures in 30 seconds
// Half-open after 15 seconds: try one request to check recovery
.AddCircuitBreaker(new CircuitBreakerStrategyOptions<string?>
{
FailureRatio = 0.5,
SamplingDuration = TimeSpan.FromSeconds(30),
MinimumThroughput = 5,
BreakDuration = TimeSpan.FromSeconds(15),
ShouldHandle = new PredicateBuilder<string?>()
.Handle<HttpRequestException>()
.Handle<TimeoutRejectedException>()
})
.Build();
}
La implementación del servicio
public class AiSummaryService : IAiSummaryService
{
private readonly HttpClient _http;
private readonly ResiliencePipeline<string?> _pipeline;
private readonly ILogger<AiSummaryService> _logger;
public AiSummaryService(
IHttpClientFactory factory,
ILogger<AiSummaryService> logger)
{
_http = factory.CreateClient("ai");
_pipeline = ResiliencePipelines.BuildAiPipeline();
_logger = logger;
}
public async Task<string?> SummarizeAsync(
string text,
CancellationToken cancellationToken = default)
{
try
{
return await _pipeline.ExecuteAsync(async ct =>
{
var response = await _http.PostAsJsonAsync(
"/summarize",
new { text },
ct);
response.EnsureSuccessStatusCode();
var result = await response.Content
.ReadFromJsonAsync<SummaryResponse>(ct);
return result?.Summary;
}, cancellationToken);
}
catch (BrokenCircuitException)
{
// Circuit is open — AI is known to be unavailable
_logger.LogWarning("AI circuit open. Returning null.");
return null;
}
catch (Exception ex)
{
// Exhausted retries or unexpected error
_logger.LogError(ex, "AI summarization failed after retries.");
return null;
}
}
}
record SummaryResponse(string Summary);
Conectándolo todo
// Program.cs
builder.Services.AddHttpClient("ai", client =>
{
client.BaseAddress = new Uri(builder.Configuration["Ai:BaseUrl"]
?? throw new InvalidOperationException("Ai:BaseUrl is required."));
client.DefaultRequestHeaders.Add(
"Authorization",
$"Bearer {builder.Configuration["Ai:ApiKey"]}");
// HttpClient timeout is the outer safety net.
// Set it higher than the Polly timeout so Polly fires first.
client.Timeout = TimeSpan.FromSeconds(15);
});
builder.Services.AddScoped<IAiSummaryService, AiSummaryService>();
El llamador: modo degradado en la práctica
[HttpGet("{id}/summary")]
public async Task<IActionResult> GetSummary(
Guid id,
CancellationToken cancellationToken)
{
var document = await _documents.GetByIdAsync(id, cancellationToken);
if (document is null) return NotFound();
var summary = await _ai.SummarizeAsync(document.Body, cancellationToken);
return Ok(new
{
document.Id,
document.Title,
// Null means AI was unavailable. Client handles this.
Summary = summary,
AiAvailable = summary is not null
});
}
El cliente recibe una respuesta válida en cualquier caso. Si AiAvailable es false, el frontend muestra un mensaje como “Resumen temporalmente no disponible” en vez de romperse. El usuario aún puede leer el documento.
Eso es modo degradado. La funcionalidad ha desaparecido. La aplicación no.

Probándolo
No esperes a una incidencia real para descubrir que tu fallback no funciona. Pruébalo ahora:
// In your integration tests, use a mock handler that returns 503
public class AlwaysFailHandler : DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken) =>
Task.FromResult(new HttpResponseMessage(HttpStatusCode.ServiceUnavailable));
}
Conéctalo a un HttpClient de test, ejecuta tu servicio y verifica que SummarizeAsync devuelve null en vez de lanzar una excepción. Si lanza una excepción, tu fallback está roto y te enterarás en producción.
Checklist
Antes de que cualquier funcionalidad de IA salga a producción, responde a esto:
- ¿Hay un timeout en cada llamada a la IA? ¿Es menor que el timeout general de la petición?
- ¿Reintentas en 429 y 503 con backoff? ¿Evitas reintentar con respuestas malas?
- ¿Hay un circuit breaker? ¿Sabes en qué estado está ahora mismo?
- ¿Qué ve el usuario cuando la IA no está disponible? ¿Lo has probado?
- ¿Una caída total de la IA afecta solo a la funcionalidad de IA, o a toda la aplicación?
- ¿Has probado el camino de fallback en un test de integración real?
Si alguna respuesta es “no lo sé” — esa es la que hay que arreglar primero.
Antes del Próximo Artículo
Has construido un sistema que sobrevive a fallos de IA. Pero hay una pregunta más difícil: ¿cómo sabes cuándo está a punto de fallar?
El circuit breaker se abre después de 5 fallos. Pero ¿qué pasa con los 4 fallos anteriores? ¿Y qué pasa con la degradación lenta — peticiones que tienen éxito pero tardan 3 veces más de lo normal, o respuestas que son técnicamente válidas pero que van empeorando silenciosamente?
Eso es observabilidad. Y es lo siguiente.
Si esta serie te ayuda, considera invitarme a un café.
Este es el artículo 2 de la serie AI in Production. Siguiente: Observabilidad — cómo medir lo que no puedes ver.
Loading comments...