Saltar al contenido
Fantástico Mundo de Jon
RSS

Escribir el caso de borde aumenta la precisión del 46% al 90%

360 generaciones, tres modelos locales, tres formas de la misma solicitud. El gran salto está entre no escribir el caso de borde y escribirlo: del 46% al 90%. La tabla en markdown, frente al mismo texto corrido, no dio lo que esperaba.

¿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

En julio defendí que cambiar el prompt en prosa por un archivo de spec hacía que el agente acertara más. La publicación está aquí: contrato en tabla, casos de borde, fuera de alcance, criterio de aceptación y una tabla de resultados con las celdas vacías y la promesa de rellenar después.

En octubre completé el formulario. La tesis de julio, tal como yo lo había escrito, no resistió. La estructura del archivo no fue lo que movió la aguja.

Lo que movió fue otra cosa, y eso quedó más fuerte que la tesis original. Entre una solicitud de una frase y la misma tarea con los casos de borde escritos en algún lugar, el acierto en borde fue del 46% al 90%. Casi el doble. Entre la spec organizada en markdown y el mismo contenido lanzado en un párrafo corrido, la diferencia fue de 3,6 puntos — pequeña, estable entre repeticiones y a favor del texto corrido.

O sea: el formato en que organizas la solicitud importa poco para el modelo. Haber decidido y escrito lo que ocurre en el caso extraño importa mucho. Es eso lo que llevo de este experimento, y es eso lo que cambia lo que hago mañana por la mañana cuando abro Aider.

El tamaño del tropiezo de la tesis estructural, para ser específico: en la carrera sin aleatoriedad, el brazo con la spec y el brazo con exactamente la misma información en párrafo dieron resultados idénticos en 18 de las 30 celdas. En la tarea slugificar, con devstral:24b a temperatura 0, los dos volvieron con el mismo código. No era parecido: era byte por byte el mismo archivo.

Bancada · octubre de 2025

Placa
RTX 5090 · 32 GB
Modelos
qwen3-coder:30b · devstral:24b · qwen2.5-coder:32b
Cuantizació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 el orden de los modelos
Tiempo de las 2 carreras
15,2 min sumados de llamada

Por qué son tres brazos y no dos

Comparar “una frase” con “spec de sesenta líneas” no responde nada. Claro que más información ayuda. Eso es obviedad con cara de resultado, y era el error que más quería evitar.

Si solo comparaba prompt corto con spec, no sabría separar dos cosas diferentes: el hecho de que la información exista y el hecho de que esté en tabla con encabezado. La primera es contenido. La segunda es forma. En julio estaba vendiendo lo segundo. Necesitaba medir las dos.

Por eso la bancada tiene tres brazos, y el del medio es el que da sentido a los otros dos:

  • prosa — solicitud 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, con los cuatro bloques de siempre.

Así prosa contra prosa larga mide información, y prosa larga contra spec mide forma. La segunda comparación era la tesis de julio. No resistió. La primera es el hallazgo que quedó, y es lo que justifica el resto de este post.

El resto del diseño existe para cerrar 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 a un elemento que la spec declara y la prosa corta omite, uno por uno. La instrucción de formato de salida es igual en los tres brazos, de lo contrario estaría midiendo formato junto con contenido. Cada tarea trae una implementación de referencia que necesita pasar el 100% de sus propias pruebas antes de que comience la carrera: si la referencia falla, lo incorrecto es la prueba, no el modelo. Y el código generado se ejecuta en un proceso aislado con tiempo límite de 10 s, porque el código del modelo no merece confianza. Puede entrar en bucle, llamar a process.exit, agotar la memoria.

El arnés completo está en el repositorio. scripts/bancada-spec.mjs ejecuta la carrera, scripts/bancada-spec/tarefas.mjs guarda las diez tareas con los tres prompts y las pruebas de cada una, y scripts/bancada-spec/executor.mjs ejecuta un candidato en su propio proceso.

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

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

la carrera de varianza; sin --amostras y --temperatura, se ejecuta determinísticamente

El --retomar aprovecha lo que ya ha ejecutado. Lo necesitarás porque 270 generaciones no caben en una sesión sin que algo se caiga en el medio.

Primera carrera: temperatura 0, 90 generaciones

Una solicitud por celda, sin aleatoriedad alguna. Es la carrera que responde “con este prompt exacto, ¿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. Pruebas en proceso aislado, tiempo límite de 10 s. Los totales de prueba difieren entre brazos porque la generación que no devuelve un módulo válido no cuenta ninguna prueba — y esto sucedió más en la prosa corta.

La primera columna es la que ya esperaba, y no es noticia: el camino feliz el modelo acierta casi siempre, en cualquier brazo. Todos entienden “formatea un número en reales”.

La columna de borde es donde la historia ocurre de verdad. 47% contra 89%. Es el mismo fenómeno que describí en julio, ahora con número encima. El modelo no se equivoca por estupidez. Se equivoca porque cerró solo la laguna que dejó abierta la solicitud. Cuando nadie dice qué hacer con null, con negativo, con string vacía, inventa una respuesta plausible — y plausible reproba en la prueba.

Y ahí viene la tercera línea, que es donde mi tesis de julio muere sin drama. La spec estructurada quedó un punto por debajo de la misma información lanzada en párrafo. Un punto de diferencia no lo reclamo. Pero necesitaba que la spec estuviera adelante, y no lo estuvo.

Lee de nuevo la comparación que importa: prosa corta contra cualquiera de los dos brazos informados. Casi el doble de acierto en borde. Es eso lo que paga el costo de sentarse y escribir el comportamiento antes de pedir el código.

Segunda carrera: 270 generaciones a temperatura 0,7

Una carrera determinística no distingue diferencia real de suerte. Repetí tres veces la 0,7, que es donde estas herramientas suelen ejecutarse en realidad.

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 el promedio aquí. La prosa corta oscila cinco puntos entre repeticiones, mientras que la spec oscila poco más de uno. Y las tres repeticiones de la spec quedaron por debajo de la peor repetición de la prosa larga, lo que significa que los 3,6 puntos de diferencia no salieron de una ronda desafortunada. Tampoco hacen veredicto, y la nota más abajo explica por qué.

Lo que se repite, estable, en las tres rondas, es el abismo entre no escribir el borde y escribirlo. 46% de un lado. 90% y 87% del otro. Si solo puedes llevar un número de este post, lleva ese.

Rompiendo por modelo, en 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, entonces allí la base es 193 pruebas en lugar de 207.

El modelo más fuerte empata los dos brazos informados en 95%. En los otros dos, la prosa larga va por delante por siete y tres puntos. Ninguno de los tres puso a la spec por delante. La tabla agrega las tres repeticiones, entonces no prueba nada sobre una repetición aislada — la estabilidad la muestra la columna de la derecha de la tabla anterior.

En todo modelo, el gran salto sigue siendo el mismo: salir de la prosa corta. El devstral:24b va del 39% al 92% solo porque alguien escribió lo que hace la función en los rincones. No porque alguien abrió un archivo con ## Casos de borde en markdown.

Dónde el promedio esconde lo que sucedió

El promedio es una herramienta pésima para esta pregunta. Mirando celda por celda en la carrera determinística, que son 3 modelos veces 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.

Abrí una de las idénticas a mano, porque no lo creí. Tarea slugificar, devstral:24b, temperatura 0: los dos brazos produjeron código byte por byte igual. Dos prompts de tamaño y diagramación completamente diferentes, cargando el mismo contenido, colapsando en la misma salida.

Resulta que, para el modelo, en ese punto del espacio de decodificación, tabla y párrafo eran la misma solicitud. La información era una sola. La forma se evaporó.

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

El hallazgo más interesante de la carrera es cualitativo y no aparece en ningún promedio. También es el mejor ejemplo de lo que “escribir el borde” significa en la práctica.

En la tarea formatarBRL, el brazo prosa sacó 0 de 11 en los tres modelos. Cero absoluto, incluyendo el camino feliz. Y la causa es la misma en los tres:

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

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

El modelo escribió la solución idiomática, aquella que cualquier revisor humano aprobaría en un vistazo. Y reproba en assert.equal contra 'R$ 1.234,56' con espacio común, porque el Intl devuelve espacio inseparable.

Cuando la información está escrita, y aquí cabe en una línea (“prefijo R$ seguido de un solo espacio común, no use espacio no separable”), dos de los tres modelos comienzan a tratar el caso: o llaman a Intl y cambian el carácter, o formatean a mano. El qwen2.5-coder:32b sigue llamando a Intl sin tratamiento y falla en los tres brazos, con la spec delante de él y todo.

Este es exactamente el “error plausible” que describí en julio, ahora al nivel del carácter. No es código absurdo, que yo detectaría en diez segundos de revisión. Es la línea que todos escribirían, decidiendo en silencio algo que la solicitud no decidió, y que solo aparecerá cuando el valor caiga en un CSV o en una comparación de cadena en el otro lado del sistema.

Si hay un gesto concreto que este post recomienda, es este: antes de pedir la función, escribe la línea que formatarBRL necesitaba. Una frase. El carácter entre el R$ y el número. Qué hacer con null. Qué ocurre en la última página. No necesitas ninguna tabla para esto. Necesitas que la decisión exista en algún lugar que el modelo vaya a leer.

Lo que cuesta la spec en tokens y tiempo

En la tarea truncar, el prompt en prosa corta gastó 94 tokens de entrada. La misma tarea en spec gastó 556, algo así como seis veces más, para una diferencia de acierto que, frente a la prosa larga, no apareció.

Con modelo local estos 462 tokens adicionales son tiempo de prefill, y es poco: 4 ms por generación en esta placa, mediana de 141 ms contra 145 ms, cinco mediciones de cada. Con modelo de API se convierten en 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 en ambos. Lo que dice la medición es que ese costo compra el salto del 46% al 90%, y no compra nada más por estar organizado en tabla.

Al final de cuentas, la cuenta que importa no es “spec versus prosa larga”. Es “solicitud de una frase versus yo me senté y decidí los rincones”. El segundo cuesta tokens. También cuesta el tiempo de pensar. Y es el único costo, de los que medí, que devolvió un retorno claro.

Por qué la bancada no pasó por Aider

En la práctica no converso con el modelo por fetch. Dirijo el modelo local a través de Aider:

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

# o a través de una puerta de enlace 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 más importante. Carga la spec en el contexto sin hacerla editable. Agente que puede editar la spec “resuelve” divergencia reescribiendo la referencia, y entonces pierdo la única cosa que servía de parámetro.

Esto sigue valiendo incluso después de esta bancada. El archivo no ganó acierto por generación por estar estructurado. Ganó utilidad por ser un artefacto que controlo, que no desaparece con la sesión, y que el agente no puede reescribir para librarse del problema.

Y es precisamente por eso que esta bancada no midió a través de Aider. Aider añade mapa de repositorio, formato de edición y reintento. Midiendo con él, el número sería de Aider y no del modelo. La bancada habla con el modelo a través de API cruda, un turno, sin historial. Responde “¿el modelo entendió el requisito?”, que no es la misma pregunta que “¿la herramienta terminó la tarea?”.

Ahora, dónde los modelos pequeños se rompen de verdad en el uso diario en 2025 no es escribir la función. Es aplicar el parche en el formato de búsqueda y reemplazo que Aider espera. El bloque vuelve con la indentación cambiada, o con un fragmento de contexto que no coincide byte por byte con el archivo, y la edición falla. El código estaba correcto todo el tiempo. Esto no aparece en ninguna tasa de acierto de función, y es la mayor parte del atrito real de quien usa esto todos los días.

Dónde esta medición no vale

  • 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”, 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 muy posible que la estructura pague justo donde este experimento no mira.
  • Un solo turno, sin bucle de agente. Sin prueba ejecutándose, sin error volviendo, sin corrección. Gran 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 garantiza que el resultado se transporte a modelo grande de API, ni a modelo más pequeño que estos.
  • Comparación de cadena es frágil, y la formatarBRL prueba. Un carácter invisible anuló una tarea entera. Donde mi prueba es demasiado rígida, reprobo código que serviría; donde es floja, apruebo código que no sirve. Los 3,6 puntos entre spec y prosa larga son lo suficientemente pequeños como para caber en esta fragilidad.
  • Tres repeticiones dan idea de dispersión, no intervalo de confianza. No realicé ningún test estadístico, y no voy a fingir que lo hice.

Lectura honesta: lo que medí con fuerza es el efecto de escribir versus no escribir, en función pura, en modelo local, en un turno. Lo que no medí es si la tabla ayuda al humano a no olvidar el caso, si el archivo ayuda en el bucle con prueba fallando, o si en repositorio real la sección de fuera de alcance protege al modelo. Estas preguntas quedan abiertas. No voy a empujar el resultado a donde no fue.

Lo que cambié de opinión y qué hacer con eso mañana

Escribí en julio que la spec hacía que el agente acertara más. Tal como lo escribí, está equivocado. Lo que hace que el modelo acierte más es que yo haya decidido y escrito lo que ocurre 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 eso, y no el mecanismo que produce el resultado.

De la publicación anterior sigue en pie la parte que interesa: la sección de casos de borde es lo que carga todo. Fue allí donde aparecieron los 44 puntos. Lo que cae es la idea de que la tabla, los encabezados y el markdown hacen diferencia para el modelo. Para el modelo, por lo que medí, no hacen.

Entonces la pregunta útil pasa a ser otra. Si la estructura no compra acierto por generación, ¿para qué sirve el archivo? Aquí necesito ser claro, porque la frontera importa: esto es juicio, no medición. No probé nada de lo que viene abajo.

  • Versionado. Spec pegada 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 coincide con el archivo al lado?”. La segunda tiene respuesta.
  • Reutilización entre herramientas. El mismo archivo sirve Aider, Claude Code, Cursor y yo leyendo. Prosa pegada en un chat no sirve ni la segunda vez.
  • Supervivencia. El modelo cambia cada pocos meses. El archivo que dice lo que hace el sistema sigue valiendo después del cambio; el prompt afinado para un modelo específico, no.

La estructura sirve para mí, y para la próxima persona que abra el archivo. Es por eso que sigo escribiendo spec. Solo que no es lo que dije que era.

Si quieres una regla práctica salida de esta bancada, queda así. Antes de disparar al agente, escribe los casos de borde — en párrafo, en lista, en tabla, da igual para el modelo. Lo que no puede es dejar la laguna abierta y esperar. Después, si la solicitud va a vivir más que una sesión, ponlo en un archivo con --read y versionado. La primera parte es lo que pagaron los 46% contra 90%. La segunda es el oficio alrededor, y yo sigo haciendo, con menos ilusión sobre lo que compra en acierto puro.

Si vas a repetir, es 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, y esa es la parte que vale abrir antes de creer cualquier cosa que acabas de leer. No creas en número de publicación técnica. Ni en los míos. Ejecuta.