Parte 3 de 3. Las dos anteriores son el porqué: los números de 25 modelos de IA en local medidos uno a uno y los siete errores que cometí midiendo. Esta es el cómo: la configuración que ha quedado funcionando, para que puedas copiarla.

Si has llegado aquí solo por el resultado, este es el artículo. Todo lo que lleva montar un servidor de IA local con Ollama y un agente de código encima, sin comparativas ni metodología: qué corre en la máquina, con qué ajustes, y los cinco fallos que te van a pasar y no parecen fallos.

El resumen, por si no quieres nada más

  • Ubuntu Server, Ollama y OpenCode. El cliente corre en el servidor, no en el portátil.
  • Un solo modelo residente para agente, código y facturas, con el razonamiento apagado y 128k de contexto.
  • Entre OpenCode y Ollama hay un proxy de treinta líneas. Sin él, el ajuste que más rinde no llega nunca al modelo.
  • Todo son servicios de systemd: la máquina se reinicia y vuelve sola.
  • La carga se reparte asimétrica entre las dos tarjetas: trabaja más la que mejor se refrigera.

Qué corre y dónde

Pieza Dónde Para qué
ollama servicio, puerto 11434 Sirve los modelos
proxy-ollama servicio, 127.0.0.1:11435 Arregla lo que el cliente manda mal
opencode servicio, 127.0.0.1:4096 El agente de código
buscar script en ~/bin Búsqueda web, que el agente no trae
Instrucciones globales ~/.config/opencode/AGENTS.md Las reglas que sigue siempre

El hardware, por si te sirve de referencia: una workstation reacondicionada con dos RTX 5060 Ti de 16 GB, 64 GB de RAM y un Xeon de seis núcleos. Unos 1.850 € en total. Nada de esto depende de esa máquina en concreto: con 32 GB de VRAM repartidos en dos tarjetas, sirve igual.

La decisión que más ordena el montaje es que el cliente vive en el servidor. No por velocidad —la red es ruido frente a los segundos que tarda el modelo en leerse el contexto— sino porque el agente edita el disco de la máquina donde corre. Si lo lanzas desde el portátil, edita el portátil y el servidor solo pone la GPU.

Ollama, y el guardia que evita el fallo silencioso

La instalación es la de siempre. Lo que no viene en ninguna guía son estas cuatro líneas, y son las que evitan el fallo más difícil de diagnosticar de todo el montaje:

sudo tee /etc/systemd/system/ollama.service.d/override.conf > /dev/null <<'EOF'
[Service]
Environment="OLLAMA_CONTEXT_LENGTH=131072"
ExecStartPre=/bin/sh -c 'nvidia-smi -L | grep -q "^GPU 0"'
Restart=on-failure
RestartSec=15
EOF
sudo systemctl daemon-reload && sudo systemctl restart ollama

Qué hace cada cosa:

  • OLLAMA_CONTEXT_LENGTH: es el contexto con el que arranca cualquier modelo que cargues. Ollama trae 4.096 por defecto, que para un agente es inservible: se queda sin sitio a la segunda herramienta. Los 131.072 de ahí arriba son los 128k con los que trabajo, y más abajo está lo que cuestan. Si vas justo de VRAM, 32768 es un suelo razonable para empezar; ten en cuenta que la variable aplica a todos los modelos, también a los pequeños que no lo necesitan.
  • El ExecStartPre es el guardia. Si el driver de NVIDIA no ha inicializado, Ollama arranca igualmente y se cae a CPU: el servicio queda active (running), con total_vram="0 B", y genera a 2 tokens por segundo en vez de 120. En una máquina sin pantalla la única pista es la lentitud, que es facilísimo confundir con «este modelo es malo». Esa línea obliga al servicio a fallar en vez de fingir.

Aviso que me costó un rato: este override.conf se pierde. Un día apareció vacío, probablemente por una actualización del paquete. Compruébalo después de cada apt upgrade, porque el síntoma de que falte no es un error: es que todo parece ir bien y va a 2 tok/s.

sudo systemctl cat ollama | grep -nE "ExecStartPre|Restart=|CONTEXT"

Y al reponerlo, escribe el fichero directamente como arriba en vez de usar systemctl edit: en el editor es fácil escribir por debajo de la línea que avisa de que lo de abajo se descarta, y entonces se guarda sin efecto y sin decir nada.

El modelo titular, y cómo se le añade visión

Un solo modelo residente para todo: agente, código y lectura de facturas. Es un 35B con 3B activos en una cuantización con calibración agéntica, y ocupa 21,7 GB con 128k de contexto.

Venía empaquetado sin visión: quien subió la versión de texto dejó fuera el mmproj.gguf, que es el proyector que convierte una imagen en algo que el modelo puede leer. Estaba en el mismo repositorio, sin usar. Se le añade con un Modelfile de dos FROM:

FROM ~/gguf/Qwen3.6-35B-A3B-APEX-I-Compact.gguf
FROM ~/gguf/mmproj-apex.gguf
ollama create qwen3.6-apex-vision -f Modelfile.vision

Cuesta 200 MB de VRAM y cero velocidad: 21,5 → 21,7 GB, y de 119 a 120 tok/s. A cambio te ahorras cargar y descargar veinte gigas cada vez que pasas de programar a leer una factura, que en una máquina de 32 GB vale más que cualquier punto de benchmark.

Sobre el contexto, esto es lo que cuesta en esta máquina:

Contexto VRAM Libre Velocidad
64k 19,4 GB 12,6 GB 119 tok/s
128k 21,5 GB 10,5 GB 119 tok/s
256k 23,3 GB 8,7 GB 119 tok/s

Cada 32k cuesta 1 GB con la caché de atención comprimida a q8_0, y la velocidad no se mueve entre 64k y 256k. Lo dejé en 128k y no en el máximo porque los modelos se degradan bastante antes de su límite nominal, y eso no lo he medido.

Ese número lo tuve mal durante un mes: daba por hecho que a 128k se desbordaba a la CPU. Era cierto con el modelo anterior y la caché sin comprimir, y dejó de serlo dos cambios después sin que yo volviera a medirlo.

Con 64k tuve una sesión que llegó a 59.678 tokens, OpenCode compactó la conversación para hacer sitio y en la compactación se perdió la pregunta original: el modelo siguió contestando sobre otro proyecto con toda naturalidad. Con 128k, la misma sesión se quedó en 53.374 y no compactó.

OpenCode contra Ollama, con el proxy en medio

Esta es la configuración del servidor, que es la que manda:

{
  "$schema": "https://opencode.ai/config.json",
  "model": "ollama/qwen3.6-apex-vision",
  "provider": {
    "ollama": {
      "npm": "@ai-sdk/openai-compatible",
      "name": "servidor local",
      "options": { "baseURL": "http://127.0.0.1:11435/v1" },
      "models": {
        "qwen3.6-apex-vision": {
          "name": "Titular (agente, código y visión)",
          "limit": { "context": 131072, "output": 8192 },
          "reasoning": false,
          "options": { "extraBody": { "reasoning_effort": "none" } },
          "attachment": true,
          "modalities": { "input": ["text", "image"], "output": ["text"] }
        },
        "lfm2.5:8b": {
          "name": "Rápido, solo inglés",
          "limit": { "context": 131072, "output": 8192 }
        },
        "muse-glimmer:30b-q4_K_M": {
          "name": "Manuscrito y código puntual",
          "limit": { "context": 131072, "output": 8192 },
          "reasoning": false,
          "attachment": true
        }
      }
    }
  },
  "agent": {
    "build":   { "temperature": 0.2 },
    "plan":    { "temperature": 0.2 },
    "general": { "temperature": 0.2 }
  },
  "small_model": "ollama/lfm2.5:8b",
  "compaction": { "auto": true, "prune": true }
}

Tres detalles que hacen fallar la conexión y no dan un error claro:

  • El baseURL lleva /v1. OpenCode habla el dialecto de OpenAI, no la API nativa de Ollama.
  • El puerto es el 11435, no el 11434. Ahí escucha el proxy, y ahora explico por qué.
  • Los identificadores son la etiqueta completa de ollama list, con los dos puntos incluidos.

Y cuatro ajustes que no son evidentes y cambian bastante el comportamiento:

  • limit.context es lo que el cliente pide en cada petición. Aquí es donde se fijan de verdad los 128k de trabajo; la variable del servicio es el suelo con el que arranca el modelo.
  • temperature: 0.2 en los tres agentes. Para escribir código y usar herramientas no quieres creatividad, quieres que repita el comportamiento que ya funcionó. Además, medir con temperatura alta es lo que te hace ver tendencias donde solo hay ruido.
  • small_model: los recados internos —titular de la sesión, resúmenes— se los lleva el modelo pequeño en vez de ocupar al titular. Sale gratis y se nota.
  • compaction: auto compacta la conversación sola antes de reventar el contexto, y prune va tirando lo que ya no hace falta —salidas de herramientas viejas, sobre todo— en vez de esperar a la compactación completa. Con 128k y las dos activas dejé de ver el fallo de perder el hilo a media sesión.

Honestidad sobre prune: lo que sí puedo afirmar es que las sesiones largas dejaron de romperse. No he medido que mejore la calidad de las respuestas, y con lo que cuenta la segunda parte de esta serie sobre creerse mejoras no medidas, no pienso afirmarlo.

Por qué hay un proxy

Porque el cliente manda mal el único parámetro que de verdad importa. Esto es lo que sale por el cable:

{"extraBody": {"reasoning_effort": "none"}, "model": "...", "messages": [...]}

Envía extraBody como una clave literal de nivel superior en vez de fusionar su contenido en el cuerpo. Ollama recibe una clave que no conoce y la ignora sin avisar. Resultado: el razonamiento no se apaga nunca, por mucho que lo pongas en el fichero.

Y apagarlo no es un detalle. En mi banco de pruebas, el mismo modelo hace 25 aciertos de 25 con el razonamiento apagado y 10 de 25 con él encendido. Cuando razona y falla, lo que emite es un turno en blanco: ni llama a la herramienta ni contesta.

El apaño es un proxy de unas treinta líneas que escucha en el 11435, desenvuelve esa clave y reenvía todo lo demás intacto, streaming incluido. Cuando lo arreglen en OpenCode, se quita el proxy y el baseURL vuelve al 11434.

Ojo con generalizar: apagar el razonamiento no es bueno siempre. Con este modelo son 25/25 contra 10/25, pero otro de los que probé gana una tarea entera al razonar. Se decide por pareja modelo-tarea, midiendo.

Entrar desde el portátil

Un alias que abre el túnel SSH y se engancha a la sesión que ya corre en el servidor:

ssh -N -L 4096:127.0.0.1:4096 usuario@servidor &
opencode attach --dir ~/proyectos/<proyecto>

Dos cosas que se pagan caras si se olvidan:

  • Pásale siempre --dir. Sin él cae a un proyecto global con la raíz en /.
  • Usa 127.0.0.1, no localhost. ssh -L abre el puerto solo en IPv4, y muchas aplicaciones resuelven localhost a IPv6, donde no escucha nadie. Lo confuso es que curl prueba las dos y funciona, así que compruebas a mano, te sale bien, y la aplicación sigue fallando.

Las instrucciones globales, que es donde se arregla el comportamiento

Este es el ajuste con mejor relación entre esfuerzo y resultado de todo el montaje, y no cuesta un euro. OpenCode inyecta en el prompt de sistema de todas las sesiones y proyectos lo que pongas en ~/.config/opencode/AGENTS.md. Un modelo local mediano se comporta bastante peor que uno de pago por defecto, y buena parte de esa diferencia se recupera diciéndole explícitamente lo que un modelo grande da por supuesto.

Estas son las tres reglas que más me han cambiado el día a día, y van las primeras del fichero, porque la posición pesa:

## Reglas que no se negocian

1. NO tienes memoria de sesiones anteriores. NUNCA digas que vas a
   revisar el historial ni que recuerdas lo que hicimos.
2. Anunciar es actuar: NUNCA escribas "voy a X" sin llamar a la
   herramienta en ese mismo turno.
3. Antes de terminar, actualiza docs/ESTADO.md con lo hecho, lo
   pendiente y el siguiente paso concreto.

Cada una ataca un fallo que había visto de verdad:

  • La primera, que se invente una capacidad que no tiene. Sin ella, ante un «sigamos por donde lo dejamos» me anunció que iba a revisar el historial de la sesión anterior, no llamó a ninguna herramienta, y al insistirle acabó preguntándome a mí qué estábamos haciendo.
  • La segunda ataca el turno en blanco: el modelo dice que va a hacer algo y no llama a la herramienta. Es el modo de fallo dominante en los modelos pequeños.
  • La tercera es la que le da memoria, y va en el apartado siguiente.

Un aviso para no esperar milagros: las instrucciones globales arreglan cómo trabaja, no que fabrique. Con todo a favor, el modelo me afirmó que PHP 8.4 depreca tres funciones que no depreca, y para sostenerlo se inventó una URL que lo confirmara. Sirve para generar la lista de cosas que comprobar, no la lista de cosas ciertas.

Memoria entre sesiones

El agente no recuerda nada de una sesión a otra. Se arregla con dos ficheros, y confundirlos es el error:

Fichero Qué guarda
AGENTS.md del proyecto Índice de conocimiento: qué ficheros hay, qué fuentes sirven, qué evitar. Va inyectado en el prompt de sistema.
docs/ESTADO.md Estado del trabajo: hecho, pendiente, siguiente paso concreto. Se lee al empezar y se actualiza antes de terminar.

Con solo el índice, el agente sabe qué hay pero no por dónde ibas, y te contesta con un menú de temas. Con la tercera regla de arriba escribiendo el ESTADO.md, retoma la tarea.

Con las tres reglas puestas, la primera tarea que completó de principio a fin sin que yo interviniera fue exactamente esa: leyó el estado, retomó lo pendiente y editó el fichero de verdad.

El buscador que no viene

Con un proveedor compatible con OpenAI apuntando a Ollama, OpenCode no expone búsqueda web. No hay interruptor. El modelo solo tiene la herramienta que descarga una URL exacta, así que cuando necesita buscar algo, se inventa la dirección: en mi caso adivinó el mismo dato mal cuatro veces en tres sesiones y acabó recomendándome instalar un paquete que da 404.

Lo suplí con un script en ~/bin/buscar que llama a una API de búsqueda por HTTP y le devuelve al modelo los resultados en texto plano. Pasé por tres opciones antes de quedarme con una:

Opción Cómo salió
Raspar un buscador Descartada. Funciona dos o tres veces y luego devuelve un captcha. Un fallo intermitente es peor que no tener la herramienta
API de búsqueda de Ollama, capa gratuita Descartada. No cuesta nada, pero los límites por hora y por semana son demasiado pequeños para trabajar
serper.dev La que uso. 2.500 búsquedas gratis, la más barata después, y claramente mejor en castellano

La capa gratuita de Ollama parece la opción evidente porque ya tienes la cuenta, pero se agota enseguida: tiene tope por hora y por semana, y una sola sesión de investigación con el agente se los come. Y en español devolvía resultados peores.

Con serper.dev los números salen, y por eso me quedé ahí: 2.500 búsquedas gratis al registrarte, sin tarjeta, que para probar si esto te sirve dan de sobra. A partir de ahí es prepago, no suscripción, desde 1 dólar por cada 1.000 búsquedas —unos 50 dólares por 50.000— y va bajando con el volumen hasta unos 0,30 por millar. Para un uso personal, la mayoría de los meses no pasas del regalo inicial.

Dos detalles prácticos antes de que hagas cuentas: los créditos caducan a los seis meses, y una búsqueda normal —hasta 10 resultados— gasta un crédito, pero si pides entre 11 y 100 resultados gasta dos. Es decir, pedir listas largas te dobla el precio real.

Precios comprobados en septiembre de 2026: las 2.500 gratis están en la web de Serper; los tramos por volumen salen de comparativas de terceros. Verifícalos antes de presupuestar nada.

Y el fondo del asunto es el mismo patrón que recorre toda la serie: lo que funciona en inglés no tiene por qué funcionar en español, y hay que probarlo. Un buscador que devuelve resultados mediocres en castellano te sale caro en tiempo del modelo, que cuando no encuentra se pone a inventar.

Dos avisos de montaje:

  • La clave, fuera del script. En un fichero de entorno con permisos 600, y nunca dentro del AGENTS.md, que se inyecta entero en el prompt.
  • El PATH del servicio no incluye ~/bin. Hay que añadirlo en la unidad de systemd o el comando no se encuentra.

Y ojo con la privacidad: las consultas salen hacia el proveedor de búsqueda. El contenido de tus ficheros no, pero lo que le preguntas sí.

Repartir la carga entre las dos tarjetas, según cuál respira

Este es el ajuste más específico de la máquina y el que no vas a encontrar en ningún benchmark, porque todos miden tarjetas sueltas.

Con dos tarjetas separadas por un solo hueco, la de arriba respira el aire caliente que expulsa la de abajo y apenas tiene sitio para coger aire fresco. Haciendo exactamente el mismo trabajo, esto es lo que midieron:

GPU 0 (arriba) GPU 1 (abajo)
Temperatura 76 → 84 °C 59 → 69 °C
Ventilador 50 → 96 % 0 → 43 %
Consumo medio 174,7 W 156,3 W
¿Se estabilizó? No, seguía subiendo Sí, plana

15 °C y más del doble de revoluciones de diferencia entre dos tarjetas idénticas haciendo lo mismo. Y el problema no era el calor —el límite de estas tarjetas es 87 °C y no hubo throttling— sino el ruido: para mantenerse a 84 °C, la de arriba tuvo que ponerse casi al máximo.

De ahí sale el criterio, que es lo contrario del reparto simétrico que hace todo el mundo por defecto: que trabaje más la que mejor se refrigera. La de abajo tiene todo el chasis para respirar y se queda plana; la de arriba se ahoga. Repartir a partes iguales significa que el conjunto va al ritmo de la que peor lo pasa, y que el ventilador que oyes es siempre el mismo.

En la práctica son dos palancas:

  • Límite de potencia asimétrico, para castigar a la que se ahoga y dejar respirar a la otra:
    sudo nvidia-smi -pm 1
    sudo nvidia-smi -i 0 -pl 150   # la de arriba, al mínimo que permite
    sudo nvidia-smi -i 1 -pl 180   # la de abajo, al máximo

    Antes de escribir números, mira el rango real con nvidia-smi -i 0 -q -d POWER: en mis tarjetas el suelo son 150 W y la BIOS no deja bajar más, así que el recorte máximo por esta vía es del 17 %.

  • Repartir el trabajo por tarjeta en vez de partir cada modelo entre las dos: el modelo pequeño y permanente vive en la que sufre, y el pesado se lo lleva la que respira. Se fija con CUDA_VISIBLE_DEVICES, levantando una instancia por tarjeta en puertos distintos si hace falta. Confiar en el planificador no basta: un modelo que casi llena una tarjeta lo reparte igualmente entre las dos.

Y una buena noticia que ahorra trabajo: el escenario que asusta no es el uso real. Todo lo de arriba es tortura sintética, las dos tarjetas al 93 % de forma sostenida. Trabajando de verdad con un agente —llega una petición, genera medio minuto y vuelve al reposo— la máquina se queda en 53 % de ventilador y 76 °C, y en reposo los ventiladores se paran del todo. Antes de rediseñar nada por el peor caso, mide el tuyo.

Comprobación de salud

ollama list
nvidia-smi --query-gpu=index,name,memory.used,power.limit --format=csv
systemctl status ollama --no-pager | head -5

En el arranque de Ollama deben aparecer las dos tarjetas, id=0 e id=1, con su VRAM. Si dice id=cpu con total_vram="0 B", el driver no ha inicializado y hace falta un ciclo de corriente completo: un reinicio en caliente no basta.

Para ver la carga en directo mientras trabaja:

watch -n1 --no-title 'nvidia-smi --query-gpu=index,utilization.gpu,temperature.gpu,fan.speed,power.draw,memory.used \
  --format=csv,noheader,nounits'

Los cinco fallos que no parecen fallos

Si algo va raro, empieza por aquí. Los cinco los he sufrido y ninguno da un mensaje de error:

Síntoma Causa real
Va a 2 tok/s y el servicio está «running» Ollama se cayó a CPU. Falta el ExecStartPre, o el driver no inicializó
El modelo responde peor que ayer, sin tocar nada El override.conf desapareció en una actualización
Un ajuste no hace efecto por mucho que lo pongas El cliente no lo entrega. Míralo en el cable, no en el fichero
El agente edita ficheros donde no debe Falta --dir y ha caído al proyecto global con raíz en /
Un modelo de visión arranca bien y está ciego Le falta el mmproj. No avisa: simplemente no ve

Y una advertencia de seguridad

Si expones Ollama a la red para llegar desde otro equipo, ten presente que no tiene ninguna autenticación y que su API permite borrar modelos. Cualquiera en tu red puede cargarse cien gigas de descargas.

En una red doméstica de confianza es asumible, y se acota con una regla de cortafuegos que solo deje pasar a tu propia subred. Lo que no hay que hacer nunca es abrirle el puerto en el router. Para llegar desde fuera de casa, o VPN, o un túnel SSH:

ssh -N -L 11434:localhost:11434 usuario@servidor

Lo que me llevo

Nada de este montaje es complicado. Lo que ha costado tiempo no es instalar Ollama: es que media docena de piezas fallan de forma silenciosa —el driver que no inicializa, el fichero que desaparece, el parámetro que no llega, el modelo ciego, la carpeta equivocada— y todas se manifiestan como «esto va un poco peor de lo que esperaba», que es justo el síntoma que uno atribuye al modelo.

Si te llevas una sola cosa de los tres artículos, que sea esta: en local, comprueba lo que está pasando de verdad antes de juzgar al modelo. La mayoría de las veces que un modelo local parece malo, lo que está mal es la instalación.

Es la misma idea de automatizar entendiendo primero y de por qué las soluciones sencillas funcionan mejor: la parte que se rompe casi nunca es la que estabas mirando.


Todo medido en una máquina concreta —dos RTX 5060 Ti de 16 GB sobre Ubuntu Server— entre agosto y septiembre de 2026. Las versiones de Ollama y OpenCode se mueven rápido: algunos de los fallos descritos aquí estarán arreglados cuando leas esto, y el proxy sobra en cuanto lo esté el del extraBody.