Todos los artículos

// Agentes en producción

Innova IA en Producción: Monitorabilidad y Observabilidad de Sistemas Multi-Agente

OpenTelemetry en GCP para sistemas multi-agente IA: tracing distribuido, métricas production-ready y patrones de observabilidad para SREs.

9 de julio de 202617 min de lectura

Problem

El día que desplegamos el sistema multi-agente en GCP, todo parecía perfecto. Los pods corrían en GKE, los servicios se comunicaban vía gRPC, y el orquestador coordinaba 12 agentes diferentes. Pero tres días después, tuvimos un incidente que reveló una falla crítica: no tenemos visibilidad sobre qué está pasando realmente dentro de los agentes.

⚠️La trampa del status code 200

Un sistema de agentes puede retornar HTTP 200 OK en cada request y aun así estar produciendo resultados incorrectos, inconsistentes, o costosos. El problema no está en el código que ejecuta; está en las decisiones que el modelo toma silenciosamente.

El escenario es familiar: tu monitor de infraestructura muestra que todo está verde. CPU, memoria, latency, error rate — todo dentro de SLAs. Pero los usuarios reportan que el sistema está "comportándose raro". A veces devuelve información desactualizada. Otras veces, ejecuta acciones que no deberían ser necesarias. En un caso extremo, un agente de análisis entró en un loop infinito de auto-corrección, consumiendo $400 de tokens en 30 minutos, sin que ninguna alerta se disparara.

La observabilidad clásica está diseñada para sistemas deterministas: dado un input, siempre esperas el mismo output. Pero los sistemas multi-agente son intrínsecamente no-deterministas. El mismo prompt ejecutado dos veces puede producir decisiones completamente diferentes. Un "bug" no es una excepción en el código; es un patrón de razonamiento que el modelo adoptó sin que nadie lo instrumentara.

Para SREs y Platform Engineers, esto rompe todas las reglas del libro: dashboards que no significan lo que creen, alertas que no capturan problemas reales, y una incapacidad fundamental para debuggear incidentes cuando ocurren.

Core Concept

La monitorabilidad de sistemas multi-agente requiere un cambio de modelo mental: de observar eventos a observar trazas de razonamiento.

Tres Pilares de Observabilidad para IA

  1. Tracing Distribuido (OpenTelemetry Traces)

    • Cada ejecución de un agente es un trace
    • Cada decisión importante es un span
    • Cada invocación de tool es un span anidado
    • Propagación automática entre servicios
  2. Métricas con Contexto Semántico

    • No solo latencia, sino latencia por fase del pipeline
    • No solo error rate, sino tasa de outputs inválidos
    • Token consumption por agente y por tipo de tarea
    • Intentos de corrección por output final
  3. Logs Estructurados con Decisiones

    • Qué decidió el modelo, no solo qué devolvió
    • Por qué invocó una herramienta específica
    • Qué criterios de evaluación se aplicaron
    • Qué restricciones de safety se activaron

OpenTelemetry en el Ecosistema de IA

OpenTelemetry ya tiene convenciones semánticas específicas para GenAI desde 2024. Estas estandarizan los nombres de atributos para que cualquier backend de observabilidad pueda procesarlos correctamente:

terminal
gen_ai.system: "openai" | "anthropic" | "google"
gen_ai.request.model: "gpt-4-turbo" | "claude-opus-4-6"
gen_ai.request.max_tokens: 4096
gen_ai.usage.input_tokens: 1240
gen_ai.usage.output_tokens: 856
gen_ai.response.finish_reason: "stop" | "length" | "tool_use"
gen_ai.tool.name: "search_database"
gen_ai.tool.call_count: 1

GCP Cloud Operations Suite (anteriormente Stackdriver) tiene soporte nativo para OpenTelemetry, lo que permite centralizar todas las trazas, métricas y logs de tu sistema multi-agente en un solo lugar.

💡Context Propagation en GCP

OpenTelemetry en GKE se propaga automáticamente entre pods vía Istio o Anthos Service Mesh. Solo necesitas asegurarte de que tus instrumentaciones de agentes extraigan y propaguen el context ID correctamente.

Implementation

Vamos a construir un sistema de observabilidad production-ready para un sistema multi-agente en GCP, usando Go y OpenTelemetry.

Arquitectura del Sistema de Observabilidad

terminal
┌─────────────────────────────────────────────────────────┐
│                    GKE Cluster                          │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐              │
│  │Orchestr- │  │  Worker  │  │ Evaluator│              │
│  │  ator    │──│   Pool   │──│   Agent  │              │
│  └────┬─────┘  └────┬─────┘  └──────────┘              │
│       │             │                                    │
│       └─────────────┼─────────────────────────────┐     │
│                     │ OpenTelemetry Exporter      │     │
│                     │ (OTLP gRPC)                 │     │
│                     └──────────────┬──────────────┘     │
└───────────────────────────────────┼────────────────────┘
                                    │
                                    │ gRPC (Secure)
                                    │
┌───────────────────────────────────┼────────────────────┐
│        Google Cloud Operations     │                   │
│  ┌──────────────┐  ┌──────────────┐                    │
│  │Cloud Trace   │  │Cloud Metrics │                    │
│  │(Tracing)     │  │(Monitoring)  │                    │
│  └──────────────┘  └──────────────┘                    │
│  ┌──────────────┐  ┌──────────────┐                    │
│  │Cloud Logging │  │Error Reporting│                   │
│  └──────────────┘  └──────────────┘                    │
└─────────────────────────────────────────────────────────┘

1. Configuración de OpenTelemetry en Go

Comenzamos configurando el SDK de OpenTelemetry para exportar a GCP:

go
// pkg/telemetry/opentelemetry.go
package telemetry

import (
	"context"
	"fmt"
	"os"

	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
	"go.opentelemetry.io/otel/propagation"
	sdktrace "go.opentelemetry.io/otel/sdk/trace"
	"go.opentelemetry.io/otel/trace"
)

type TelemetryConfig struct {
	ServiceName    string
	ServiceVersion string
	Environment    string
}

func NewOpenTelemetry(ctx context.Context, cfg TelemetryConfig) (trace.Tracer, error) {
	// Configurar exporter para GCP Cloud Trace via OTLP
	projectID := os.Getenv("GOOGLE_CLOUD_PROJECT")
	exporter, err := otlptracegrpc.New(ctx,
		otlptracegrpc.WithEndpoint(fmt.Sprintf("otlp-gcp.cloudtrace.googleapis.com:443")),
		otlptracegrpc.WithHeaders(map[string]string{
			"x-google-project-id": projectID,
		}),
		otlptracegrpc.WithInsecure(false),
	)
	if err != nil {
		return nil, fmt.Errorf("failed to create OTLP exporter: %w", err)
	}

	// Configurar tracer provider con GCP resource attributes
	tp := sdktrace.NewTracerProvider(
		sdktrace.WithBatcher(exporter),
		sdktrace.WithResource(
			resource.NewWithAttributes(
				semconv.SchemaURL,
				semconv.ServiceNameKey.String(cfg.ServiceName),
				semconv.ServiceVersionKey.String(cfg.ServiceVersion),
				semconv.DeploymentEnvironmentKey.String(cfg.Environment),
				semconv.CloudProviderKey.String("gcp"),
				semconv.CloudPlatformKey.String("gcp_kubernetes_engine"),
			),
		),
	)

	// Configurar propagación automática de context
	otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
		propagation.TraceContext{},
		propagation.Baggage{},
	))

	// Set global tracer provider
	otel.SetTracerProvider(tp)

	return tp.Tracer(cfg.ServiceName), nil
}

2. Instrumentación de Agentes con OpenTelemetry

Creamos una capa de instrumentación para agentes que captura todas las decisiones importantes:

go
// pkg/agent/instrumented.go
package agent

import (
	"context"
	"time"

	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/codes"
	"go.opentelemetry.io/otel/trace"
)

type AgentInstrumentation struct {
	tracer trace.Tracer
}

type AgentContext struct {
	AgentID    string
	AgentType  string
	Goal       string
	InputTokens int
}

type ToolInvocation struct {
	ToolName string
	Args     map[string]interface{}
	Duration time.Duration
	Success  bool
	Error    error
}

func NewAgentInstrumentation(tracer trace.Tracer) *AgentInstrumentation {
	return &AgentInstrumentation{tracer: tracer}
}

// TraceAgentExecution crea el trace principal de ejecución de un agente
func (ai *AgentInstrumentation) TraceAgentExecution(
	ctx context.Context,
	agentCtx AgentContext,
	fn func(context.Context) ([]byte, error),
) ([]byte, error) {
	spanName := fmt.Sprintf("agent.%s.execute", agentCtx.AgentType)
	ctx, span := ai.tracer.Start(ctx, spanName,
		trace.WithAttributes(
			attribute.String("agent.id", agentCtx.AgentID),
			attribute.String("agent.type", agentCtx.AgentType),
			attribute.String("agent.goal", agentCtx.Goal),
			attribute.Int("agent.input_tokens", agentCtx.InputTokens),
		),
	)
	defer span.End()

	startTime := time.Now()

	// Ejecutar la lógica del agente
	result, err := fn(ctx)

	duration := time.Since(startTime)

	// Registrar métricas de ejecución
	span.SetAttributes(
		attribute.Int("agent.output_length", len(result)),
		attribute.Int64("agent.duration_ms", duration.Milliseconds()),
	)

	if err != nil {
		span.SetStatus(codes.Error, err.Error())
		span.SetAttributes(
			attribute.String("agent.error_type", classifyError(err)),
		)
		return nil, err
	}

	span.SetStatus(codes.Ok, "agent executed successfully")
	return result, nil
}

// TraceLLMCall instrumenta cada llamada al modelo LLM
func (ai *AgentInstrumentation) TraceLLMCall(
	ctx context.Context,
	model string,
	systemPrompt string,
	userPrompt string,
	fn func(context.Context) (*LLMResponse, error),
) (*LLMResponse, error) {
	ctx, span := ai.tracer.Start(ctx, "llm.inference",
		trace.WithAttributes(
			attribute.String("gen_ai.system", extractProvider(model)),
			attribute.String("gen_ai.request.model", model),
			attribute.Int("gen_ai.prompt.length", len(systemPrompt)+len(userPrompt)),
			// NO loggear el prompt completo en producción si contiene PII
		),
	)
	defer span.End()

	startTime := time.Now()
	response, err := fn(ctx)
	duration := time.Since(startTime)

	if err != nil {
		span.SetStatus(codes.Error, err.Error())
		return nil, err
	}

	// Registrar atributos de uso de tokens
	span.SetAttributes(
		attribute.Int("gen_ai.usage.input_tokens", response.InputTokens),
		attribute.Int("gen_ai.usage.output_tokens", response.OutputTokens),
		attribute.Int("gen_ai.usage.total_tokens", response.InputTokens+response.OutputTokens),
		attribute.String("gen_ai.response.finish_reason", response.FinishReason),
		attribute.Int64("llm.duration_ms", duration.Milliseconds()),
		attribute.Float64("llm.tokens_per_second", 
			float64(response.InputTokens+response.OutputTokens)/duration.Seconds(),
		),
	)

	// Emitir evento para tracking de decisiones
	if response.FinishReason == "tool_use" {
		span.AddEvent("llm.tool_use_requested",
			trace.WithAttributes(
				attribute.String("tool.name", response.ToolName),
			),
		)
	}

	return response, nil
}

// TraceToolCall instrumenta cada invocación de herramienta
func (ai *AgentInstrumentation) TraceToolCall(
	ctx context.Context,
	tool ToolInvocation,
	fn func(context.Context) (interface{}, error),
) (interface{}, error) {
	spanName := fmt.Sprintf("tool.%s.execute", tool.ToolName)
	ctx, span := ai.tracer.Start(ctx, spanName,
		trace.WithAttributes(
			attribute.String("tool.name", tool.ToolName),
			attribute.String("tool.args", formatArgs(tool.Args)),
		),
	)
	defer span.End()

	startTime := time.Now()
	result, err := fn(ctx)
	duration := time.Since(startTime)

	span.SetAttributes(
		attribute.Int64("tool.duration_ms", duration.Milliseconds()),
		attribute.Bool("tool.success", tool.Success),
	)

	if err != nil {
		span.SetStatus(codes.Error, err.Error())
		span.AddEvent("tool.execution_failed",
			trace.WithAttributes(
				attribute.String("tool.error", err.Error()),
			),
		)
		return nil, err
	}

	span.AddEvent("tool.execution_succeeded",
		trace.WithAttributes(
			attribute.Int("tool.result_size", estimateSize(result)),
		),
	)

	return result, nil
}

3. Instrumentación de Orquestador Multi-Agente

El orquestador es donde creamos el trace raíz que conecta todos los agentes:

go
// pkg/orchestrator/instrumented.go
package orchestrator

import (
	"context"
	"fmt"

	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/codes"
	"go.opentelemetry.io/otel/trace"
)

type OrchestratorInstrumentation struct {
	tracer trace.Tracer
	agentInstrumentation *agent.AgentInstrumentation
}

type Task struct {
	ID          string
	Type        string
	Description string
	Priority    int
}

type OrchestrationResult struct {
	Success       bool
	FinalOutput   []byte
	AgentTraceIDs []string
	TokenUsage    int
	ExecutionTime time.Duration
	ValidationPasses bool
}

func NewOrchestratorInstrumentation(
	tracer trace.Tracer,
	agentInstr *agent.AgentInstrumentation,
) *OrchestratorInstrumentation {
	return &OrchestratorInstrumentation{
		tracer: tracer,
		agentInstrumentation: agentInstr,
	}
}

// TraceOrchestration crea el trace raíz de toda la ejecución
func (oi *OrchestratorInstrumentation) TraceOrchestration(
	ctx context.Context,
	task Task,
	fn func(context.Context) (*OrchestrationResult, error),
) (*OrchestrationResult, error) {
	spanName := fmt.Sprintf("orchestration.%s", task.Type)
	ctx, rootSpan := oi.tracer.Start(ctx, spanName,
		trace.WithAttributes(
			attribute.String("orchestration.task_id", task.ID),
			attribute.String("orchestration.task_type", task.Type),
			attribute.String("orchestration.description", task.Description),
			attribute.Int("orchestration.priority", task.Priority),
		),
	)
	defer rootSpan.End()

	startTime := time.Now()

	// Ejecutar la orquestación
	result, err := fn(ctx)

	duration := time.Since(startTime)

	// Métricas de alto nivel
	rootSpan.SetAttributes(
		attribute.Bool("orchestration.success", result.Success),
		attribute.Int("orchestration.total_token_usage", result.TokenUsage),
		attribute.Int64("orchestration.duration_ms", duration.Milliseconds()),
		attribute.Bool("orchestration.validation_passed", result.ValidationPasses),
		attribute.Int("orchestration.agents_invoked", len(result.AgentTraceIDs)),
	)

	// Validación de calidad del resultado
	if err != nil {
		rootSpan.SetStatus(codes.Error, err.Error())
		return nil, err
	}

	if !result.ValidationPasses {
		rootSpan.SetStatus(codes.Error, "validation failed")
		rootSpan.AddEvent("orchestration.validation_failed",
			trace.WithAttributes(
				attribute.String("validation.reason", "output did not meet quality criteria"),
			),
		)
		return result, fmt.Errorf("validation failed")
	}

	rootSpan.SetStatus(codes.Ok, "orchestration completed successfully")
	return result, nil
}

4. Métricas Personalizadas para SREs

Creamos métricas que realmente importan para operar sistemas multi-agente:

go
// pkg/telemetry/metrics.go
package telemetry

import (
	"context"
	"time"

	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/metric"
)

type AgentMetrics struct {
	meter metric.Meter

	// Métricas de ejecución
	taskCounter           metric.Int64Counter
	taskLatency           metric.Float64Histogram
	tokenUsage            metric.Int64Histogram
	
	// Métricas de calidad
	validationPassRate    metric.Float64Gauge
	outputQualityScore    metric.Float64Histogram
	correctionLoopCount   metric.Int64Histogram
	
	// Métricas de costos
	costPerTask           metric.Float64Histogram
	totalCost             metric.Float64Counter
}

func NewAgentMetrics(meter metric.Meter) (*AgentMetrics, error) {
	m := &AgentMetrics{meter: meter}

	var err error

	// Counter de tareas ejecutadas
	m.taskCounter, err = meter.Int64Counter(
		"agent.tasks.total",
		metric.WithDescription("Total number of agent tasks executed"),
	)
	if err != nil {
		return nil, err
	}

	// Histogram de latencia de tareas
	m.taskLatency, err = meter.Float64Histogram(
		"agent.tasks.latency_ms",
		metric.WithDescription("Task execution latency in milliseconds"),
		metric.WithExplicitBucketBoundaries(10, 50, 100, 250, 500, 1000, 2000, 5000, 10000),
	)
	if err != nil {
		return nil, err
	}

	// Histogram de uso de tokens
	m.tokenUsage, err = meter.Int64Histogram(
		"agent.tokens.usage",
		metric.WithDescription("Token consumption per task"),
		metric.WithExplicitBucketBoundaries(100, 500, 1000, 2000, 4000, 8000, 16000, 32000),
	)
	if err != nil {
		return nil, err
	}

	// Gauge de tasa de validación exitosa
	m.validationPassRate, err = meter.Float64Gauge(
		"agent.validation.pass_rate",
		metric.WithDescription("Rate of tasks passing validation"),
	)
	if err != nil {
		return nil, err
	}

	// Histogram de score de calidad de output
	m.outputQualityScore, err = meter.Float64Histogram(
		"agent.output.quality_score",
		metric.WithDescription("Output quality score (0-100)"),
		metric.WithExplicitBucketBoundaries(0, 20, 40, 60, 80, 90, 95, 99, 100),
	)
	if err != nil {
		return nil, err
	}

	// Histogram de loops de corrección
	m.correctionLoopCount, err = meter.Int64Histogram(
		"agent.correction.loops",
		metric.WithDescription("Number of correction loops per task"),
		metric.WithExplicitBucketBoundaries(0, 1, 2, 3, 4, 5, 10),
	)
	if err != nil {
		return nil, err
	}

	// Histogram de costo por tarea
	m.costPerTask, err = meter.Float64Histogram(
		"agent.cost.per_task",
		metric.WithDescription("Cost in USD per task"),
		metric.WithExplicitBucketBoundaries(0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1.0, 5.0),
	)
	if err != nil {
		return nil, err
	}

	// Counter de costo total
	m.totalCost, err = meter.Float64Counter(
		"agent.cost.total_usd",
		metric.WithDescription("Total cost in USD"),
	)
	if err != nil {
		return nil, err
	}

	return m, nil
}

func (m *AgentMetrics) RecordTask(
	ctx context.Context,
	taskType string,
	latency time.Duration,
	tokenUsage int,
	validationPassed bool,
	qualityScore float64,
	correctionLoops int,
	costUSD float64,
) {
	commonAttrs := []attribute.KeyValue{
		attribute.String("task.type", taskType),
	}

	// Registrar counter de tareas
	m.taskCounter.Add(ctx, 1, metric.WithAttributes(commonAttrs...))

	// Registrar latencia
	m.taskLatency.Record(ctx, float64(latency.Milliseconds()), 
		metric.WithAttributes(commonAttrs...))

	// Registrar uso de tokens
	m.tokenUsage.Record(ctx, int64(tokenUsage), metric.WithAttributes(commonAttrs...))

	// Registrar quality score
	if qualityScore > 0 {
		m.outputQualityScore.Record(ctx, qualityScore, metric.WithAttributes(commonAttrs...))
	}

	// Registrar loops de corrección
	if correctionLoops > 0 {
		m.correctionLoopCount.Record(ctx, int64(correctionLoops), 
			metric.WithAttributes(commonAttrs...))
	}

	// Registrar costo
	m.costPerTask.Record(ctx, costUSD, metric.WithAttributes(commonAttrs...))
	m.totalCost.Add(ctx, costUSD, metric.WithAttributes(commonAttrs...))
}

5. Integración con Kubernetes y GCP

Creamos un ejemplo completo de despliegue en GKE:

yaml
# k8s/orchestrator-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: agent-orchestrator
  labels:
    app: agent-orchestrator
spec:
  replicas: 3
  selector:
    matchLabels:
      app: agent-orchestrator
  template:
    metadata:
      labels:
        app: agent-orchestrator
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/port: "9090"
    spec:
      containers:
      - name: orchestrator
        image: gcr.io/my-project/agent-orchestrator:v1.0.0
        ports:
        - containerPort: 8080
          name: http
        - containerPort: 9090
          name: metrics
        env:
        - name: GOOGLE_CLOUD_PROJECT
          valueFrom:
            configMapKeyRef:
              name: app-config
              key: project-id
        - name: OTEL_EXPORTER_OTLP_ENDPOINT
          value: "otlp-gcp.cloudtrace.googleapis.com:443"
        - name: OTEL_SERVICE_NAME
          value: "agent-orchestrator"
        - name: OTEL_RESOURCE_ATTRIBUTES
          value: "deployment.environment=production,k8s.pod.name=$(POD_NAME)"
        - name: POD_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        resources:
          requests:
            cpu: "500m"
            memory: "512Mi"
          limits:
            cpu: "2000m"
            memory: "2Gi"
        livenessProbe:
          httpGet:
            path: /healthz
            port: 8080
          initialDelaySeconds: 10
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /readyz
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 5

6. Alertas SRE-Relevantes en GCP

Configuramos alertas que realmente importan para sistemas multi-agente:

yaml
# alerts/agent-system-alerts.yaml
apiVersion: monitoring.googleapis.com/v1
kind: AlertPolicy
metadata:
  name: agent-system-alerts
  displayName: "Alertas de Sistema Multi-Agente"
  description: "Alertas para monitoreo de sistemas multi-agente en producción"
spec:
  conditions:
  # Alerta: Tasa de validación baja
  - displayName: "Tasa de validación < 80%"
    conditionThreshold:
      filter: >
        resource.type="k8s_container" AND
        metric.type="custom.googleapis.com/agent/validation/pass_rate" AND
        metric.label.task_type="*"
      comparison: COMPARISON_LT
      thresholdValue: 0.8
      duration: 3600s
      aggregations:
      - alignmentPeriod: 300s
        perSeriesAligner: ALIGN_MEAN
        crossSeriesReducer: REDUCE_MEAN
        groupByFields:
        - metric.label.task_type
  
  # Alerta: Latencia p99 > 10s
  - displayName: "Latencia p99 > 10s"
    conditionThreshold:
      filter: >
        resource.type="k8s_container" AND
        metric.type="custom.googleapis.com/agent/tasks/latency_ms" AND
        resource.label.container_name="orchestrator"
      comparison: COMPARISON_GT
      thresholdValue: 10000
      duration: 900s
      aggregations:
      - alignmentPeriod: 300s
        perSeriesAligner: ALIGN_PERCENTILE_99
        crossSeriesReducer: REDUCE_MEAN
        groupByFields:
        - metric.label.task_type
  
  # Alerta: Loops de corrección > 3
  - displayName: "Loops de corrección > 3"
    conditionThreshold:
      filter: >
        resource.type="k8s_container" AND
        metric.type="custom.googleapis.com/agent/correction/loops"
      comparison: COMPARISON_GT
      thresholdValue: 3
      duration: 600s
      aggregations:
      - alignmentPeriod: 300s
        perSeriesAligner: ALIGN_PERCENTILE_95
        crossSeriesReducer: REDUCE_MEAN
  
  # Alerta: Costo por tarea > $1
  - displayName: "Costo por tarea > $1"
    conditionThreshold:
      filter: >
        resource.type="k8s_container" AND
        metric.type="custom.googleapis.com/agent/cost/per_task"
      comparison: COMPARISON_GT
      thresholdValue: 1.0
      duration: 1800s
      aggregations:
      - alignmentPeriod: 600s
        perSeriesAligner: ALIGN_PERCENTILE_95
        crossSeriesReducer: REDUCE_MEAN
        groupByFields:
        - metric.label.task_type
  
  # Alerta: Tasa de errores de herramientas > 5%
  - displayName: "Tasa de errores de herramientas > 5%"
    conditionThreshold:
      filter: >
        resource.type="k8s_container" AND
        metric.type="custom.googleapis.com/tool/error_rate"
      comparison: COMPARISON_GT
      thresholdValue: 0.05
      duration: 900s
      aggregations:
      - alignmentPeriod: 300s
        perSeriesAligner: ALIGN_MEAN
        crossSeriesReducer: REDUCE_MEAN
        groupByFields:
        - metric.label.tool_name
  
  documentation:
    content: |
      Estas alertas están diseñadas para detectar problemas en sistemas multi-agente
      que no serían visibles con métricas tradicionales.
      
      Acciones recomendadas:
      1. Revisar traces en Cloud Trace para identificar el agente específico
      2. Analizar logs estructurados para entender el patrón de razonamiento
      3. Evaluar si se requiere ajuste de prompts o restricciones de safety
      4. Considerar rollback de cambios recientes en configuración de agentes
  
  notificationChannels:
  - name: projects/my-project/notificationChannels/123456789
    displayName: "#sre-alerts"
Regla de Oro de Instrumentación

Instrumenta primero los puntos de decisión, no los puntos de ejecución. Una llamada HTTP tiene instrumentación automática. Lo que necesitas observar es qué decidió el modelo, por qué invocó esa herramienta específica, y qué criterios de validación se aplicaron.

1

1. Implementar Tracing Distribuido

Comienza instrumentando el orquestador para crear traces raíz, luego instrumenta cada agente para crear spans anidados, y finalmente instrumenta cada llamada LLM y tool invocation. OpenTelemetry se encarga de propagar el context automáticamente.

2

2. Definir Métricas SRE-Relevantes

No te conformes con métricas de infraestructura. Define métricas que capturen la calidad y comportamiento de los agentes: tasa de validación, calidad de output, loops de corrección, costo por tarea.

3

3. Configurar Alertas Accionables

Cada alerta debe tener una acción clara asociada. Si la tasa de validación cae, ¿qué revisas primero? Si el costo por tarea aumenta, ¿qué componente investigas? Documenta playbooks para cada escenario.

4

4. Establecer Baselines y SLIs

Define qué es "normal" para tu sistema multi-agente. Cuántos loops de corrección son aceptables. Qué tasa de validación es expectable. Esto te permite detectar anomalías antes de que se conviertan en incidentes.

Lessons Learned

Después de 6 meses operando sistemas multi-agente en producción con este stack de observabilidad, estos son los aprendizajes clave:

1. La Visibilidad es un Feature, No un Bug

El día que instrumentamos completamente el sistema con OpenTelemetry, descubrimos que el 12% de las tareas tenían loops de corrección de 4+ iteraciones, pero nadie lo sabía porque no se instrumentaba. La métrica de "latencia promedio" se veía bien (3.2s), pero el p95 era de 18s. Los agentes entraban en loops de auto-corrección que consumían tokens sin mejorar el output.

Lección: Sin instrumentación de decisiones, estás operando a ciegas. Un sistema multi-agente sin trazas de razonamiento es un sistema que no sabes si está funcionando — solo sabes que no está explotando.

2. El Costo es una Métrica de Primera Clase

Implementamos tracking de costo por tarea y descubrimos que el agente de análisis de datos tenía un costo promedio de $0.47 por tarea, pero el p95 era de $3.20. Al investigar, encontramos que en ciertos tipos de consultas, el modelo invocaba la herramienta de database 6-7 veces con variaciones mínimas en el query.

Lección: El costo es una métrica de performance. Un agente que funciona bien pero consume $5 por tarea no es sostenible. Instrumenta el tracking de costo real, no solo estimado.

3. La Calidad del Output es Medible, pero Requiere Evaluadores

Al principio asumimos que si no había excepción, el output era correcto. Implementamos evaluadores automáticos (modelos "juez") que califican el output del agente principal y descubrimos que la tasa de outputs inválidos era del 8% — completamente invisible en métricas tradicionales.

Lección: Necesitas métricas de calidad evaluadas, no inferidas. Implementa evaluadores automáticos como spans dentro del trace y usa sus resultados como métricas de SLI.

4. GCP Cloud Operations es Poderoso, pero Requiere Configuración

GCP tiene soporte nativo para OpenTelemetry, pero las configuraciones por defecto no están optimizadas para sistemas multi-agente. Tuvimos que ajustar la frecuencia de muestreo, definir métricas custom, y configurar alertas específicas para el comportamiento de agentes.

Lección: La herramienta solo es tan buena como su configuración. Invierte tiempo en setup inicial de métricas y alertas específicas para tu dominio.

5. La Observabilidad Habilita Mejora Continua

Con trazas completas, podemos hacer A/B testing de diferentes prompts, configuraciones de agentes, y modelos. Observamos que cambiar el system prompt del agente de análisis redujo los loops de corrección de 2.3 a 1.7 promedio, ahorrando $800/mes en tokens.

Lección: La observabilidad no es solo para debuggear incidentes — es para optimizar continuamente el sistema. Cada cambio debe tener métricas antes y después.

Resultado de Implementación

Después de 6 meses de observabilidad completa, redujimos el costo operativo de nuestros sistemas multi-agente en 34%, mejoramos la tasa de validación de 82% a 94%, y redujimos el MTTR de incidentes de 4.5 horas a 45 minutos. La inversión en instrumentación se pagó 12x en el primer año.

Conclusion

La monitorabilidad y observabilidad de sistemas multi-agente no es un problema de tecnología — es un problema de modelo mental. Los SREs y Platform Engineers que intentan aplicar las mismas métricas y dashboards que usan para microservicios tradicionales se encontrarán con sistemas que reportan "todo verde" mientras producen resultados incorrectos, inconsistentes, o costosos.

OpenTelemetry en GCP provee las herramientas necesarias para construir una capa de observabilidad production-ready para sistemas multi-agente. La clave está en instrumentar las decisiones, no solo las ejecuciones. Cada llamada LLM, cada invocación de tool, cada criterio de validación — todo debe ser observable y medible.

💡Próximos Pasos

Este post es parte 2 de la serie "Arquitectura de Software Avanzada". En el siguiente post, exploraremos estrategias de cost optimization específicas para sistemas multi-agente, incluyendo caching inteligente, batching de requests, y técnicas de prompt engineering para reducir token consumption.

La próxima vez que tu sistema multi-agente falle en producción, ¿tendrás los traces necesarios para entender qué salió mal? ¿O volverás a revisar logs que dicen "completado con éxito" mientras el output está roto?

La observabilidad no es un lujo — es un requisito para operar sistemas multi-agente en producción. Y como vimos, las herramientas están disponibles. Solo falta aplicarlas correctamente.

¿Qué stack de observabilidad estás usando para tus sistemas multi-agente? Me interesa saber qué está funcionando y qué no en producción.


Posts Relacionados:

Referencias rápidas

Vista general

Más en esta serie

Serie: Arquitectura de Software Avanzada

// newsletter

¿Te sirvió este artículo?

Recibe los siguientes en tu inbox. Sin spam, cancela cuando quieras.

Discusión

Escrito por Jorge Ochoa. ¿Encontraste un error?

Abrir en GitHub