Markr (1): por qué tiré la primera versión
Esta es la primera entrada de una serie de siete en la que voy a contar, fase a fase, cómo construí Markr: una aplicación web que corrige redacciones de estudiantes de inglés según el MCER (el Marco Común Europeo de Referencia, o CEFR en inglés). Marca los errores con su explicación, señala las palabras y estructuras del alumno con su nivel (A1 a C2) y calcula una nota de 0 a 10.
La versión que hay hoy se hizo en un solo día, el 24 de octubre de 2026, con Claude Code como compañero de programación. Yo decidía qué construir y cómo tenía que comportarse; Claude escribía la mayor parte del código, los tests y las pruebas en el navegador, y yo revisaba, corregía el rumbo y desplegaba. En la serie intentaré contar las dos cosas: las decisiones técnicas y cómo es trabajar así.
El plan de la serie:
- Por qué tiré la primera versión (esta entrada).
- El motor: el MCER manda y la IA propone.
- La aplicación web: subida de ficheros, notas seguras y compartir con PIN.
- Infraestructura y despliegue en Dokploy con servicios compartidos.
- Cuánto cuesta corregir una redacción con IA.
- Créditos y planes sin marear al usuario.
- Pulido final y lecciones aprendidas.
Pero antes de contar lo nuevo hay que contar lo viejo, porque Markr no empezó ayer.
Mayo de 2025: el primer intento
La idea viene de lejos. Un profesor de inglés con cien redacciones el lunes tiene un problema muy concreto: corregir con criterio, de forma homogénea y sin dejarse la vida. El MCER define qué vocabulario y qué estructuras corresponden a cada nivel, y existe un recurso muy valioso para el vocabulario, el English Vocabulary Profile (EVP), con unas 15.000 acepciones de palabras y expresiones, cada una con su nivel, definición y ejemplos.
En mayo de 2025 hice un primer prototipo en un fin de semana. El historial de commits cuenta la historia mejor que yo:
2025-05-16 Proyecto Markr para corrección de texto en inglés basado en reglas de CEFR
2025-05-17 Separada la detección por niveles CEFR
2025-05-17 Punto interesante de identificación de tokens, pero priman los
individuales sobre los elaborados.
2025-05-17 Introducidas mejoras, pero ahora es muy lento.
2025-05-18 Mejorada velocidad de analisis. [...] Fallan algunas intenciones.
2025-05-18 Alcanzado buen punto de equilibrio, de resultados y velocidad.
2025-05-18 Añadida aplicación base con configuración para redis y celery [...]
2025-05-18 [...] Peta fuertemente por exceso de consumo de memoria a pesar de
usar celery.
2025-05-18 borrado fichero de embeddings por espacio, generar en cada máquina
La arquitectura de entonces era la típica de NLP "clásico":
- spaCy para separar frases y tokens.
- sentence-transformers (mpnet) para comparar cada frase del alumno con los ejemplos del EVP mediante embeddings.
- LanguageTool para los errores gramaticales.
- Django + Celery + Redis para lanzar el análisis en segundo plano.
Funcionaba a ratos, y cada mejora rompía otra cosa. Después de ese fin de semana el proyecto se quedó parado diecisiete meses.
El diagnóstico: problemas de diseño, no de ajuste
Ayer, antes de tocar nada, le pedí a Claude Code que revisara el repositorio entero y me dijera por qué fallaba. El diagnóstico fue duro pero justo. Los problemas no se arreglaban afinando parámetros:
- No detectaba estructuras. El fichero de referencia es solo vocabulario: palabras sueltas y unas pocas expresiones. Lo que más me importaba (la gramática que demuestra el alumno) no existía en el sistema.
- Medía parecido de tema, no de nivel. Comparar el embedding de una frase con los ejemplos del diccionario dice si hablan de lo mismo, no si la gramática es de B2. "I used to think learning English was dificult" salía como B2 porque se parecía al ejemplo de learner.
- Elegía mal la acepción. Cualquier similitud por encima de 0,4 se aceptaba, y con mpnet eso es casi todo. "Every morning" acababa en C2 por every other day. Además buscaba por la forma exacta de la palabra sin lematizar, así que went no encontraba go.
- Se quedaba sin memoria. Los embeddings precalculados (tres vectores de 768 números por entrada, guardados como listas de Python) superaban 1 GB, y se cargaban en cada proceso de Celery y en Django, porque una vista importaba el módulo de tareas y ese módulo instanciaba el anotador al importarse. De ahí el "peta fuertemente".
- Errores incompletos. LanguageTool estaba filtrado solo a gramática, así que no marcaba faltas de ortografía como dificult o realy.
- No había forma de medir. Todo se había ajustado contra un único texto de prueba. Sin un conjunto de evaluación, cada cambio era una apuesta.
Y unos cuantos fallos sueltos: un campo de estado que se escribía con otro nombre (y por tanto nunca cambiaba), db.sqlite3 y los __pycache__ versionados en el repositorio... Lo de la base de datos versionada me pasaría factura ayer mismo; lo cuento en la última entrada.
La decisión: tirarlo todo
La propuesta fue empezar de cero y conservar solo el fichero del EVP. Mi respuesta fue literalmente: "tira todo lo que quieras y empezamos de cero con lo que comentas, la API de Claude me va bien".
¿Por qué no intentar salvar algo? Porque el enfoque había cambiado de raíz. En 2025, usar un modelo de lenguaje grande para esto era caro y poco fiable. En 2026, un modelo como Claude reconoce estructuras gramaticales aunque cambien las palabras, corrige y explica los errores en español mucho mejor que cualquier combinación de reglas y embeddings que yo pudiera mantener. Lo que hace falta ya no es más NLP clásico, sino un buen sistema alrededor del modelo que controle lo que devuelve.
La idea central de la nueva versión, que desarrollo en la siguiente entrada, se resume en tres principios:
- El modelo nunca da posiciones ni niveles oficiales. Devuelve citas literales del texto e identificadores de entradas del fichero; el código busca esas citas y saca los niveles del fichero. Si algo no aparece, se descarta.
- La nota la calcula una fórmula en código, no la "opina" el modelo. Es reproducible y se puede calibrar.
- Todo se mide contra textos de prueba anotados, antes y después de cada cambio.
Lo que quedó y lo que se fue
Del proyecto antiguo se borró todo: la app cefr/, el anotador, los scripts de reglas y de embeddings, result.json, los requirements*.txt (sustituidos por pyproject.toml y uv.lock), los __pycache__ versionados y la base de datos. También desaparecieron las dependencias pesadas: sentence-transformers, torch y LanguageTool.
Solo sobrevivió el vocabulario, movido a data/evp_vocab.json. Y una lección: un prototipo de fin de semana sin forma de medir resultados puede parecer que avanza mientras da vueltas en círculo.
En la próxima entrada entro en el corazón del sistema: cómo se reparten el trabajo spaCy, el fichero MCER y Claude, y cómo descubrí que la IA se estaba inventando niveles.