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.
"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:
- 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. - 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.
- 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.
- 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:
- 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. - Inspección de capacidades registradas: Consulta el catálogo dinámico de herramientas provisto por
CapabilityRegistry(app/core/registry.py). - 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 ahttps://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 localhttp://localhost:11434/v1. Al respetar la especificación OpenAI, la factoría reutiliza la misma clase baseOpenAICompatibleChatModel. - vLLM / TGI: Servidores de inferencia en clusters GPU propios se configuran con
LLM_PROVIDER=customyOPENAI_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:
- 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". - Generar
tool_callsléxicos sin coste: Cuando las herramientas han sido vinculadas mediantebind_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 contool_callsválidos y argumentos JSON estructurados. - Cero dependencias externas: Permite que toda la suite de tests unitarios y de integración de
jg_agentese 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": ...}AIMessagecon 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:
- Portabilidad Inmediata: Transición instantánea entre inferencia de ultra-alta velocidad en la nube y privacidad absoluta en hardware local.
- Robustez Operativa: Tolerancia a caídas, rate limits y respuestas malformadas mediante normalización canónica de mensajes.
- 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
| 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
|