AI in Production -- Parte 3

Observabilidad para Sistemas de IA: Midiendo Lo Que No Puedes Ver

#ai #architecture #observability #opentelemetry #dotnet

El Problema

Tu dashboard de monitorización está en verde. Todos los endpoints devuelven 200. Los tiempos de respuesta parecen normales. No hay alertas saltando.

Pero los usuarios se quejan. “La IA da respuestas raras.” “Antes iba más rápido.” “No creo que funcione bien.”

Miras los logs. HTTP 200. Miras las métricas. Nada raro. Miras los traces. La llamada a la IA se completó con éxito.

Y sin embargo, algo va mal.

Este es el problema de observabilidad en sistemas de IA. La monitorización tradicional te dice si la red funcionó. No te dice si la IA funcionó. Una respuesta puede ser técnicamente correcta — código HTTP correcto, JSON válido, dentro del timeout — y aun así ser incorrecta, inútil, o estar empeorando poco a poco con el tiempo.

A developer sits surrounded by green dashboards and "All systems operational" banners, looking satisfied — while behind them an AI brain silently produces gibberish answers nobody is reading.

Hay tres tipos de degradación que la monitorización estándar no detecta:

Caída silenciosa de calidad. El modelo devuelve respuestas menos relevantes, menos precisas, o con una estructura diferente a la de antes. Quizás el proveedor actualizó el modelo. Quizás tus prompts están llegando a casos límite con más frecuencia a medida que crece tu base de usuarios. No salta ningún error. No se dispara ninguna alarma. Te enteras cuando los usuarios dejan de usar la funcionalidad.

Aumento gradual de costes. El uso de tokens va subiendo. Quizás un nuevo flujo de código genera prompts más largos. Quizás los usuarios descubrieron que pueden hacer preguntas complejas. La llamada a la API sigue funcionando, pero ahora estás gastando 3x lo que presupuestaste. Finanzas se entera a final de mes.

Drift de latencia. El p50 de latencia está bien. Pero el p95 empeora cada semana. La mayoría de usuarios no lo notan. Los power users — los que más usan tu producto — sí. Pierdes a tus mejores usuarios primero.

Nada de esto aparece en la monitorización estándar. Necesitas instrumentar tus llamadas a la IA de forma específica.

Qué Medir

Antes de escribir código, ten claro qué necesitas saber realmente.

Latencia — de la forma correcta. Las medias esconden problemas. Un p95 de 8 segundos significa que 1 de cada 20 peticiones tarda 8 segundos o más. Eso no es aceptable para una funcionalidad interactiva, y las medias no te lo van a mostrar. Monitoriza p50, p95 y p99 por separado.

Uso de tokens. Los tokens son tu unidad de coste. Monitoriza los prompt tokens (lo que envías) y los completion tokens (lo que recibes) por separado, por endpoint. Los prompt tokens están mayormente bajo tu control — si suben de golpe, algo cambió en tu código. Los completion tokens reflejan la verbosidad del modelo — si suben, el modelo cambió o tus prompts cambiaron.

Tasa de fallback. ¿Con qué frecuencia está abierto el circuit breaker? ¿Con qué frecuencia la IA devuelve null y se activa el fallback? Si esto está por encima del 1-2%, algo va mal — o la IA no es fiable o tu timeout es demasiado agresivo.

Señales de calidad. Esta es la difícil. No puedes saber automáticamente si una respuesta es correcta. Pero puedes medir indicadores indirectos: longitud de la respuesta (demasiado corta puede significar rechazo), conformidad de estructura (¿el modelo devolvió el formato JSON que pediste?), y feedback explícito del usuario (pulgar arriba/abajo, ediciones, reintentos).

Modelo y versión. Registra qué modelo respondió a cada petición. Cuando un proveedor actualice en silencio, sabrás exactamente cuándo cambió el comportamiento.

Ejecución

Vamos a usar OpenTelemetry para métricas y tracing, con logging estructurado para los detalles que no encajan en métricas. Esto funciona con cualquier backend de observabilidad — Azure Monitor, Grafana, Datadog, lo que tengas.

Añadir los paquetes

dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol
dotnet add package System.Diagnostics.DiagnosticSource

Definir tus métricas de IA

Crea una clase dedicada para métricas. Esto mantiene todos los instrumentos relacionados con IA en un solo lugar y facilita encontrarlos en tus dashboards.

using System.Diagnostics;
using System.Diagnostics.Metrics;

public static class AiMetrics
{
    private static readonly Meter Meter = new("victorz.ai", "1.0.0");

    // Histogram: tracks distribution of values (p50, p95, p99)
    public static readonly Histogram<double> RequestDuration =
        Meter.CreateHistogram<double>(
            "ai.request.duration",
            unit: "ms",
            description: "Duration of AI API calls");

    public static readonly Histogram<int> PromptTokens =
        Meter.CreateHistogram<int>(
            "ai.tokens.prompt",
            unit: "tokens",
            description: "Prompt tokens sent per request");

    public static readonly Histogram<int> CompletionTokens =
        Meter.CreateHistogram<int>(
            "ai.tokens.completion",
            unit: "tokens",
            description: "Completion tokens received per request");

    // Counter: monotonically increasing total
    public static readonly Counter<int> FallbackActivations =
        Meter.CreateCounter<int>(
            "ai.fallback.activations",
            description: "Number of times the AI fallback was used");

    public static readonly Counter<int> Requests =
        Meter.CreateCounter<int>(
            "ai.requests.total",
            description: "Total AI requests by outcome");
}

Instrumentar el servicio

Actualiza AiSummaryService del artículo 2 para registrar métricas en cada llamada:

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)
    {
        var stopwatch = Stopwatch.StartNew();
        string? outcome = "success";

        try
        {
            var result = await _pipeline.ExecuteAsync(async ct =>
            {
                var response = await _http.PostAsJsonAsync(
                    "/summarize",
                    new { text },
                    ct);

                response.EnsureSuccessStatusCode();

                var body = await response.Content
                    .ReadFromJsonAsync<SummaryResponse>(ct);

                // Record token usage from the API response headers or body
                // Most providers return this in the response
                if (body?.Usage is not null)
                {
                    AiMetrics.PromptTokens.Record(
                        body.Usage.PromptTokens,
                        new TagList { { "endpoint", "summarize" } });

                    AiMetrics.CompletionTokens.Record(
                        body.Usage.CompletionTokens,
                        new TagList { { "endpoint", "summarize" } });
                }

                return body?.Summary;
            }, cancellationToken);

            if (result is null)
            {
                outcome = "empty_response";
            }

            return result;
        }
        catch (BrokenCircuitException)
        {
            outcome = "circuit_open";
            AiMetrics.FallbackActivations.Add(1,
                new TagList { { "reason", "circuit_open" } });

            _logger.LogWarning("AI circuit open. Returning null.");
            return null;
        }
        catch (Exception ex)
        {
            outcome = "error";
            AiMetrics.FallbackActivations.Add(1,
                new TagList { { "reason", "error" } });

            _logger.LogError(ex, "AI summarization failed.");
            return null;
        }
        finally
        {
            stopwatch.Stop();

            AiMetrics.RequestDuration.Record(
                stopwatch.Elapsed.TotalMilliseconds,
                new TagList { { "endpoint", "summarize" }, { "outcome", outcome } });

            AiMetrics.Requests.Add(1,
                new TagList { { "endpoint", "summarize" }, { "outcome", outcome } });

            // Structured log for every call — cheap to store, invaluable to query
            _logger.LogInformation(
                "AI call completed. Endpoint={Endpoint} Outcome={Outcome} DurationMs={DurationMs}",
                "summarize", outcome, stopwatch.Elapsed.TotalMilliseconds);
        }
    }
}

record SummaryResponse(string? Summary, TokenUsage? Usage);
record TokenUsage(int PromptTokens, int CompletionTokens);

Configurar OpenTelemetry

// Program.cs
builder.Services.AddOpenTelemetry()
    .WithMetrics(metrics =>
    {
        metrics
            .AddMeter("victorz.ai")
            .AddRuntimeInstrumentation()
            .AddAspNetCoreInstrumentation()
            .AddOtlpExporter(options =>
            {
                options.Endpoint = new Uri(
                    builder.Configuration["Otlp:Endpoint"]
                    ?? "http://localhost:4317");
            });
    })
    .WithTracing(tracing =>
    {
        tracing
            .AddAspNetCoreInstrumentation()
            .AddHttpClientInstrumentation()
            .AddOtlpExporter(options =>
            {
                options.Endpoint = new Uri(
                    builder.Configuration["Otlp:Endpoint"]
                    ?? "http://localhost:4317");
            });
    });

Los cuatro dashboards que necesitas

Una vez que los datos estén fluyendo, construye estas cuatro vistas. Todo lo demás es opcional.

1. Distribución de latencia — Histograma de ai.request.duration dividido por p50/p95/p99. Configura una alerta si el p95 supera tu SLA. Nunca alertes sobre la media.

2. Uso de tokens a lo largo del tiempoai.tokens.prompt y ai.tokens.completion como series temporales. Dibuja una línea base en la primera semana. Alerta si hay desviaciones por encima del 20%.

3. Tasa de fallbackai.fallback.activations / ai.requests.total como porcentaje a lo largo del tiempo. Alerta si supera el 2%. Por encima del 5% significa que algo va muy mal.

4. Desglose de resultadosai.requests.total dividido por la etiqueta de outcome: success, empty_response, circuit_open, error. Esto te dice qué tipo de fallos estás teniendo, no solo que hay fallos.

A control room with four screens: p95 Latency, Token Usage, Fallback Rate, Outcome Breakdown. A calm engineer points at one with a coffee mug. Servers on fire visible through the window outside, but the room is orderly.

Detectar problemas de calidad

Las métricas detectan problemas de rendimiento y disponibilidad. Los problemas de calidad son más difíciles. El enfoque más simple que realmente funciona en producción:

public class AiSummaryService : IAiSummaryService
{
    // ... existing code ...

    private static bool IsResponseSuspicious(string? response)
    {
        if (string.IsNullOrWhiteSpace(response)) return true;

        // Too short to be a real summary
        if (response.Length < 50) return true;

        // Model refusal patterns
        if (response.StartsWith("I'm sorry", StringComparison.OrdinalIgnoreCase)) return true;
        if (response.StartsWith("I cannot", StringComparison.OrdinalIgnoreCase)) return true;

        return false;
    }
}

Registra las respuestas sospechosas con la entrada y salida completas. Revísalas semanalmente. Esto no es control de calidad automatizado — es una señal que te dice dónde mirar.

A factory conveyor belt where a quality inspector robot flags boxes labeled "I'm sorry, I cannot help with that" and "[empty]" into a SUSPICIOUS bin, while boxes labeled "Good Answer" pass through normally.

Checklist

  • ¿Estás monitorizando latencia p95 y p99, no solo la media?
  • ¿Estás registrando prompt tokens y completion tokens por petición?
  • ¿Tienes una métrica de tasa de fallback con un umbral de alerta?
  • ¿Puedes decir, ahora mismo, qué porcentaje de llamadas a la IA están teniendo éxito?
  • ¿Puedes saber cuándo cambió el comportamiento, y qué versión del modelo lo causó?
  • ¿Tienes una forma de detectar respuestas sospechosas o de baja calidad?

Si no puedes responder a estas preguntas mirando un dashboard, estás volando a ciegas.

Antes del Próximo Artículo

Ahora puedes ver lo que está pasando. Latencia, tasa de fallback, señales de calidad — todo visible.

Pero mira esa gráfica de uso de tokens. Obsérvala durante una semana. Casi seguro que sube. Quizás despacio. Quizás con saltos repentinos.

Eso es tu coste. Y el coste en sistemas de IA tiene la costumbre de pillar a la gente por sorpresa. El artículo 4 trata sobre hacerlo predecible.


Si esta serie te resulta útil, considera invitarme a un café.

Este es el artículo 3 de la serie AI in Production. Siguiente: El Problema del Coste — tokens, caching, y cómo evitar que tu factura de IA se convierta en una conversación con finanzas.

Comments

Loading comments...