Volver al blog
Las dos mitades del montaje, separadas por una diagonal. A la izquierda, sobre fondo oscuro, la gráfica de coste por modelo de Langfuse a lo largo de tres meses, con el gasto acumulado del periodo. A la derecha, la tabla de la pasarela: cuatro alias estables —chat-estandar, chat-premium, razonador y economico— apuntando a modelos de cuatro proveedores distintos, cada uno con su coste de entrada y de salida por millón de tokens.

LLMOps en un clúster propio con LiteLLM, Langfuse y ArgoCD

LaboratorioLLMOpsGitOpsObservabilidadIA aplicada

La pregunta con la que empecé era razonable: si ya tengo la infraestructura declarada en Git y ArgoCD reconciliando el clúster, ¿cuál es el siguiente escalón? La respuesta que encuentras en cualquier sitio es una arquitectura de referencia con seis o siete piezas —Crossplane, Argo Workflows, MLflow, KServe, Kubeflow— y la sensación de que te faltan todas.

Las fui tachando una a una y acabé donde ya estaba. Pero con una diferencia que sí importa: ahora sé por qué. Este post es ese recorrido —qué descarté y con qué razón—, qué corre hoy en el clúster, y el problema que sigo sin resolver.

Lo que corre hoy

Tres piezas, y cada una resuelve un problema que duele de verdad cuando pones agentes en producción.

Las claves de los proveedores no viven en el repositorio. Hay una única fuente de secretos y el operador de External Secrets los materializa donde hacen falta: hoy son trece secretos distintos en el namespace de producción, cada uno declarado en Git como una referencia, nunca como un valor. Rotar una clave de proveedor es cambiarla en un sitio.

Delante de los modelos hay una pasarela, LiteLLM, y esa decisión es la que más se nota con el tiempo. Las aplicaciones no hablan con Anthropic ni con OpenAI: hablan con un alias estable. Cambiar de modelo detrás de ese alias no toca una línea de código de producto. Y como todo el tráfico pasa por ahí, se puede poner presupuesto por clave: cada agente o cada equipo tiene la suya, con su tope, y cuando se acaba se acaba.

Y encima, Langfuse autoalojado, con su propia Application en ArgoCD y sus secretos aparte. Ahí acaba cada llamada con su coste, sus tokens, su latencia y su sesión. Lleva meses corriendo en producción, y la pasarela también. Los dos se declaran en Git como cualquier otra cosa del clúster, así que el gateway de modelos y la observabilidad del gasto se despliegan con el mismo mecanismo que el resto de la plataforma. Eso es todo el «Ops» que hay en LLMOps.

77 / 51días en producción · trazas y pasarela
13secretos, ninguno en el repositorio
1alias por modelo, estable
0modelos alojados por mí

Para no depender de producción para probar cosas, el mismo montaje vive en un laboratorio reproducible: pasarela, trazas, almacenamiento y una API que los consume, todo en un contenedor de desarrollo. Tiene un detalle del que estoy contento: un modelo falso que no llama a ningún proveedor y no necesita clave, pero lleva precios declarados. Recorre exactamente el mismo camino de cálculo de coste que un modelo real, así que puedes validar que la cadena entera funciona —que el gasto se calcula, se devuelve y llega a las trazas— sin gastar un céntimo.

# Modelo sintético: no llama a ningún proveedor y no necesita API key.
# Lleva pricing declarado para que el gasto sea visible end-to-end sin gastar dinero.
- model_name: mock
  litellm_params:
    model: openai/mock
    api_key: not-needed
    mock_response: "El stack funciona: se ha calculado el coste y se ha enviado a las trazas."
    input_cost_per_token: 0.000003
    output_cost_per_token: 0.000015

Los descartes, uno a uno

Esta es la parte útil, porque descartar bien es más difícil que añadir.

  • Crossplane. Sirve para aprovisionar infraestructura de nube desde manifiestos del clúster. Mi infraestructura es local y ya está encendida: las máquinas existen, Kubernetes está encima y las dependencias corren como pods dentro. Tendría sentido si quisiera que un commit creara máquinas virtuales al vuelo en el hipervisor, o si gestionara bases de datos fuera del clúster. No es el caso, y meterlo sería añadir una capa que no aprovisiona nada.
  • Argo Workflows. Es un motor de flujos nativo del clúster, y brilla cuando hay que procesar volúmenes grandes en paralelo o encadenar pasos con dependencias. Yo no tengo pipelines de datos: tengo peticiones. Lo que sí tengo —construir imágenes, pasar tests, publicar versiones— ya lo hace la integración continua del repositorio, que es su terreno natural.
  • MLflow. Es un registro de modelos y un cuaderno de experimentos. No entreno ninguno. Un registro de modelos sin entrenamiento es un armario vacío con inventario.
  • KServe y Seldon. Sirven un modelo declarándolo, sin escribir la API a mano. La cuestión es que no tengo pesos que servir: consumo modelos de terceros por su API. Y para tenerlo hay que montar debajo un gestor de certificados y una capa de red o de serverless, que es mucha máquina para no alojar nada.
  • Kubeflow. La suite entera, que por dentro usa varias de las anteriores. Si las piezas sueltas no me hacen falta, el paquete que las agrupa tampoco.

No era MLOps, era LLMOps

Al llegar al cuarto descarte se ve el patrón: todas esas herramientas dan por supuesto que el modelo es tuyo. Que lo entrenas, lo versionas, lo guardas y lo sirves. Mi problema es el contrario. Los modelos son de otros y están detrás de una API de pago; lo que tengo que gobernar es el tráfico hacia ellos.

La arquitectura de referencia no era un plan: era un menú. El trabajo no estaba en instalar, estaba en tachar.
Conclusión de la revisiónde MLOps a LLMOps

Y cuando el problema se llama por su nombre, las prioridades se reordenan solas. Lo crítico pasa a ser dónde viven las claves, quién puede gastar y cuánto, qué ocurre cuando un proveedor devuelve un error y hay que enrutar a otro, y cuánto cuesta exactamente cada respuesta. Nada de eso lo cubre un registro de modelos. Todo eso lo cubren tres piezas que caben en el mismo Git que el resto de la plataforma.

Lo que las trazas no ven

Aquí es donde el montaje se queda corto, y prefiero decirlo que fingir que está resuelto.

Cuando una respuesta sale mal en un sistema que consulta documentos antes de responder, hay dos culpables posibles. O la búsqueda devolvió los fragmentos equivocados y el modelo no tenía con qué contestar, o la búsqueda acertó y el modelo se lo saltó. Las trazas ven bien el segundo caso: ahí está el prompt, la respuesta y lo que costó. Del primero apenas dicen nada.

Miré herramientas especializadas en eso y las descarté para el caso general —solapan casi por completo con lo que ya tengo—, salvo justo en este escenario. Y este escenario es el mío: la búsqueda del portal es híbrida, mezcla coincidencia de palabras con similitud semántica, y el equilibrio entre las dos lo he ajustado a mano, mirando resultados y decidiendo que parecían mejores.

«Parecían mejores» no es una métrica. Hoy no tengo forma automática de saber si tocar esos pesos, o cambiar un prompt, mejora o empeora las respuestas. Lo que haría falta no es una herramienta más: es un conjunto de preguntas de referencia con sus respuestas buenas, una evaluación que corra en la pipeline cuando alguien toca la búsqueda, y un umbral por debajo del cual el cambio no entra. Eso está pendiente, y es lo siguiente que montaré.

El otro camino: servir modelos propios

Queda la otra mitad de la pregunta: dejar de pagar por token y servir modelos abiertos en hierro propio. Aviso por delante de que esta sección es investigación, no experiencia: no lo he montado, y el clúster no tiene ninguna GPU. Es el mapa que me hice antes de decidir si compraba hardware, y el mapa ya dice bastante.

Lo primero es el sistema operativo. Mis nodos corren Talos, que es inmutable y no tiene gestor de paquetes: los drivers se hornean en la imagen como extensiones. Para una tarjeta moderna hay que elegir los módulos abiertos, y para una anterior a Turing los propietarios; y el toolkit de contenedores tiene que ser de la misma rama que el driver, o no se hablan. Luego hay que cargar los módulos explícitamente en la configuración de máquina, un paso que es fácil olvidar.

# Extensiones de la imagen (misma rama en driver y toolkit)
#   siderolabs/nvidia-open-gpu-kernel-modules-production
#   siderolabs/nvidia-container-toolkit-production

# Y los módulos, cargados a mano en el machine config:
machine:
  kernel:
    modules:
      - name: nvidia
      - name: nvidia_uvm
      - name: nvidia_drm
      - name: nvidia_modeset

Hay además dos fricciones que no aparecen en los tutoriales. Talos no se instala en una partición: reclama el disco entero y crea su propia tabla, así que un equipo que ya tenga un hipervisor encima obliga a elegir entre dedicarle un disco o formatearlo. Y si en vez de instalarlo en el metal quieres pasar la tarjeta a una máquina virtual, en placas de consumo la GPU suele compartir grupo de aislamiento con otros dispositivos, lo que obliga a un parche que compra separación a cambio de estabilidad.

Por encima hay otra bifurcación. Se puede declarar el servicio de inferencia de forma mínima, apoyándose en la red que ya tienes, o montar la capa serverless que apaga el pod cuando nadie pregunta y libera la tarjeta. Lo primero es más ligero y deja la GPU reservada siempre; lo segundo la libera, a cambio de otra pieza de red que mantener.

Y luego está la restricción que decide de verdad si esto compensa, y que tiene poco que ver con Kubernetes. Un pod que pide una GPU la reserva entera: mientras un modelo esté cargado, cualquier otro se queda esperando a que el primero se apague. Levantar uno no es instantáneo, porque hay que subir los pesos a la memoria de la tarjeta. Y esos pesos ocupan: un modelo de ocho mil millones de parámetros en fp16 llena una tarjeta de dieciséis gigas él solo, sin dejar sitio para el contexto de la conversación; cuantizado a cuatro bits baja a una cuarta parte —unos cuatro gigas— y ya respira. Si lo que quieres es servir muchas variantes a la vez con una sola tarjeta, la salida no es levantar muchos modelos: es un modelo base con adaptadores pequeños que se intercambian en caliente.

Con todo eso sobre la mesa, la pregunta deja de ser técnica. Alojar modelos propios tiene sentido cuando el volumen es alto y estable, cuando los datos no pueden salir, o cuando quieres independencia de proveedor. Con tráfico irregular, una pasarela que paga por uso gana casi siempre — y ese es exactamente mi caso hoy.

Lo que me llevo

Que una arquitectura de referencia no es una lista de la compra. Cada una de las herramientas que taché es excelente resolviendo el problema para el que se hizo, y ninguna resolvía uno mío. La pregunta útil ante cualquiera de ellas no es si está en la lista, sino cuál de mis problemas quita hoy — y si no sé nombrarlo, todavía no me hace falta.

Es la misma conversación que tengo cuando alguien me pide «montar MLOps». Casi siempre, lo que necesita no es entrenar ni alojar nada: es saber qué está gastando, poder cambiar de modelo sin tocar el producto, y que las claves no anden sueltas. Eso son tres piezas y un repositorio, no una suite. Lo difícil no es instalarlas: es resistirse a instalar las otras seis.

Más del laboratorio