Saltar al contenido
Fantástico Mundo de Jon
RSS

Medí si el archivo de especificación hace que el modelo acierte más. No lo hace.

En julio defendí que la especificación estructurada cambia el resultado. Medí en octubre: 360 generaciones, 3 modelos locales, 3 formas del mismo pedido. Lo que paga es la información escrita, no el formato del archivo.

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

es Traducción automática por qwen3:32b, revisada por el autor. Leer el original en portugués

En 18 de 30 celdas, el brazo con spec estructurada en markdown y el brazo con exactamente la misma información en párrafo corrido produjeron el mismo resultado. En la tarea slugificar con el devstral:24b a temperatura 0, los dos volvieron con código byte a byte idéntico.

En julio escribí que cambiar el prompt en prosa por un archivo de spec — contrato en tabla, casos de borde, fuera de alcance, criterio de aceptación — hacía que el agente acertara más. Cerré aquel post admitiendo que el beneficio era mi hipótesis, con las celdas de la tabla marcadas para rellenar después.

Ahora están rellenas. Y derrumban la hipótesis.

Lo que cambia el resultado es la información estar escrita. Entre un pedido de una frase y la misma tarea especificada, los casos de borde saltan de 46% a 90%. Entre la spec estructurada y la misma información en texto corrido, la diferencia desaparece dentro del ruido.

Bancada · octubre de 2025

Placa
RTX 5090 · 32 GB
Modelos
qwen3-coder:30b · devstral:24b · qwen2.5-coder:32b
Quantización
Q4 via Ollama
Acceso
API cruda, un turno, sin historial
Tareas
10 funciones puras pequeñas en JS
Brazos
prosa · prosa larga · spec
Generaciones
360 (90 + 270)
VRAM ocupada
20 · 17 · 24 GB, en orden de los modelos
Tiempo de las 2 carreras
15,2 min sumados de llamada

El brazo del medio es el experimento

Comparar “una frase” con “spec de sesenta líneas” no responde nada. Más información ayuda — esto es obviedad, no medición. Por eso son tres brazos, y el del medio es el que da sentido al resto:

  • prosa — pedido de una frase, solo camino feliz, ningún caso de borde mencionado.
  • prosa larga — exactamente la misma información de la spec, en texto corrido, sin estructura.
  • spec — la misma información en markdown estructurado: contrato en tabla, casos de borde, fuera de alcance, criterio de aceptación.

Así prosa contra prosa larga mide información, y prosa larga contra spec mide forma — que es la afirmación que necesitaba sostener y no sostuve.

El resto del diseño existe para quitar las salidas fáciles. Las pruebas de cada tarea están separadas en camino feliz y casos de borde, y cada caso de borde corresponde 1:1 a un ítem que la spec declara y la prosa corta omite. La instrucción de formato de salida es idéntica en los tres brazos — si el formato pedido variara junto con el contenido, estaría midiendo formato. Cada tarea trae una implementación de referencia que debe pasar en 100% de sus propios tests antes de que la carrera comience; si la referencia falla, lo equivocado es el test. El código generado corre en proceso aislado con timeout de 10 s, porque el código de modelo no merece confianza: entra en bucle, llama a process.exit, estalla la memoria.

Todo el harness está en el repositorio: scripts/bancada-spec.mjs corre la carrera, scripts/bancada-spec/tarefas.mjs guarda las diez tareas con los tres prompts y los tests de cada una, scripts/bancada-spec/executor.mjs corre un candidato en proceso propio.

pnpm bancada --modelos qwen3-coder:30b,devstral:24b,qwen2.5-coder:32b --amostras 3 --temperatura 0.7

referencias validadas: 10 tareas, todas en 100% bancada: 3 modelos × 10 tareas × 3 brazos, temperatura 0.7

la carrera de variación; sin --amostras y --temperatura, corre determinística

--retomar aprovecha lo que ya corrió, porque 270 generaciones no caben en una sesión sin que algo caiga en medio.

Carrera 1: temperatura 0, 90 generaciones

Un pedido por celda, sin aleatoriedad. Es la carrera que responde “con el mismo prompt, qué sale”.

Brazo Camino feliz Casos de borde % borde Tareas 100%
prosa 78/116 93/200 47% 0/30
prosa larga 116/120 185/207 89% 18/30
spec 110/120 183/207 88% 14/30
RTX 5090 · 3 modelos locales en Q4 · temperatura 0 · una generación por celda · 90 generaciones. Tests en proceso aislado, timeout de 10 s. Los totales de test difieren entre brazos porque generación que no devuelve módulo válido no contabiliza test ninguno — y esto pasó más en la prosa corta.

La primera columna es la que yo esperaba y no es noticia: el camino feliz el modelo acierta casi siempre, en cualquier brazo. Todo el mundo entiende “formata un número en reales”.

La columna de borde es donde la información aparece — 47% contra 89%. Es el mismo fenómeno del post de julio, ahora con número: el modelo no erra por torpeza, erra porque cerró solo la brecha que el pedido dejó.

Y la línea de abajo. La spec estructurada quedó un punto abajo de la misma información en párrafo. No es victoria de nadie.

Carrera 2: 270 generaciones a temperatura 0,7

Una carrera determinística no distingue diferencia real de suerte. Repetí tres veces a 0,7, que es donde estas herramientas suelen correr de hecho.

Brazo Casos de borde % borde Por repetición
prosa 277/607 46% 46%, 48%, 43%
prosa larga 554/614 90% 88,0%, 91,3%, 91,3%
spec 538/621 87% 87,4%, 86,5%, 86,0%
RTX 5090 · 3 modelos locales en Q4 · temperatura 0,7 · 3 repeticiones · 270 generaciones. La columna de la derecha es la tasa de cada repetición aislada, para dar idea de la dispersión.

La dispersión importa más que la media. La prosa corta oscila 5 puntos entre repeticiones; la spec, poco más de 1. Las tres repeticiones de la spec quedaron abajo de la peor repetición de la prosa larga, lo que quiere decir que los 3,6 puntos de diferencia no son un artefacto de una ronda mala.

Por modelo, con la misma carrera:

Modelo prosa prosa larga spec
qwen3-coder:30b 48% 95% 95%
devstral:24b 39% 92% 85%
qwen2.5-coder:32b 49% 84% 81%
Porcentaje de casos de borde por modelo en la carrera de temperatura 0,7. El devstral:24b tuvo dos generaciones sin módulo válido en el brazo de prosa, así que allí la base es 193 tests en vez de 207.

El modelo más fuerte empató los dos brazos en 95%. En los otros dos, la prosa larga ganó 7 y por 3 puntos. Ningún modelo puso la spec adelante.

El dato que decide la interpretación

El promedio oculta lo que pasó. En la carrera determinística, celda por celda — 3 modelos × 10 tareas —, spec y prosa larga dieron resultado idéntico en 18 de las 30 celdas. En las 12 restantes, la prosa larga ganó 8 y la spec ganó 4.

Fui a ver una de las idénticas a mano. Tarea slugificar, devstral:24b, temperatura 0: los dos brazos produjeron código byte a byte idéntico. Dos prompts de tamaño y diagramación completamente diferentes, con el mismo contenido, colapsando en la misma salida.

formatarBRL: cero en 11 tests, en los tres modelos, por un carácter

El hallazgo más interesante de la carrera es cualitativo y no aparece en ninguna media.

En la tarea formatarBRL, el brazo prosa sacó 0 de 11 en los tres modelos. Cero absoluto, incluso en el camino feliz. Causa única, idéntica en los tres:

Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(1234.56);
// 'R$ 1.234,56' — visualmente correcto, y falla en cualquier comparación de cadena

// el carácter entre "R$" y "1" no es espacio
// código 160 (U+00A0, espacio no interrumpible), no 32

El modelo escribió la solución idiomática, la que cualquier revisor humano aprobaría en una pasada. Y falla en assert.equal contra 'R$ 1.234,56' con espacio común, porque Intl devuelve espacio inquebrable.

Con la información escrita — “prefijo R$ seguido de un único espacio común, no usar espacio no separable” —, dos de los tres modelos pasan a tratar esto: usan Intl y sustituyen el carácter, o formatean a mano. El qwen2.5-coder:32b sigue llamando a Intl sin tratamiento y falla en los tres brazos, incluso con la spec delante.

Este es exactamente el error plausible del post de julio, al nivel del carácter. No es código absurdo, que yo cogería en diez segundos. Es la línea que todo el mundo escribiría, decidiendo en silencio una cosa que el pedido no decidió — y que solo aparece cuando el valor va para un CSV, o para una comparación de string en otro sistema.

Lo que cuesta la spec

En la tarea truncar, el prompt en prosa corta gastó 94 tokens de entrada. La misma tarea en spec gastó 556 — cerca de 6× — para una diferencia de acierto que, contra la prosa larga, no apareció.

Con modelo local, esos 462 tokens más son tiempo de prefill: 4 ms más por generación en esta placa (mediana de 141 ms contra 145 ms, cinco mediciones de cada). Con modelo de API, son cuenta al final del mes, multiplicada por toda llamada en que la spec entra en el contexto.

El costo es el mismo en los dos brazos informados, porque la información es la misma. Lo que la medición dice es que compra el salto de 46% a 90% — y no compra nada más por el hecho de estar en tabla.

Aider, Ollama y la trampa que invalida la medición

En la práctica no converso con el modelo por fetch. Dirijo el modelo local por Aider:

# directamente en Ollama
aider --model ollama_chat/qwen2.5-coder:32b --read docs/specs/truncar.md src/texto.js

# o por un gateway compatible con OpenAI
export OPENAI_API_BASE=http://localhost:4000/v1
aider --model openai/qwen2.5-coder:32b --read docs/specs/truncar.md src/texto.js

El --read sigue siendo el detalle que más importa: carga la spec en el contexto sin hacerla editable. Agente que puede editar la spec “resuelve” divergencia reescribiendo la referencia, que es el peor desenlace posible.

Y es por eso que esta bancada no midió a través del Aider. Aider añade repo map, formato de edición y retentativa. Midiendo por él, el número sería de Aider, no del modelo. La bancada habla con el modelo por API cruda, un turno, sin historial — ella responde “el modelo entendió el requisito?”, no “la herramienta terminó la tarea?”. Son preguntas diferentes y la segunda no aísla nada.

Donde los modelos pequeños quebran de verdad en 2025, en uso diario, no es escribir la función: es aplicar el patch en el formato de búsqueda-y-sustitución que Aider espera. El bloque vuelve con la indentación cambiada, o con un trozo de contexto que no bate byte a byte con el archivo, y la edición falla. El código estaba correcto. Esto no aparece en tasa de acierto de función ninguna — y es la mayor parte del roce real.

Límites del experimento

  • Diez funciones puras pequeñas no son tarea de repositorio real. No hay dependencia entre archivos, convención del proyecto, código legado para respetar. La sección “fuera de alcance” de la spec, que llamé en julio la que más paga, casi no tiene qué hacer aquí — no existe vecino para que el modelo refactorice de paso. Es plausible que la estructura pague justamente donde este experimento no mira.
  • Un turno solo, sin bucle de agente. Sin test corriendo, sin error volviendo, sin corrección. Buena parte del valor de un criterio de aceptación ejecutable está exactamente en el bucle que esta bancada no tiene.
  • Modelos locales de 24 a 32B. Nada dice que el resultado se transporte a modelo grande de API, ni a modelo menor.
  • Comparación de string es frágil, y la formatarBRL lo prueba. Un carácter invisible zereó una tarea entera. Donde mi test es demasiado rígido, repruebo código que serviría; donde es flojo, apruebo código que no sirve. Los 3,6 puntos entre spec y prosa larga son pequeños bastante para caber en esta fragilidad.
  • Tres repeticiones dan idea de dispersión, no intervalo de confianza. No hice test estadístico.

Lo que cambié de opinión

Escribí en julio que la spec hacía que el agente acertara más. Está equivocado, de la manera que escribí. Lo que hace que el modelo acierte más es que yo haya decidido y escrito qué pasa cuando el valor es negativo, cuando la entrada es null, cuando la página pasa del final. La spec es el lugar donde suelo hacer esto — no es el mecanismo.

Lo que sigue de pie del post anterior: la sección de casos de borde es lo que carga el resultado. Allí es donde aparecieron los 44 puntos. Lo que cae: la idea de que la tabla, los encabezados y el markdown hacen diferencia para el modelo. Para el modelo, por lo que medí, no la hacen.

Entonces, ¿para qué sirve el archivo, si no es acierto por generación? Esto es juicio, no medición — no probé nada de lo que viene abajo:

  • Versionamiento. Spec en el prompt muere con la sesión. En el repositorio, entra en el diff y alguien revisa la decisión antes de que se convierta en comportamiento.
  • Revisión contra referencia. La pregunta en la revisión deja de ser “esto parece correcto?” y se convierte en “esto bate con el archivo al lado?”. La segunda tiene respuesta.
  • Reuso entre herramientas. El mismo archivo sirve a Aider, Claude Code, Cursor y a mí leyendo. Prosa pegada en un chat no sirve ni la segunda vez.
  • Sobrevivencia. El modelo cambia cada pocos meses. El archivo que dice qué hace el sistema sigue valiendo después del cambio; el prompt afinado para un modelo específico, no.

La estructura sirve para , y para la próxima persona que abra el archivo. Esto me parece suficiente para seguir escribiendo spec. Solo no es lo que dije que era.

Si vas a repetir: pnpm bancada, con --modelos, --amostras, --temperatura y --retomar. Las tareas están en scripts/bancada-spec/tarefas.mjs, con los tres prompts lado a lado — la parte que más vale la pena revisar antes de creer en cualquier número de este post.