Todos los artículos

// Agentes en producción

Go para Backend de Agentes: Por Qué y Cuándo

Descubre por qué Go domina en backend de agentes IA con goroutines, performance compilado y escalabilidad masiva en arquitecturas multi-agente.

25 de junio de 202612 min de lectura

Problem: Python es el Estándar, Pero No la Solución Completa

Python dominó la revolución de IA por una razón simple: es el lenguaje del ecosistema. PyTorch, TensorFlow, LangChain, y prácticamente todos los frameworks de agentes están escritos en Python. Es genial para prototipar rápido, investigar, y construir MVPs. Pero cuando llevas sistemas de agentes a producción a gran escala, Python muestra sus grietas.

⚠️Atención

Advertencia: Python tiene limitaciones fundamentales que no puedes ignorar en producción. El GIL (Global Interpreter Lock) limita el paralelismo real, el garbage collector causa pausas impredecibles, y el overhead del intérprete se vuelve significativo cuando procesas miles de requests por segundo.

El Dilema en Arquitecturas Multi-Agente

Imagina un orquestador de agentes que debe:

  • Gestionar 100+ agentes ejecutando tareas simultáneas
  • Manejar timeouts y retries distribuidos
  • Procesar streaming de LLM responses en tiempo real
  • Emitir métricas y tracing para observabilidad
  • Escalar horizontalmente sin fricción

Python puede manejar esto, pero con tradeoffs costosos:

  • Multiprocesing aumenta drásticamente el consumo de memoria
  • Async/await añade complejidad cognitiva y no soluciona el GIL
  • Cold starts en serverless se hacen evidentes
  • Debugging de race conditions es complicado
💡Nota

Nota: No estoy diciendo que Python sea "malo". Es excelente para muchas cosas. Pero en arquitecturas de agentes donde la concurrencia y el throughput son críticos, Go ofrece ventajas estructurales que Python simplemente no tiene por diseño.

Core Concept: Go como Orquestador de Agentes

Go (Golang) fue diseñado explícitamente para resolver problemas de concurrencia y escalabilidad en sistemas distribuidos. Sus características clave para backend de agentes incluyen:

1. Goroutines: Concurrency de Grano Fino

Las goroutines son threads ligeros gestionados por el runtime de Go. Una goroutine pesa ~2KB de stack inicial y puede escalarse a millones en una sola máquina. Comparado con threads del OS (~8MB), la diferencia es masiva.

2. Channels: Comunicación Segura entre Goroutines

Los channels de Go implementan el modelo CSP (Communicating Sequential Processes). No compartes memoria por comunicarla; comunicas memoria compartiendo. Esto elimina categorías enteras de bugs de concurrencia.

3. Performance Tipo Compilado

Go compila a código máquina nativo. No hay overhead de intérprete ni JIT. Resultados de benchmarks consistentemente muestran 5-10x mejor performance que Python en tareas I/O-bound.

4. Ecosistema de Microservicios Maduro

Go tiene soporte de primera clase para gRPC, HTTP/2, y protocolos modernos. El tooling para observabilidad (OpenTelemetry, Prometheus) es robusto y bien integrado.

Excelente

Resultado: Go te permite construir un orquestador de agentes que procesa miles de requests concurrentes con overhead mínimo, latencia predecible, y sin las trampas del GIL.

Implementation: Agent Gateway en Go

Vamos a construir un agente gateway completo que demuestra estos conceptos en acción.

Arquitectura del Gateway

El gateway actúa como un orquestador entre clientes y múltiples agentes/LLM providers:

terminal
Cliente → Go Gateway → Worker Pool → LLM Providers
                   ↓
              OpenTelemetry
                   ↓
              Metrics & Tracing

1. Estructura del Proyecto

go
// cmd/gateway/main.go
package main

import (
    "context"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"

    "github.com/prometheus/client_golang/prometheus/promhttp"
)

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()

    // Configuración
    cfg := loadConfig()

    // Inicializar componentes
    agentPool := NewAgentPool(cfg.WorkerCount, cfg.MaxQueueSize)
    telemetry := setupTelemetry(cfg.TelemetryConfig)

    // HTTP server
    mux := http.NewServeMux()
    mux.Handle("/metrics", promhttp.Handler())
    mux.HandleFunc("/api/agent/invoke", agentHandler(agentPool, telemetry))

    srv := &http.Server{
        Addr:         ":" + cfg.Port,
        Handler:      mux,
        ReadTimeout:  10 * time.Second,
        WriteTimeout: 30 * time.Second,
        IdleTimeout:  120 * time.Second,
    }

    // Graceful shutdown
    go func() {
        log.Printf("Server starting on :%s", cfg.Port)
        if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("Server error: %v", err)
        }
    }()

    shutdown := make(chan os.Signal, 1)
    signal.Notify(shutdown, syscall.SIGINT, syscall.SIGTERM)
    <-shutdown

    log.Println("Shutting down server...")
    shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer shutdownCancel()

    agentPool.Shutdown()
    if err := srv.Shutdown(shutdownCtx); err != nil {
        log.Printf("Server shutdown error: %v", err)
    }
    log.Println("Server stopped")
}

2. Worker Pool para Procesamiento Concurrente

go
// internal/agent/pool.go
package agent

import (
    "context"
    "sync"
    "time"
)

type AgentRequest struct {
    ID        string
    AgentType string
    Payload   []byte
    Callback  chan<- AgentResponse
}

type AgentResponse struct {
    ID      string
    Result  []byte
    Error   error
    Latency time.Duration
}

type AgentPool struct {
    workers    int
    queue      chan AgentRequest
    wg         sync.WaitGroup
    shutdown   chan struct{}
    llmClient  LLMClient
    telemetry  Telemetry
}

func NewAgentPool(workers, queueSize int) *AgentPool {
    pool := &AgentPool{
        workers:   workers,
        queue:     make(chan AgentRequest, queueSize),
        shutdown:  make(chan struct{}),
        llmClient: NewOpenAIClient(),
        telemetry: NewOpenTelemetry(),
    }

    pool.start()
    return pool
}

func (p *AgentPool) start() {
    for i := 0; i < p.workers; i++ {
        p.wg.Add(1)
        go p.worker(i)
    }
}

func (p *AgentPool) worker(id int) {
    defer p.wg.Done()

    for {
        select {
        case req := <-p.queue:
            // Crear span de tracing
            ctx, span := p.telemetry.StartSpan(context.Background(),
                "agent.process",
                map[string]string{
                    "agent.id":      req.ID,
                    "agent.type":    req.AgentType,
                    "worker.id":     string(rune(id)),
                },
            )
            defer span.End()

            // Procesar request
            start := time.Now()
            resp := p.processRequest(ctx, req)
            resp.Latency = time.Since(start)

            // Emitir métricas
            p.telemetry.RecordLatency("agent.process", resp.Latency)
            p.telemetry.RecordRequest("agent.process", resp.Error == nil)

            // Enviar respuesta
            req.Callback <- resp

        case <-p.shutdown:
            return
        }
    }
}

func (p *AgentPool) processRequest(ctx context.Context, req AgentRequest) AgentResponse {
    timeout := 30 * time.Second
    ctx, cancel := context.WithTimeout(ctx, timeout)
    defer cancel()

    result, err := p.llmClient.Invoke(ctx, req.AgentType, req.Payload)
    if err != nil {
        return AgentResponse{
            ID:    req.ID,
            Error: err,
        }
    }

    return AgentResponse{
        ID:     req.ID,
        Result: result,
    }
}

func (p *AgentPool) Submit(req AgentRequest) error {
    select {
    case p.queue <- req:
        return nil
    default:
        return ErrQueueFull
    }
}

func (p *AgentPool) Shutdown() {
    close(p.shutdown)
    p.wg.Wait()
}

3. Integración con LLM Providers

go
// internal/llm/openai.go
package llm

import (
    "bytes"
    "context"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "time"
)

type OpenAIClient struct {
    apiKey     string
    baseURL    string
    httpClient *http.Client
}

type ChatRequest struct {
    Model    string    `json:"model"`
    Messages []Message `json:"messages"`
    Stream   bool      `json:"stream,omitempty"`
}

type Message struct {
    Role    string `json:"role"`
    Content string `json:"content"`
}

type ChatResponse struct {
    ID      string   `json:"id"`
    Object  string   `json:"object"`
    Created int64    `json:"created"`
    Model   string   `json:"model"`
    Choices []Choice `json:"choices"`
}

type Choice struct {
    Index        int     `json:"index"`
    Message      Message `json:"message"`
    FinishReason string  `json:"finish_reason"`
}

func NewOpenAIClient() *OpenAIClient {
    return &OpenAIClient{
        apiKey:  os.Getenv("OPENAI_API_KEY"),
        baseURL: "https://api.openai.com/v1",
        httpClient: &http.Client{
            Timeout: 60 * time.Second,
        },
    }
}

func (c *OpenAIClient) Invoke(ctx context.Context, agentType string, payload []byte) ([]byte, error) {
    // Parsear payload
    var req ChatRequest
    if err := json.Unmarshal(payload, &req); err != nil {
        return nil, fmt.Errorf("invalid payload: %w", err)
    }

    // Serializar request
    body, err := json.Marshal(req)
    if err != nil {
        return nil, fmt.Errorf("marshal request: %w", err)
    }

    // Crear HTTP request
    httpReq, err := http.NewRequestWithContext(ctx,
        "POST",
        fmt.Sprintf("%s/chat/completions", c.baseURL),
        bytes.NewReader(body),
    )
    if err != nil {
        return nil, fmt.Errorf("create request: %w", err)
    }

    httpReq.Header.Set("Content-Type", "application/json")
    httpReq.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.apiKey))

    // Ejecutar request con retries
    var resp *http.Response
    err = retry(ctx, 3, 1*time.Second, func() error {
        var err error
        resp, err = c.httpClient.Do(httpReq)
        return err
    })
    if err != nil {
        return nil, fmt.Errorf("execute request: %w", err)
    }
    defer resp.Body.Close()

    // Leer response
    respBody, err := io.ReadAll(resp.Body)
    if err != nil {
        return nil, fmt.Errorf("read response: %w", err)
    }

    if resp.StatusCode != http.StatusOK {
        return nil, fmt.Errorf("API error: %s", string(respBody))
    }

    return respBody, nil
}

func retry(ctx context.Context, maxAttempts int, initialDelay time.Duration, fn func() error) error {
    var lastErr error
    delay := initialDelay

    for attempt := 0; attempt < maxAttempts; attempt++ {
        if attempt > 0 {
            select {
            case <-time.After(delay):
            case <-ctx.Done():
                return ctx.Err()
            }
            delay *= 2 // Exponential backoff
        }

        if err := fn(); err != nil {
            lastErr = err
            continue
        }
        return nil
    }

    return lastErr
}

4. Métricas y Observabilidad con OpenTelemetry

go
// internal/telemetry/opentelemetry.go
package telemetry

import (
    "context"
    "time"

    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/attribute"
    "go.opentelemetry.io/otel/exporters/jaeger"
    "go.opentelemetry.io/otel/metric"
    "go.opentelemetry.io/otel/sdk/resource"
    sdktrace "go.opentelemetry.io/otel/sdk/trace"
    semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
    "go.opentelemetry.io/otel/trace"
)

type Telemetry struct {
    tracer      trace.Tracer
    meter       metric.Meter
    latencyHist metric.Float64Histogram
    requestCnt  metric.Int64Counter
}

func NewOpenTelemetry() *Telemetry {
    // Configurar exporter Jaeger
    exp, err := jaeger.New(jaeger.WithCollectorEndpoint(
        jaeger.WithEndpoint("http://localhost:14268/api/traces"),
    ))
    if err != nil {
        panic(err)
    }

    // Configurar tracer provider
    tp := sdktrace.NewTracerProvider(
        sdktrace.WithBatcher(exp),
        sdktrace.WithResource(resource.NewWithAttributes(
            semconv.SchemaURL,
            semconv.ServiceNameKey.String("agent-gateway"),
        )),
    )
    otel.SetTracerProvider(tp)

    // Configurar meter provider
    meterProvider := metric.NewMeterProvider()
    otel.SetMeterProvider(meterProvider)

    // Crear métricas
    meter := meterProvider.Meter("agent-gateway")
    latencyHist, err := meter.Float64Histogram(
        "agent.process.latency",
        metric.WithDescription("Agent processing latency"),
    )
    if err != nil {
        panic(err)
    }

    requestCnt, err := meter.Int64Counter(
        "agent.process.requests",
        metric.WithDescription("Agent request count"),
    )
    if err != nil {
        panic(err)
    }

    return &Telemetry{
        tracer:      tp.Tracer("agent-gateway"),
        meter:       meter,
        latencyHist: latencyHist,
        requestCnt:  requestCnt,
    }
}

func (t *Telemetry) StartSpan(ctx context.Context, name string, attrs map[string]string) (context.Context, trace.Span) {
    attributes := make([]attribute.KeyValue, 0, len(attrs))
    for k, v := range attrs {
        attributes = append(attributes, attribute.String(k, v))
    }
    return t.tracer.Start(ctx, name, trace.WithAttributes(attributes...))
}

func (t *Telemetry) RecordLatency(name string, latency time.Duration) {
    t.latencyHist.Record(context.Background(),
        float64(latency.Milliseconds()),
        metric.WithAttributes(attribute.String("operation", name)),
    )
}

func (t *Telemetry) RecordRequest(name string, success bool) {
    t.requestCnt.Add(context.Background(), 1,
        metric.WithAttributes(
            attribute.String("operation", name),
            attribute.Bool("success", success),
        ),
    )
}

5. Handler HTTP con Streaming

go
// internal/handler/agent.go
package handler

import (
    "encoding/json"
    "io"
    "net/http"
    "time"
)

type agentHandler struct {
    pool     *AgentPool
    telemetry *Telemetry
}

func agentHandler(pool *AgentPool, telemetry *Telemetry) http.HandlerFunc {
    h := &agentHandler{
        pool:     pool,
        telemetry: telemetry,
    }
    return h.handle
}

func (h *agentHandler) handle(w http.ResponseWriter, r *http.Request) {
    // Solo POST
    if r.Method != http.MethodPOST {
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        return
    }

    // Parsear body
    body, err := io.ReadAll(r.Body)
    if err != nil {
        http.Error(w, "Bad request", http.StatusBadRequest)
        return
    }

    // Crear request ID
    reqID := generateRequestID()

    // Crear canal para respuesta
    respChan := make(chan AgentResponse, 1)

    // Submit al worker pool
    req := AgentRequest{
        ID:        reqID,
        AgentType: r.URL.Query().Get("type"),
        Payload:   body,
        Callback:  respChan,
    }

    if err := h.pool.Submit(req); err != nil {
        http.Error(w, "Queue full", http.StatusServiceUnavailable)
        return
    }

    // Esperar respuesta con timeout
    ctx, cancel := context.WithTimeout(r.Context(), 35*time.Second)
    defer cancel()

    select {
    case resp := <-respChan:
        if resp.Error != nil {
            http.Error(w, resp.Error.Error(), http.StatusInternalServerError)
            return
        }

        w.Header().Set("Content-Type", "application/json")
        w.Write(resp.Result)

    case <-ctx.Done():
        http.Error(w, "Timeout", http.StatusGatewayTimeout)
        return
    }
}

func generateRequestID() string {
    return time.Now().Format("20060102150405") + "-" +
           randomString(8)
}

Lessons Learned: Tradeoffs y Patrones de Migración

Cuándo Usar Go vs Python

Tip

Patrón Híbrido: Usa Python para el core de AI (training, fine-tuning, investigación) y Go para el orquestador/gateway que maneja concurrencia, routing, y observabilidad. Ambos lenguajes pueden comunicarse vía gRPC o HTTP.

Patrones de Migración

1

1. Wrapper Pattern

Mantiene tu código Python existente y lo envuelve en un servicio gRPC expuesto por Go. Go actúa como un proxy inteligente que maneja routing, retries, y observabilidad.

2

2. Gradual Translation

Identifica los cuellos de botella de performance (hot paths) y traduce esos componentes específicos a Go primero. Deja el resto en Python.

3

3. Protocol Boundary

Define un contrato claro (gRPC, HTTP, message queue) entre el orquestador en Go y los servicios de Python. Esto permite evolución independiente.

Tradeoffs Importantes

Go no es una solución mágica. Considera:

  1. Curva de aprendizaje: Go es más explícito que Python. Los desarrolladores de Python necesitan tiempo para adaptarse a las convenciones de Go (error handling, interfaces, channels).

  2. Ecosistema AI más pequeño: Aunque crece rápido, el ecosistema de Go para AI no es tan rico como Python. Para tareas de ML complejas, Python sigue siendo superior.

  3. Verbosidad: Go requiere más código que Python para la misma funcionalidad. Esto puede ser una ventaja (legibilidad) o desventaja (boilerplate) dependiendo del contexto.

  4. Compilación: El ciclo edit-compile-run es más lento que Python. Sin embargo, en sistemas de larga duración (servers), esto es irrelevante.

⚠️Atención

Pitfall: No migres a Go solo porque es "moderno". Hazlo cuando tengas métricas que justifiquen el cambio: latencia alta, escalabilidad limitada, o problemas de concurrencia que no se pueden resolver en Python.

Conclusion: Guía de Decisión

Go es ideal para backend de agentes cuando:

Necesitas procesar miles de requests concurrentes
La latencia y throughput son críticos
El orquestador debe ser altamente disponible y escalable
Quieres observabilidad robusta con overhead mínimo
El deployment debe ser simple (single binary)

Mantén Python cuando:

Estás prototipando o en fase de investigación
El ecosistema de Python es insustituible
La velocidad de desarrollo es más importante que performance
El workload no escala a miles de requests concurrentes

Excelente

Takeaway: Go y Python no son mutuamente excluyentes. En arquitecturas de sistemas de agentes modernos, el enfoque óptimo suele ser híbrido: Python para el core de AI, Go para el orquestador/gateway. Esto te da lo mejor de ambos mundos.

Lecturas Adicionales

Para profundizar en arquitectura de sistemas, te recomiendo:


La arquitectura de sistemas de agentes está evolucionando rápidamente. Go ofrece una base sólida para construir orquestadores escalables y observables, mientras que Python sigue siendo el rey del ecosistema AI. La clave está en entender las fortalezas de cada herramienta y aplicarlas donde brillan.

¿Tienes experiencia migrando sistemas de agentes a Go? ¿O prefieres el enfoque híbrido? Me encantaría escuchar tus experiencias en los comentarios.

Referencias rápidas

Vista general

Recursos externos

Incluye recursos adicionales en el frontmatter para que aparezcan aquí.

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