Todos los artículos
16 min de lectura

Hice fine-tuning de un modelo de 9B hasta convertirlo en especialista en Godot 4, y el juez era el propio motor

Cómo Estragon-9B pasó del 38 % al 83 % en un benchmark de GDScript verificado por el motor, por unos 150 $: el dataset, los ciclos de RL que fallaron, el que funcionó y números honestos de cada quant.

AIGodotSide ProjectsOpen Source

Pídele código de Godot a un LLM local pequeño y mira lo que devuelve: yield en vez de await, export var en vez de @export, señales conectadas con strings y una clase File que lleva años sin existir. Compila en la imaginación del modelo y en ningún sitio más. Cuando lo medí en serio, el Qwen3-8B de serie producía código de Godot 4 que parseaba en 1 de 10 tareas básicas. Una.

Los modelos no son tontos: sus datos de entrenamiento están contaminados. Godot 4.0 rompió la compatibilidad con una década de tutoriales, respuestas de Stack Overflow y repositorios open source, y ese corpus anterior a 4.0 pesa más que todo lo escrito después. El modelo ha visto muchísimo más Godot 3 que Godot 4, así que eso es lo que escribe. Con total confianza, además.

Ese diagnóstico importa, porque significa que esto es un problema de distribución, no de capacidad, y desplazar una distribución es justo lo que el fine-tuning hace bien. Así que me pasé un verano de tardes averiguando hasta dónde podía llegar una sola persona con una GPU de gama media y unos 150 $.

El resultado es Estragon-9B: un fine-tuning de Qwen3.5-9B que saca 248/300 (82,7 %) en un benchmark de 300 tareas de GDScript donde cada respuesta la juzga un binario headless de Godot de verdad. El modelo base con el que empecé el proyecto saca 115/300 en las mismas tareas. Los pesos, el benchmark, el pipeline de entrenamiento y todos los registros de decisiones, experimentos fallidos incluidos, son públicos.

Sobre el nombre: Godot (el motor) se llama así por Esperando a Godot de Beckett, como broma de que el motor no se terminaría nunca. Estragon es uno de los dos vagabundos que esperan a un Godot que jamás llega. Este modelo es la referencia a la referencia, y la gracia está en la inversión: Estragon dejó de esperar.

La decisión que sostuvo todo el proyecto

Antes de tocar una línea de entrenamiento, monté el juez: un binario headless de Godot 4.7, con versión fijada, que parsea (y en las tareas difíciles, ejecuta de verdad) cada trozo de GDScript del proyecto. Nada de un LLM corrigiendo los deberes de otro LLM. El motor mismo.

Todo lo que vino después se apoya en él:

  • Ningún ejemplo entra al dataset sin parsear en Godot headless. Los ~20.000 pares, sin excepciones, incluidos los escritos a mano.
  • El benchmark lo verifica el motor. Una tarea solo cuenta si el archivo generado parsea y sus aserciones en runtime se cumplen: una escena de prueba instancia el nodo, llama a los métodos, comprueba el estado e informa.
  • El aprendizaje por refuerzo tuvo una señal de recompensa a la que no se puede engatusar. Más abajo entro en eso.

Si te llevas una sola idea de este artículo, que sea esta: si tu dominio tiene un compilador, un intérprete o un motor con CLI, tienes un verificador. Y un verificador convierte “parece que el modelo va mejor” en un número que puedes defender. Además mantuvo el proyecto honesto de una segunda forma que al principio no valoré: nunca tuve que fiarme del código generado ni de los datos sintéticos. Solo del veredicto del motor sobre ellos.

Unos 20.000 pares validados, cinco fuentes

Todo apunta a Godot 4.7, fijado al binario estable exacto para que “correcto” tenga un significado fijo. La mezcla:

Fuente Peso Qué es
Documentación ~30 % El XML de referencia de clases oficial, convertido en preguntas y respuestas sobre métodos, señales y propiedades
Código real ~20 % 38 repos de Godot 4 con licencia MIT/Apache, convertidos en tareas de explicar, implementar y rellenar huecos
Sintético ~30 % Tareas generadas con Claude, ancladas en fragmentos de docs o código real, y cada salida pasada por el juez
Migración ~8 % Código válido de Godot 4 corrompido mecánicamente a modismos de Godot 3, entrenando corrupto→arreglado
General ~15 % Una porción de dolly-15k para que el modelo no olvide ser un asistente

La fuente de migración es mi favorita, y salió gratis. Escribí un corruptor que reescribe código correcto de Godot 4 hacia atrás, a modismos de Godot 3 (await pasa a yield, @export a export var, las conexiones con Callable a conexiones con strings), y el modelo entrena arreglándolo. Ataca directamente el fallo principal, y las “respuestas” son código real ya validado.

Una regla que puse pronto y que me alegro de haber mantenido: las tareas del benchmark son sagradas. Nada del benchmark puede aparecer en los datos de entrenamiento, en los prompts de generación, ni siquiera pegado en un contexto de generación de datos. La descontaminación corre de forma mecánica (solape de texto a nivel de shingles) en cada ensamblado del dataset, y pilló solapes reales más de una vez: 140 pares fuera la primera vez que corrió con dientes.

El entrenamiento de 2,60 $

Mi GPU es una RTX 3060 Ti con 8 GB de VRAM, la misma tarjeta en la que corro agentes locales. Medí el entrenamiento QLoRA en local con honestidad: 766 segundos por paso. Posible en teoría, absurdo en la práctica. Una RTX 4090 alquilada hizo el primer entrenamiento completo en unas 3,5 horas por 2,60 $.

Eso marcó el patrón de todo el proyecto: mi máquina crea datos y valida todo; los pods alquilados entrenan y evalúan; cada pod se borra el mismo día. Gasto total en GPU contando todos los experimentos, los fallidos incluidos: unos 50 $.

El primer fine-tuning llevó la prueba rápida de 10 tareas de 1/10 a 8/10, con cero modismos de Godot 3. En el primer benchmark serio de 100 tareas: base 39/100, fine-tuning 74/100. (Claude Opus sacó 100/100 en el mismo benchmark, un techo útil para no olvidar lo que es un modelo de 9B.)

Un benchmark que no pueda mentirme

El benchmark de 100 tareas tenía un problema que solo entendí tras quemar un ciclo entero con él: con n=100, el intervalo de confianza al 95 % es de unas ±9 tareas. Estaba intentando dirigir el entrenamiento con una brújula que oscila nueve grados en reposo. Un ciclo completo de iteración de datos dio “71 contra 74”: un empate estadístico, sin forma de saber si algo había mejorado.

Así que primero la medición. El benchmark creció hasta 300 tareas en 10 categorías (señales, tweens, ficheros, físicas, ciclo de vida de nodos…), cada una con solución de referencia y autotest, cada una juzgada por el motor. Eso baja el ruido lo bastante para leer una diferencia de 5 tareas. A partir de ahí, cada experimento corrió contra una puerta de promoción fija: superar al campeón por ≥5 tareas en la misma GPU alquilada, el mismo día, más comprobaciones de comportamiento. Si no, no se publica.

Dos lecciones de medición que me costaron dinero real:

  • Repite tu línea base en el mismo hardware y el mismo día. Con otra GPU, otra versión de librería u otro día, el mismo adaptador se movía unas cuantas tareas. La puerta compara contra una base recién medida, no contra un número recordado.
  • El system prompt es parte del modelo. Las ablaciones enseñaron que el prompt de despliegue vale unas +15 tareas y, lo bonito, el modelo base puntúa peor con ese mismo prompt que sin él. Es el instruction tuning lo que hace que el prompt rinda. Así que el prompt se publica con los pesos y todos los números lo incluyen.

El capítulo donde todo falla

Esta es la parte que casi todos los anuncios de modelos se saltan, y la que más quiero dejar por escrito. Cuando el fine-tuning supervisado se estancó, lancé cuatro ciclos de entrenamiento contra esa puerta de promoción. Fallaron tres.

DPO, ciclo 1: fallo, y gordo. Direct Preference Optimization: generas varios candidatos por prompt, el juez los ordena y entrenas con las preferencias. Resultado: 222/300, contra su propio punto de partida en 229. El modelo empeoró. La autopsia encontró un sesgo de longitud escondido en mis reglas de ordenación: el modelo aprendió a comprimir el código hasta romper la corrección.

DPO, ciclo 2: fallo, distinto. Quité la presión de longitud y anclé las preferencias en 154 tareas nuevas verificadas en runtime. Resultado: +1. Empate estadístico, y encima volvió el ruido de estilo que la ordenación debía suprimir. Dos fallos con dos causas distintas. Cerré la vía DPO y dejé escrito el porqué.

GRPO, ciclo 1: fallo por exactamente una tarea. GRPO es aprendizaje por refuerzo con el juez como función de recompensa: el modelo genera, el motor puntúa, aprobado = recompensa. La curva de aprendizaje sobre el pool era preciosa (tasa de aprobados del 51 % al 68 % durante el entrenamiento), pero en el benchmark reservado se quedó en +4 contra una puerta de +5. La autopsia encontró un hueco de transferencia: el modelo mejoró de verdad en las tareas de ficheros con las que entrenaba mientras empeoraba en ficheros dentro del benchmark. Una familia de nueve prompts no puede enseñar un tema. Apuntado.

GRPO, ciclo 2: aprobado, y con claridad. Amplié el pool de entrenamiento con 100 tareas nuevas apuntadas exactamente a lo que la autopsia decía que faltaba. Resultado: +13 (240 contra 227), la primera diferencia estadísticamente significativa de toda la fase (McNemar p=0,019), con la regresión de ficheros curada.

La metalección está por encima de cualquier técnica concreta: la mayoría de los ciclos fallan, y la puerta es el producto. Un umbral fijo, medido en el mismo pod contra una base repetida, es lo que impidió que tres fallos seguidos se publicaran con cerebro de deadline (“seguro que está bien, la curva pintaba genial”). Los ciclos fallidos tienen sus propios post-mortems en los registros de decisiones del repo, porque en las autopsias es donde estuvo el aprendizaje de verdad.

El reward hacking existe, y es creativo

Entrenar contra un juez implica que el modelo acabará intentando hacerle trampas. Mis defensas: un conjunto canario de tareas ya resueltas (si retroceden, el modelo está jugando con algo), una métrica de ruido de estilo y detectores de degeneración. A lo largo del proyecto cazaron cuatro variantes distintas de salida degenerada, incluida una tardía que ciclaba declaraciones de variables con una forma diseñada, en la práctica, para esquivar los dos primeros detectores que escribí. Nada consiguió colarse por una puerta, pero solo porque había algo vigilando. Si haces RL contra un verificador, presupuesta esto.

Cambiar de modelo base a mitad de proyecto

A mitad de camino salió Qwen3.5-9B y sacó 219/300 en mi benchmark tal cual, de serie: a tiro de piedra de mi mejor fine-tuning de entonces. El coste hundido decía “sigue con el linaje viejo”; los números decían “repite la receta entera sobre la base nueva”. Ganaron los números:

Modelo gdeval_v2 %
Qwen3-8B (base original) 115/300 38,3 %
granite-4.1-8b 123/300 41,0 %
phi-4 (14B) 146/300 48,7 %
gemma-4-12B-it 211/300 70,3 %
Qwen3.5-9B (base nueva) 219/300 73,0 %
Estragon-9B (SFT + GRPO encima) 248/300 82,7 %
Claude Opus 4.8 (el techo) 300/300 100 %

Las filas de la comparativa fueron un añadido tardío para la tabla de la release, y me sorprendieron: phi-4 con 14B y gemma-4 con 12B puntúan por debajo de la base de 9B en crudo que yo afiné. Si hoy quieres GDScript con un modelo pequeño, el linaje Qwen3.5 es sencillamente donde hay que estar.

El SFT solo, sobre la base nueva, empató con mi campeón anterior. SFT más un ciclo de GRPO (el run 007 del proyecto, el que ahora lleva el nombre de Estragon) pasó la puerta con +8.

Saber cuándo parar

Probé un ciclo más de GRPO encima. Devolvió exactamente +0. Cinco tareas arregladas, cinco rotas, simetría perfecta, p=1,0.

Los diagnósticos decían que el margen restante es real pero está fuera del alcance de este método: con 8 intentos de sampling el modelo resuelve 285/300 (el conocimiento está en los pesos), pero mi pool de tareas de entrenamiento ya no podía convertir suerte de sampling en fiabilidad determinista. Es una señal de parada limpia y, por una vez en la vida, la escuché. El marco honesto: los últimos 52 fallos se concentran en precisión de APIs raras, cadenas largas de especificación y casos límite de timers y tweens, y para sacarlos hacen falta ideas nuevas, no más cómputo por la misma tubería.

Publicar con honestidad: cada quant medido

Dos cosas de la release que no veo hacer lo suficiente:

Cada cuantización pasó el benchmark completo de 300 tareas. Nada de “la pérdida por cuantización suele ser mínima”. Números:

Archivo Tamaño Puntuación vs bf16
Q5_K_M 6,5 GB 249/300 +1: estadísticamente idéntico. El que hay que bajarse.
Q8_0 9,5 GB 248/300 ±0
Q4_K_M 5,6 GB 229/300 −19: cabe en una tarjeta de 8 GB, y ahora ese precio es un número

Y una prueba de humo sobre los artefactos reales cazó dos bugs de release. Pasar el GGUF publicado por Ollama en mi propia máquina, el camino exacto de descargar-y-ejecutar que seguiría un usuario, sacó a la luz un error de parseo en el Modelfile (un espacio que faltaba) y otro más sutil: el runtime de Qwen3.5 de Ollama gestiona los bloques de razonamiento por su cuenta, y Estragon es un fine-tuning sin razonamiento. El flag importa, y de forma medible (−15 tareas con el thinking activado):

ollama run hf.co/Blugart/estragon-9b-gguf:Q5_K_M --think=false \
  "A 2D platformer jump with coyote time, CharacterBody2D"

Prueba lo que de verdad le dices a la gente que se descargue, en el hardware que dices que lo ejecuta. Los dos bugs costaron una hora de encontrar y habrían sido la primera experiencia de cada usuario.

Lo que costó

Qué Coste
API de Claude (datos sintéticos, casi todo en batch) ~100 $
GPUs alquiladas (todos los runs, todos los experimentos, la exportación) ~50 $
Mi propio hardware la 3060 Ti que ya tenía
Tiempo ~9 días de tardes, más este artículo

Cómo se hizo esto

Quiero ser preciso aquí, porque “asistido por IA” puede significar cualquier cosa. Usé Claude durante todo el proyecto: generó los datos sintéticos de entrenamiento (anclados en docs y código real), ayudó a crear tareas del benchmark, escribió conmigo la mayor parte del código del pipeline y coescribió los registros de decisiones. Yo tomé las decisiones, puse las puertas, pagué las facturas y aprendí cada pieza revisando lo que se construía. Era mi primer proyecto de fine-tuning, y usar IA para ayudar a construir un especialista de IA era la mitad del experimento.

Lo que me deja tranquilo publicando los números de todos modos es la arquitectura: el verificador no es una IA. Cada ejemplo de entrenamiento parseó en un binario real de Godot. Cada puntuación del benchmark es un recuento de programas que se ejecutaron correctamente en el motor, sobre tareas reservadas y descontaminadas mecánicamente del conjunto de entrenamiento. Lo único de este proyecto que nunca se generó es el veredicto del juez, y cada afirmación de arriba se remonta a uno.

Limitaciones, para que no las descubras tú

  • Solo Godot 4.7. No va a escribir Godot 3, y puede que te “arregle” código válido de Godot 3 por principios. Sin probar contra futuras versiones 4.x.
  • Solo GDScript. Ni C#, ni shaders más allá de lo trivial.
  • Es un especialista de 9B, no un asistente general. Los modelos frontera siguen siendo sencillamente mejores: Opus saca 300/300 en mi propio benchmark. Si tienes acceso a API y sin restricciones de privacidad, usa uno. Estragon es para la gente de local, offline y 8 GB de VRAM.

Lo que te diría que robes

  1. Busca el verificador de tu dominio. Compilador, intérprete, CLI del motor, validador de esquemas: cualquier cosa que convierta “parece correcto” en aprobado o suspenso. Vale más que una GPU más grande.
  2. Arregla tu medición antes de optimizar. Un suelo de ruido de ±9 se comió un ciclo entero. Dimensiona el benchmark para las diferencias que necesitas leer.
  3. Fija la puerta de promoción antes del run y deja que mate tu trabajo. La mía mató tres de cinco ciclos. Los tres se lo merecían.
  4. Haz la autopsia de los fallos por escrito. El ciclo que por fin funcionó lo apuntó el post-mortem del que falló por una tarea.
  5. Haz una prueba de humo del artefacto publicado en el hardware que nombras en el README.

Estragon dejó de esperar. Tú también puedes: el modelo · el benchmark y todo lo demás

Ignacio María Muñoz Márquez

Ignacio María Muñoz Márquez

Senior Game Programmer

Artículos relacionados