Saltar al contenido
Fantástico Mundo de Jon
RSS

Diseccionando un Modelfile de Ollama para que el modelo deje de improvisar en el código

Los patrones de Ollama sirven para la conversación. Para el código, el trabajo es reducir la aleatoriedad en el muestreo, escribir un SYSTEM conciso y guardarlo en un modelo derivado. Paso a paso con Qwen2.5-Coder.

¿Con prisa? Pide el TL;DR a Claude — lee la página y la resume.

es Traducción automática por mistral-small3.2:24b, revisada por el autor. Leer el original en portugués

Instalo el Qwen2.5-Coder, lo abro en Ollama y le pido una función. El modelo responde bien. Le pido de nuevo la misma función y viene otra firma, otro nombre de variable, a veces un comentario motivacional en medio. No es un error grave. Es sampling con cara de chat.

Los patrones de Ollama fueron pensados para la conversación. Temperatura lo suficientemente alta como para parecer suelto, penalización de repetición para no atascarse, contexto corto para caber en cualquier máquina. Para el código, casi todo lo que interesa es lo contrario: bajar la aleatoriedad y dejar de permitir que el modelo sea “creativo”.

En este post extraigo el Modelfile de un Qwen2.5-Coder ya instalado, disecciono parámetro por parámetro, monto un SYSTEM orientado al código, creo el modelo derivado y apunto el Aider hacia él.

Lo que ya viene en el modelo instalado

Parto de un Qwen2.5-Coder en GGUF a través de Ollama. Cualquier etiqueta sirve para este ejercicio; lo que importa es tener el modelo extraído antes. El comando que extrae la receta:

ollama show qwen2.5-coder:14b --modelfile > Modelfile

(sin salida en el terminal — el contenido fue al archivo Modelfile)

Extrae la receta del modelo instalado al directorio actual

El archivo que sale parece a esto (el FROM apunta al blob local en tu máquina; el hash cambia por cuantización y por etiqueta):

# Archivo de modelo generado por "ollama show"
# Para construir un nuevo archivo de modelo basado en este, reemplace FROM con:
# FROM qwen2.5-coder:14b

FROM /usr/share/ollama/.ollama/models/blobs/sha256-...
TEMPLATE """{{- if .System }}<|im_start|>system
{{ .System }}<|im_end|>
{{ end }}<|im_start|>user
{{ .Prompt }}<|im_end|>
<|im_start|>assistant
"""
PARAMETER stop "<|im_start|>"
PARAMETER stop "<|im_end|>"
PARAMETER temperature 0.7
SYSTEM """You are Qwen, created by Alibaba Cloud. You are a helpful assistant."""

Tres observaciones rápidas. El TEMPLATE es la plantilla de chat del Qwen (tokens im_start / im_end); tocar eso sin necesidad es la forma más corta de romper el modelo. Los stop evitan que la generación invada el turno siguiente. Y el SYSTEM de fábrica es genérico para asistente, no para código.

En varias etiquetas Ollama aún encaja otros PARAMETER encima de los patrones globales. Cuando no encaja, valen los patrones del runtime. Es esa capa la que quiero controlar a propósito, en lugar de heredar lo que sobró de un preset de conversación.

temperature: el botón que más afecta al código

temperature reescala los logits antes del softmax. En cero, el modelo siempre toma el token de mayor probabilidad. A medida que aumenta, la distribución se aplana y los tokens menos probables entran en juego.

El patrón de Ollama es 0.8. Observa que el Modelfile que extraje anteriormente ya trae temperature 0.7: el 0.8 es el patrón global del runtime, y esta etiqueta específica ya sobrescribe con 0.7. Las dos capas existen, y es la de arriba la que vale. En chat esto deja la prosa menos mecánica. En código, 0.8 es una invitación a variar el nombre de las variables en medio de una refactorización, invertir el orden de imports e inventar ramas que el enunciado no pidió.

Lo que cambia en el código generado: con temperatura alta ves diversidad entre un intento y otro. Con temperatura baja, el mismo prompt tiende a converger al mismo esqueleto. Para completar una función, generar pruebas o aplicar un diff pequeño, la convergencia es una característica.

Valor que uso para código: 0.1 en el uso diario, 0.0 cuando quiero un diff estable (parche, renombrar, repetir la misma refactorización). Rara vez paso de 0.2. Por encima de eso, en mi uso, la “creatividad” aparece como inconsistencia, no como solución mejor.

top_p: el corte por masa de probabilidad

top_p (nucleus sampling) restringe la elección a los tokens más probables hasta sumar la probabilidad p. El patrón de Ollama es 0.9: queda bastante cola.

En el código, una cola larga es donde viven identificadores casi correctos, palabras de comentario en tono de blog y, de vez en cuando, un operador cambiado. Bajar el top_p acorta esa cola.

Lo que cambia en la práctica: top_p 0.9 con temperatura 0.8 es el combo de conversación. top_p 0.9 con temperatura 0.1 ya mejora mucho, porque la temperatura domina. Por eso dejo el top_p en el patrón 0.9 incluso: con temperatura en 0.1 casi no tiene nada que cortar, y apilar dos filtros solo dificulta saber cuál de los dos causó qué. Si subo la temperatura a 0.2 en una tarea más abierta (boceto de una nueva API), entonces sí cierro top_p en 0.8.

top_k: el techo en conteo de tokens

top_k limita la muestra a los k tokens de mayor probabilidad. Patrón de Ollama: 40.

Es un corte bruto. A diferencia del top_p, no mira la masa acumulada: mira la cantidad. En una distribución bien concentrada (código idiomático después de un prompt claro), los primeros tokens ya llevan casi todo y el top_k casi no aparece. En una distribución más plana, impide que el token 200 de la fila entre por ruido.

Para código dejo 40 o bajo a 30. No creo que sea la palanca principal. Si la temperatura ya está en 0.1, jugar con top_k demasiado es microgestión. Uso top_k como red de seguridad, no como ajuste fino de estilo.

min_p: el suelo relativo que mucha gente ignora

min_p descarta tokens cuya probabilidad es menor que min_p veces la probabilidad del token más probable. Patrón en Ollama: 0.0 (desactivado).

La idea es elegante: si el mejor token tiene una prob de 0.6 y min_p es 0.05, caen todos por debajo de 0.03. En temperatura baja esto casi no cambia nada. En temperatura media, limpia la cola sin la rigidez del top_k.

Para código con temperatura 0.1, dejo 0.05 sin expectativa milagrosa — es cinturón y suspensorio. Si alguien insiste en ejecutar código con temperatura de chat (0.6–0.8), min_p en 0.05 o 0.1 ayuda más que rezar. Prefiero no estar en esa situación.

repeat_penalty: el arma de doble filo en el código

repeat_penalty > 1 penaliza tokens que ya han aparecido, empujando al modelo lejos de lo que acaba de decir. Patrón de Ollama: 1.1.

En prosa, 1.1 reduce bucles aburridos. En código, repetir no es un error: i, err, ctx, self, return, el mismo nombre de función tres veces en un archivo, el mismo tipo en cada firma. Penalizar la repetición empuja al modelo a sinónimos creativos en el peor lugar posible — identificador y palabra clave.

Lo que he visto en la práctica: con 1.1 o más, aumenta la probabilidad de cambiar un nombre estable por una variante (userId se convierte en user_id en medio del parche) y de escapar de una construcción legítima que solo tiene una forma idiomática. Para código uso 1.0 (efecto neutro) o como máximo 1.05 si algún modelo entra en un bucle raro de token. No uso 1.1 de conversación en un modelo de código.

num_predict: cuándo dejar de generar

num_predict es el techo de tokens en la salida. Patrón de Ollama: -1 (sin techo artificial más allá del contexto).

Para chat infinito tiene sentido. Para código, una salida sin techo en un prompt vago es monólogo: el modelo explica, vuelve a explicar, genera ejemplo, genera variante del ejemplo. En herramientas como Aider esto se convierte en ruido y a veces corta en medio de un archivo largo por exceder otro límite en el camino.

Yo uso 2048 o 4096 según la tarea. Completar función: 2048 resuelve la mayoría. Generar un archivo de prueba más grande: 4096. No pongo 128k de predicción solo porque el modelo lo acepta — predecible y corto comete menos errores que largo y divagante.

num_ctx: la ventana que el patrón esconde

num_ctx es el tamaño del contexto en tokens. El patrón global de Ollama es 2048, y este es el número que más sorprende a la gente. La ficha del modelo habla de 32k o 128k; el servidor se inicia con 2k si no pides otra cosa.

En código, 2048 desaparece rápidamente: prompt del sistema, archivo actual, fragmentos vecinos, mensaje del Aider, diff. Con 2k el modelo “olvida” la importación de la parte superior e inventa de nuevo. O peor: trunca en silencio y responde sobre un contexto incompleto.

Para código parto de 8192 en el día a día y subo a 16384 o 32768 cuando Aider envía multiarchivo de verdad. El techo depende de la VRAM y del caché KV, no del marketing del modelo. La cuenta de memoria queda para otro post; aquí solo el aviso: no confíes en el patrón 2048 para trabajar en un repositorio.

El SYSTEM que grabo en el modelo

Ningún parámetro sustituye a una instrucción clara. El SYSTEM de fábrica del Qwen2.5-Coder es para un asistente servicial. Lo cambio por un bloque seco, en inglés (los modelos de esta familia aún obedecen mejor las instrucciones del sistema en inglés que en portugués largo), centrado en lo que rompe el código en la práctica:

SYSTEM """You are a senior software engineer working in a local coding agent.
Prefer correct, minimal changes over rewrites.
Match the style, naming, and abstractions already present in the repository.
When editing, output only what was asked: no tutorial, no preamble, no recap.
If the request is to write code, write code. Put brief comments only where the intent is non-obvious.
Do not invent files, APIs, or config keys that were not given.
If requirements are ambiguous, state the assumption in one line and proceed.
Never wrap the whole answer in a markdown fence unless the user asked for a document.
Refuse to guess secrets, credentials, or private keys; use placeholders instead.
"""

Por qué cada línea está ahí:

  • senior… local coding agent: ancla el tono y el contexto de uso (Aider, editor, no chat de sofá).
  • minimal changes over rewrites: el vicio número uno es reescribir todo el archivo cuando pidieron un parche de seis líneas.
  • Match the style…: frena la voluntad de “mejorar” nombres y estándares en medio de una corrección.
  • output only what was asked: corta prefacios como “¡Claro! Aquí tienes una implementación robusta…”.
  • write code / comments only where…: comentarios útiles sí; narración línea por línea no.
  • Do not invent files, APIs…: alucinación de módulos vecinos es el modo de fallo más caro en agentes con repositorio real.
  • ambiguous → assumption in one line: prefiere progreso explícito a pregunta eterna, sin ocultar la suposición.
  • Never wrap the whole answer…: evita una valla gigante que dificulta la aplicación del diff.
  • Refuse to guess secrets…: cinturón de seguridad obvio, y el modelo local aún así intenta “completar” claves si lo dejas.

No pongo lista de lenguajes. No pongo “you are world-class”. No pongo regla que no quiero enforcement real. SYSTEM demasiado largo se convierte en ruido que el modelo comienza a ignorar en medio del contexto.

El Modelfile final y el create

Juntando todo en un archivo que parte de la etiqueta oficial (más portátil que la ruta del blob):

FROM qwen2.5-coder:14b

PARAMETER temperature 0.1
PARAMETER top_p 0.9
PARAMETER top_k 40
PARAMETER min_p 0.05
PARAMETER repeat_penalty 1.0
PARAMETER num_predict 4096
PARAMETER num_ctx 16384

SYSTEM """You are a senior software engineer working in a local coding agent.
Prefer correct, minimal changes over rewrites.
Match the style, naming, and abstractions already present in the repository.
When editing, output only what was asked: no tutorial, no preamble, no recap.
If the request is to write code, write code. Put brief comments only where the intent is non-obvious.
Do not invent files, APIs, or config keys that were not given.
If requirements are ambiguous, state the assumption in one line and proceed.
Never wrap the whole answer in a markdown fence unless the user asked for a document.
Refuse to guess secrets, credentials, or private keys; use placeholders instead.
"""

Creo el modelo derivado:

ollama create qwen-coder-dev -f Modelfile

transferring model data using existing layer sha256-… creating new layer sha256-… writing layer sha256-… success

Crear el modelo ajustado

Verifico si los parámetros se aplicaron:

ollama show qwen-coder-dev --modelfile

FROM qwen2.5-coder:14b TEMPLATE “”“…”“” PARAMETER temperature 0.1 PARAMETER top_p 0.9 PARAMETER top_k 40 PARAMETER min_p 0.05 PARAMETER repeat_penalty 1.0 PARAMETER num_predict 4096 PARAMETER num_ctx 16384 SYSTEM “”“You are a senior software engineer…”“”

Verificar lo que quedó grabado

El TEMPLATE y los stop heredados del modelo base siguen ahí. No los redeclaré a propósito: lo que no toco, no rompo. Al probar en el chat:

ollama run qwen-coder-dev

write a pure function in Go that returns the unique ints from a slice, preserving order

respuesta: función directa, sin prefacio y sin recapitulación.

Smoke test — la pregunta va al prompt interactivo de Ollama

Si aún viene demasiada prosa, el SYSTEM no se aplicó (etiqueta incorrecta, archivo incorrecto) o el cliente está enviando otro sistema por encima. Aider hace esto con frecuencia — es el siguiente paso.

Apuntar el Aider al modelo creado

Aider habla con Ollama a través de la API compatible con OpenAI. Con el modelo derivado en funcionamiento:

export OLLAMA_API_BASE=http://127.0.0.1:11434
aider --model ollama/qwen-coder-dev

En versiones recientes de Aider también aparece el prefijo ollama_chat/; usa lo que tu versión documente. El punto no es la bandera, sino no apuntar al qwen2.5-coder:14b crudo si acabas de gastar tiempo afinando el derivado.

Dos detalles que importan en el uso real:

  1. Aider envía sus propios system e historial. El SYSTEM del Modelfile sigue valiendo como capa base del modelo en Ollama, pero Aider apila instrucciones de editor encima. Si algo parece “sordo” a tu SYSTEM, mira lo que Aider está inyectando antes de subir la temperatura de nuevo.

  2. Contexto. El num_ctx 16384 del Modelfile define la ventana del modelo en Ollama. Si Aider envía más archivos de los que caben, la conversación se trunca. Es mejor ser selectivo con lo que añades en la sesión que inflar num_ctx hasta que la GPU llore.

Sobre el tamaño: 7B es utilizable para autocompletar y parches pequeños; 14B es mi punto estándar de trabajo en una máquina con margen; 32B mejora el razonamiento de refactorizaciones gruesas y cuesta VRAM de verdad. QwQ-32B y DeepSeek-R1, que llegaron en la ventana de 2025, son otra conversación — razonamiento largo, no completación seca de código. Para bucle con Aider me quedo con el Qwen2.5-Coder.

Receta grabada en qwen-coder-dev

Base
qwen2.5-coder:14b
temperature
0.1
top_p
0.9
top_k
40
min_p
0.05
repeat_penalty
1.0
num_predict
4096
num_ctx
16384

El conjunto que dejo grabado

El patrón de conversación de Ollama no es neutral: empuja al modelo a sonar suelto y a caber en una GPU pequeña. El código pide otra postura. Juntos:

Parámetro Padrão del Ollama Lo que uso para código
temperature 0.8 0.1
top_p 0.9 0.9
top_k 40 40
min_p 0.0 0.05
repeat_penalty 1.1 1.0
num_predict -1 4096
num_ctx 2048 16384
Columna del medio: valores padrão declarados en la documentación oficial del Modelfile de Ollama, verificados en el repositorio en el estado vigente en marzo de 2025. Columna de la derecha: lo que dejo grabado — juicio de uso, no medición. Las dos columnas no tienen el mismo peso de prueba.

Fuera de la tabla queda el SYSTEM: mínimo, en modo ingeniero, prohibiendo prefacio y API inventada.

No necesito un preset nuevo cada mañana. Necesito un modelo derivado con sampling aburrido, ventana honesta y sistema que no pida creatividad. El resto es el peso del Qwen2.5-Coder haciendo el trabajo silencioso que ya sabe hacer.

Graba el Modelfile, crea la etiqueta, apunta la herramienta hacia ella. Si el código sigue viniendo con trama, resiste la tentación de subir la temperatura: en la mayoría de los casos lo correcto es bajarla.