Docs / 02. Factoría Multimodelo de LLMs

Desacoplar el modelo del runtime: factoría multimodelo de LLMs, inferencia soberana y fallback determinista

En qué momento y de qué manera conectamos con modelos de lenguaje: de Groq y Gemini a Ollama air-gapped y simulación sin tokens en tests.

🎙️
Escucha el artículo elaborado con IA AUDIO IA
Escucha el análisis arquitectónico completo elaborado en alta fidelidad
Desacoplar el modelo del runtime: factoría multimodelo de LLMs, inferencia soberana y fallback determinista

"El peor error arquitectónico al diseñar un agente es atar su lógica de negocio con la API de un proveedor comercial concreto. Un modelo de lenguaje no es más que una unidad de procesamiento de texto intercambiable; si tu runtime no puede conmutar en milisegundos entre una GPU LPU de alta velocidad en la nube, un servidor Ollama aislado en una red air-gapped o un mock determinista sin coste para CI/CD, no has construido una plataforma agéntica soberana: has construido un envoltorio cautivo."


1. El dilema de diseño: Acoplamiento a SDKs privativos vs. Factoría Canónica

Uno de los antipatrones más frecuentes en el desarrollo de arquitecturas agénticas es el acoplamiento directo del código de negocio al SDK de un proveedor concreto (e.g., llamar directamente a openai.OpenAI(), @google/genai o anthropic.Anthropic() en el corazón de los nodos decisionales).

+---------------------------------------------------------------------------------+
| Antipatrón: Acoplamiento Directo al Proveedor                                  |
|                                                                                 |
| Nodo Reason ---> [import openai] ---> openai.chat.completions.create()         |
|                       |                                                         |
|                       +--> Excepciones específicas del vendor (APIConnectionError)
|                       +--> Formato de tool_calls no portable                    |
|                       +--> Inviabilidad de pruebas CI/CD offline                |
+---------------------------------------------------------------------------------+

+---------------------------------------------------------------------------------+
| Patrón jg_ia: Abstracción canónica mediante Factoría Multimodelo                |
|                                                                                 |
| Nodo Reason ---> [llm.bind_tools(tools)] ---> [OpenAICompatibleChatModel]       |
|                        |                              |                         |
|                        |                              +--> Groq Cloud (LPU)     |
|                        |                              +--> Google Gemini        |
|                        |                              +--> Ollama / vLLM Local  |
|                        |                                                        |
|                        +-----------------------------> [MockChatModel (CI/CD)]  |
+---------------------------------------------------------------------------------+

Este acoplamiento introduce cuatro problemas estructurales críticos:

  1. Vendor Lock-in de Protocolo: Cada proveedor impone sutilezas en la serialización de tool_calls, el manejo de mensajes de rol (system, developer, tool) y la gestión de esquemas JSON.
  2. Fragilidad ante Incidentes y Rate Limits: Si el proveedor primario sufre una degradación o agota la cuota (HTTP 429), cambiar de modelo exige refactorizar llamadas o mantener condicionales dispersos por todo el pipeline.
  3. Bloqueo de Entornos Aislados (Air-Gapped): En despliegues donde la soberanía del dato o el cumplimiento normativo impiden el tráfico hacia APIs externas, el agente queda inoperativo si no puede conmutar transparentemente a un backend de inferencia local.
  4. Costes e Imprevisibilidad en Integración Continua (CI/CD): Ejecutar suites de pruebas que realicen llamadas HTTP reales a LLMs comerciales consume tokens, introduce latencias no deterministas y provoca falsos positivos por fluctuaciones de red.

Para resolver este dilema, jg_agente encapsula la inferencia detrás de una factoría canónica (app/core/llm.py) y una interfaz unificada compatible con el ecosistema LangChain/LangGraph, aislando por completo el runtime cognitivo del motor de inferencia subyacente.


2. Ciclo de Vida: ¿En qué momento se invoca el LLM?

El agente no es un bucle infinito que consulta al modelo en cada paso. La arquitectura de jg_agente opera sobre un grafo dirigido acíclico por fases (StateGraph) donde el modelo de lenguaje interviene en puntos estrictamente delimitados:

[Input Usuario] 
       │
       ▼
[classify_node] ──(Heurística / LLM ultraligero)
       │
       ├─► [GREETING] ────────► [respond_node] ──► FIN
       │
       └─► [ACTION] ──────────► [plan_node]
                                     │
                                     ▼
                            ┌────────────────┐
                     ┌─────►│  reason_node   │◄─────┐
                     │      └───────┬────────┘      │
                     │              │               │
                     │       (tool_calls?)          │
                     │        ├── SÍ ──► [tool_node] ─┘
                     │        │
                     │        └── NO ──► [evaluate_node]
                     │                         │
                     │                    (satisfecho?)
                     │                     ├── NO ────┘
                     │                     └── SÍ ──► [respond_node] ──► FIN

El punto de contacto: El nodo reason

El nodo de razonamiento (app/nodes/reason.py) es el cerebro táctico del agente. Recibe el estado actual (AgentState), que contiene el historial de mensajes acumulados, el plan vigente y el contexto de ejecución.

Antes de invocar el modelo, reason_node ejecuta tres operaciones deterministas:

  1. Filtro heurístico de cortesía: Evalúa si la consulta es un saludo simple o agradecimiento mediante _is_simple_greeting_or_chat_llm(), evitando gasto innecesario de contexto o herramientas.
  2. Inspección de capacidades registradas: Consulta el catálogo dinámico de herramientas provisto por CapabilityRegistry (app/core/registry.py).
  3. Enlace dinámico de herramientas (bind_tools): Convierte los esquemas de herramientas habilitadas (plugins y tools del sistema) al formato esperado por el modelo.
# app/nodes/reason.py (fragmento representativo)
async def reason_node(state: AgentState) -> dict:
    llm = get_chat_model()
    registry = get_capability_registry()
    tools = registry.get_tools()
    
    # Enlace canónico de herramientas
    if tools and hasattr(llm, "bind_tools"):
        model = llm.bind_tools(tools)
    else:
        model = llm

    response = await model.ainvoke(messages)
    return {"messages": [response]}

Si el modelo responde con uno o varios tool_calls, el grafo bifurca hacia la fase de validación de seguridad (SSRF, Guards deterministas, confirmación HITL) y ejecución. Si responde con texto plano, el control avanza hacia la evaluación de objetivos.


3. La Factoría Multimodelo (app/core/llm.py)

La factoría get_chat_model() resuelve la instancia del modelo adecuada en tiempo de arranque o ejecución basándose en la configuración del entorno (LLM_PROVIDER, LLM_MODEL).

                                 +-----------------------+
                                 |   get_chat_model()    |
                                 +-----------+-----------+
                                             |
            +--------------------+-----------+-----------+--------------------+
            |                    |                       |                    |
            ▼                    ▼                       ▼                    ▼
     [LLM_PROVIDER]       [LLM_PROVIDER]          [LLM_PROVIDER]       [LLM_PROVIDER]
         "groq"               "gemini"               "ollama"         "mock" / "test"
            │                    │                       │                    │
            ▼                    ▼                       ▼                    ▼
   api.groq.com/openai  generativelanguage...   localhost:11434/v1      MockChatModel
   (LPU Hardware        (Google AI Studio        (Inferencia local      (Simulación léxica
    >250 tok/s)          Flash 2.0)               air-gapped)            determinista)

Configuración por variables de entorno

El desacoplamiento se logra mediante un conjunto homogéneo de variables en .env:

# Configuración del motor de inferencia principal
LLM_PROVIDER=groq                     # Opciones: groq, gemini, ollama, custom, mock
LLM_MODEL=llama-3.1-70b-versatile     # Modelo según el proveedor
LLM_TEMPERATURE=0.0                  # Inferencia determinista para tool calling
LLM_TIMEOUT_SECONDS=30                # Límite duro de espera HTTP

# Parámetros para proveedores locales / custom
OLLAMA_BASE_URL=http://localhost:11434/v1

Proveedores Cloud de alta velocidad: Groq y Gemini

Para flujos conversacionales interactivos y orquestación de herramientas, la latencia del primer token (TTFT) y la velocidad de generación son factores críticos:

  • Groq (LLM_PROVIDER=groq): Emplea infraestructura LPU (Language Processing Unit), alcanzando rendimientos superiores a 250-300 tokens por segundo con modelos de la familia Llama 3.1. Se conecta mediante la API canónica compatible con OpenAI apuntando a https://api.groq.com/openai/v1.
  • Google Gemini (LLM_PROVIDER=gemini): Permite aprovechar Gemini Flash a través de la pasarela compatible OpenAI de Google (https://generativelanguage.googleapis.com/v1beta/openai/), brindando ventanas de contexto masivas a bajo coste.

Ambos proveedores se gestionan en app/core/llm.py mediante la clase OpenAICompatibleChatModel, que normaliza llamadas HTTP asíncronas con httpx.AsyncClient.

Inferencia Local y Soberana: Ollama y vLLM

Cuando el entorno de despliegue exige confidencialidad absoluta (aislamiento de red, datos médicos o financieros sensibles):

  • Ollama (LLM_PROVIDER=ollama): Se enlaza al endpoint local http://localhost:11434/v1. Al respetar la especificación OpenAI, la factoría reutiliza la misma clase base OpenAICompatibleChatModel.
  • vLLM / TGI: Servidores de inferencia en clusters GPU propios se configuran con LLM_PROVIDER=custom y OPENAI_BASE_URL=http://cluster-gpu.internal:8000/v1.

El agente no distingue entre una inferencia ejecutada en un datacenter en California o en una GPU RTX local montada en el rack del cliente.

El modo MockChatModel para CI/CD sin tokens

Uno de los mayores logros del diseño es el proveedor simulado determinista (app/core/llm.py -> MockChatModel). Este mock no es una simple respuesta en blanco; es un motor heurístico capaz de:

  1. Simular el clasificador de intenciones: Si el mensaje de sistema indica clasificación de intenciones, analiza si la consulta contiene léxico de saludo o de acción (docker, servicio, reinicia, consulta, listar) y emite "GREETING" o "ACTION".
  2. Generar tool_calls léxicos sin coste: Cuando las herramientas han sido vinculadas mediante bind_tools(), el mock analiza el texto del usuario contra los nombres de las herramientas registradas (e.g., docker_status, restart_service). Si detecta coincidencia, sintetiza una respuesta con tool_calls válidos y argumentos JSON estructurados.
  3. Cero dependencias externas: Permite que toda la suite de tests unitarios y de integración de jg_agente se ejecute en segundos en GitHub Actions o pipelines de GitLab sin credenciales de API ni conexión a internet.

4. Normalización de Contratos y Tool Calling

La consistencia en el manejo de herramientas es fundamental para evitar excepciones en tiempo de ejecución. Los distintos proveedores presentan discrepancias en cómo representan los argumentos de las herramientas.

+---------------------------------------------------------------------------------+
| Normalización de Tool Calls en OpenAICompatibleChatModel                        |
|                                                                                 |
| 1. Recepción del payload upstream (Groq / Ollama / Gemini):                     |
|    "tool_calls": [                                                              |
|       {                                                                         |
|          "id": "call_abc123",                                                   |
|          "type": "function",                                                    |
|          "function": { "name": "docker_ps", "arguments": "{\"all\": true}" }   |
|       }                                                                         |
|    ]                                                                            |
|                                                                                 |
| 2. Procesamiento defensivo:                                                     |
|    raw_args = call.get("arguments", "{}")                                       |
|    try:                                                                         |
|        parsed_args = json.loads(raw_args)                                       |
|    except json.JSONDecodeError:                                                 |
|        parsed_args = {"_raw_arguments": raw_args, "_parse_error": True}         |
|                                                                                 |
| 3. Emisión canónica:                                                            |
|    AIMessage(content="", tool_calls=[{"name": "docker_ps", "args": parsed_args,|
|                                      "id": "call_abc123"}])                     |
+---------------------------------------------------------------------------------+

Conversión de roles y mensajes

OpenAICompatibleChatModel._to_openai_messages() transforma la jerarquía de mensajes de LangChain al formato estándar:

  • SystemMessage ➔ {"role": "system", "content": ...}
  • HumanMessage ➔ {"role": "user", "content": ...}
  • AIMessage con herramientas ➔ {"role": "assistant", "content": ..., "tool_calls": [...]}
  • ToolMessage ➔ {"role": "tool", "tool_call_id": ..., "content": ...}

Esta normalización garantiza que los plugins desarrollados con jg_agente_sdk reciban siempre estructuras de datos homogéneas, independientemente del backend en ejecución.


5. Resiliencia, Trade-offs y Consideraciones de Producción

Desafío en Producción Mecanismo de Defensa en jg_agente Comportamiento del Sistema
HTTP 429 Rate Limiting Backoff adaptativo con lectura de headers retry-after y cálculo de jitter (compute_rate_limit_wait) Reintento automático transparente sin abortar el flujo agéntico
JSON malformado en tool call Captura defensiva de JSONDecodeError y encapsulación en _raw_arguments El nodo de validación detecta la anomalía y fuerza reintento semántico
Timeouts de red en upstream Límite estricto con timeout=httpx.Timeout(timeout_seconds) Falla controlada que alimenta el checkpoint y notifica al usuario
Alucinación de herramientas Validación estricta contra CapabilityRegistry antes de ejecutar Rechazo inmediato si el nombre no existe en el catálogo registrado

Soberanía del Dato vs. Capacidad Cognitiva

La elección del proveedor no es únicamente una decisión técnica, sino arquitectónica y legal:

  • Modelos Propietarios Cloud (Groq / Gemini / OpenAI): Ideales para razonamientos complejos de múltiples pasos, síntesis de textos no estructurados y dependencias con esquemas JSON altamente intrincados.
  • Modelos Open Source Locales (Ollama / vLLM con Llama 3 / Mistral / Qwen): Indispensables para cumplimiento de RGPD/HIPAA, operabilidad en subredes sin salida a internet (air-gapped) o garantía de coste marginal cero en cargas masivas predecibles.

La factoría get_chat_model() asegura que el negocio pueda cambiar entre ambas modalidades modificando una única variable de entorno, sin alterar una sola línea de lógica en el grafo.


6. Reflexión de Arquitectura: La independencia del proveedor

Desacoplar el modelo del runtime mediante una factoría unificada garantiza:

  1. Portabilidad Inmediata: Transición instantánea entre inferencia de ultra-alta velocidad en la nube y privacidad absoluta en hardware local.
  2. Robustez Operativa: Tolerancia a caídas, rate limits y respuestas malformadas mediante normalización canónica de mensajes.
  3. Eficiencia en el Desarrollo: Suites de integración continua rápidas, fiables y sin coste de facturación de API.

7. Enlaces y Referencias en el Repositorio

Metadatos del documento
Título Desacoplar el modelo del runtime: factoría multimodelo de LLMs, inferencia soberana y fallback determinista
Archivo docs/02_jg_agent/articulos/02_llm_connection_multiprovider_factory.md
Descripción En qué momento y de qué manera conectamos con modelos de lenguaje: de Groq y Gemini a Ollama air-gapped y simulación sin tokens en tests.
Autor Javier Gálvez
Fecha 2026-10-17
Etiquetas
#Artículos #Arquitectura #LLM #Factoría Multimodelo #Groq #Gemini #Ollama #Air-Gapped #Fallback Determinista