leonardoprimero
Curioso
¡Usuario con pocos negocios! ¡Utiliza siempre saldo de Forobeta!
Hay un momento, cuando armás un sistema que lee documentos con IA, en el que sentís que ganaste.
El mío fue un martes. Le pasé un comprobante fotografiado con el teléfono: torcido, con el flash pegándole justo en el medio del papel, uno de esos que si se lo mandás a un humano te contesta "no se lee". El modelo me devolvió el CUIT, el monto y la fecha. Los tres perfectos. Le corrí el dígito verificador al CUIT y dio bien.
Me quedé mirando la pantalla con esa cara de idiota feliz que uno pone cuando algo funciona a la primera.
Tardé tres meses en entender que ese martes no había ganado nada.
Es un invento hermoso. Barato, instantáneo, sin conexión a nada. Casi todos los países tienen el suyo, y casi todos los sistemas de facturación del mundo se apoyan en eso para dormir tranquilos.
Ahora, la palabra clave de todo ese párrafo es tipeo. Y la segunda palabra clave es humano.
Porque el dígito verificador fue diseñado para atrapar a una persona cansada que le pifia a una tecla. Un modelo de lenguaje no le pifia a una tecla. Un modelo de lenguaje inventa. Y esas son dos formas radicalmente distintas de equivocarse.
Cuando un humano se equivoca, produce basura evidente. Cuando un modelo se equivoca, produce algo que parece bien. Ese es todo el problema, y me costó tres meses y una medición verlo.
De esos 126, 77 identificadores pasaron el dígito verificador. Aritméticamente impecables. Cualquier sistema del mundo los habría aceptado.
Fui a chequearlos contra el registro nacional, uno por uno. Diez estaban mal.
Nueve de esos diez directamente no existían: números formalmente válidos que no le corresponden a ningún contribuyente. Pero el que me sacó el sueño fue el décimo.
Ese existía. Era un CUIT real, de un contribuyente real, activo. Solo que no era el del comprobante. El papel decía "Proveedora Norte SRL" y ese número pertenecía a una señora que no tenía absolutamente nada que ver con la operación.
Pensá lo que significa. El dígito verificador dice que sí. El formato dice que sí. El registro dice que sí, existe. Y sin embargo estás por asentar un pago a nombre de otra persona, y no hay una sola validación aritmética en el universo que te lo pueda avisar. Solo te lo puede decir alguien que compare el nombre. Y eso ya no es matemática, es una consulta externa.
Un dígito verificador te dice que un número está bien formado. Nunca te dijo que fuera verdadero, y nosotros llevamos cincuenta años leyéndolo mal porque con humanos alcanzaba.
Agarré los mismos 126 documentos y los pasé dos veces. La misma imagen. El mismo prompt. El mismo modelo. Cambié cero cosas entre una corrida y la otra.
38 de 126 documentos volvieron distintos.
Mi primera reacción fue la que va a tener la mitad de los que lean esto: "ah, es la temperatura, ponele cero y listo". Lo puse en cero. Volví a correr las dos pasadas.
Dieron 39 y 38.
O sea: peor. La variación no venía de la temperatura de sampleo, venía de otro lado. Y ahí es donde cae la ficha más importante de todo el proyecto: ese número es un techo. Si un campo no se pone de acuerdo consigo mismo el 30% de las veces, no existe validación posterior que lo haga más confiable que eso. Podés detectar el desacuerdo y mandarlo a revisar. No podés arreglarlo.
Si alguna vez le prometiste a un cliente "95% de precisión" sin haber corrido tus documentos dos veces, no sabés si es verdad. Yo tampoco sabía.
Probá de calcular el dígito verificador de
Pasa. Un CUIT de once ceros es matemáticamente perfecto. Un CBU de veintidós ceros también.
¿Y por qué importa una cosa tan boluda? Porque eso es exactamente lo que devuelve un modelo cuando no puede leer un campo pero le pediste que devuelva algo. Le pusiste en el prompt "devolveme el CUIT" y el modelo, que no tiene forma de decirte "che, esta parte está quemada por el flash", te rellena con ceros. Y tu validación le da la bienvenida con una sonrisa.
No es un caso teórico que se me ocurrió tomando mate. Es una entrada real, y es la forma más común que tiene un modelo de decirte "no sé" cuando lo obligaste a contestar.
En mi código eso es una guarda explícita, con la aritmética escrita en el test al lado, para que dentro de un año nadie la borre por parecer redundante.
1. Primero lo determinístico, después lo caro. Dígitos verificadores, coherencia de fechas, plausibilidad de montos: todo eso es matemática pura, sin red, y cuesta microsegundos. Un documento que no pasa la aritmética no merece que le gastes una llamada a una API.
2. La verificación externa arranca sin poder decidir nada. Cuando conectás una consulta a un registro externo, no sabés cuánto va a estar en desacuerdo con vos. Si la enchufás de una y resulta que discrepa el 20% de las veces, el lunes alguien llega a una cola de revisión que se duplicó de un día para el otro. Así que la consulta corre desde el día uno y no cambia ningún resultado: solo anota. Juntás dos semanas de desacuerdos reales y recién ahí decidís cuánto tiene que pesar cada uno.
enchufás de una y resulta que discrepa el 20% de las veces, el lunes alguien llega a una cola de revisión que se duplicó de un día para el otro. Así que la consulta corre desde el día uno y no cambia ningún resultado: solo anota. Juntás dos semanas de desacuerdos reales y recién ahí decidís cuánto tiene que pesar cada uno.
Y ojo con esto, que es lo que más me costó: eso no se garantiza con un comentario que diga "cuidado, no usar acá". Se garantiza con la forma de la función. La mía recibe un resultado ya congelado y devuelve observaciones. No se le puede pasar nada que pueda modificar, y no devuelve nada que el que llama esté obligado a usar. Para romper la regla hay que cambiar la firma, y eso ya es una discusión en un code review, no un accidente de un jueves a las siete de la tarde.
3. Si la verificación se cae, el sistema sigue. Registro caído, credencial vencida, timeout, respuesta con basura: todo eso se marca como "no se pudo chequear" y el documento sigue su curso normal. Una capa de verificación que puede frenar el pipeline no es un control nuevo: es una caída nueva.
Y muy en serio con esto: cuando no se pudo consultar nada, la tasa de desacuerdo tiene que devolver "no sé", nunca cero. "Nadie estuvo en desacuerdo" y "no se le preguntó a nadie" son dos hechos distintos, y confundirlos es la forma exacta en la que una integración rota se ve verde en un tablero.
4. El umbral de aceptación automática es una decisión de recursos humanos disfrazada de constante. Ese número decide cuántos documentos le caen mañana a una persona para revisar a mano. Moverlo una décima le cambia el día a alguien. Que sea un valor con nombre, documentado, y que quien lo toca sepa lo que está haciendo.
https://github.com/leonardoprimero/verified-extraction
Lo dejo acá porque es el respaldo de todos los números que tiré arriba, no porque quiera visitas: si algo de lo que dije les parece exagerado, el test que lo demuestra está adentro.
Son unas 1.200 líneas de Python con biblioteca estándar y nada más. Sin dependencias, sin API key, sin red. Lo clonás, corrés
Aclaración importante: el sistema de producción es privado, porque procesa comprobantes reales de un cliente real. Lo que está publicado es una reimplementación limpia del patrón. Todos los identificadores del repo son sintéticos, generados con las mismas funciones de dígito verificador. Las mediciones que cité sí son reales, pero son conteos agregados, sin ningún dato de nadie.
Antes de prometer un porcentaje de precisión, pasá tus documentos dos veces y contá cuántos volvieron distintos.
Ese número no lo sabe nadie hasta que lo mide. A mí me dio 38 de 126, y yo estaba convencido de que mi sistema andaba bárbaro.
Si alguno lo corre sobre sus propios documentos, me encantaría que postee el resultado acá. Sospecho que somos varios los que venimos durmiendo tranquilos sin motivo.
El mío fue un martes. Le pasé un comprobante fotografiado con el teléfono: torcido, con el flash pegándole justo en el medio del papel, uno de esos que si se lo mandás a un humano te contesta "no se lee". El modelo me devolvió el CUIT, el monto y la fecha. Los tres perfectos. Le corrí el dígito verificador al CUIT y dio bien.
Me quedé mirando la pantalla con esa cara de idiota feliz que uno pone cuando algo funciona a la primera.
Tardé tres meses en entender que ese martes no había ganado nada.
El dígito verificador es un invento de los años setenta
Por si alguno no lo tiene fresco: un CUIT tiene once números y el último no es información, es un control. Se calcula haciendo una suma ponderada de los otros diez. Si te equivocás al tipear un dígito, la cuenta deja de cerrar y el sistema te lo rebota antes de que hagas un desastre.Es un invento hermoso. Barato, instantáneo, sin conexión a nada. Casi todos los países tienen el suyo, y casi todos los sistemas de facturación del mundo se apoyan en eso para dormir tranquilos.
Ahora, la palabra clave de todo ese párrafo es tipeo. Y la segunda palabra clave es humano.
Porque el dígito verificador fue diseñado para atrapar a una persona cansada que le pifia a una tecla. Un modelo de lenguaje no le pifia a una tecla. Un modelo de lenguaje inventa. Y esas son dos formas radicalmente distintas de equivocarse.
Cuando un humano se equivoca, produce basura evidente. Cuando un modelo se equivoca, produce algo que parece bien. Ese es todo el problema, y me costó tres meses y una medición verlo.
Primer número: 77 pasaron, 10 estaban mal
Agarré 126 comprobantes reales de un pipeline en producción y me puse a contar en serio, no a mirar por arriba.De esos 126, 77 identificadores pasaron el dígito verificador. Aritméticamente impecables. Cualquier sistema del mundo los habría aceptado.
Fui a chequearlos contra el registro nacional, uno por uno. Diez estaban mal.
Nueve de esos diez directamente no existían: números formalmente válidos que no le corresponden a ningún contribuyente. Pero el que me sacó el sueño fue el décimo.
Ese existía. Era un CUIT real, de un contribuyente real, activo. Solo que no era el del comprobante. El papel decía "Proveedora Norte SRL" y ese número pertenecía a una señora que no tenía absolutamente nada que ver con la operación.
Pensá lo que significa. El dígito verificador dice que sí. El formato dice que sí. El registro dice que sí, existe. Y sin embargo estás por asentar un pago a nombre de otra persona, y no hay una sola validación aritmética en el universo que te lo pueda avisar. Solo te lo puede decir alguien que compare el nombre. Y eso ya no es matemática, es una consulta externa.
Un dígito verificador te dice que un número está bien formado. Nunca te dijo que fuera verdadero, y nosotros llevamos cincuenta años leyéndolo mal porque con humanos alcanzaba.
Segundo número: el modelo no está de acuerdo consigo mismo
Este me gustó todavía menos.Agarré los mismos 126 documentos y los pasé dos veces. La misma imagen. El mismo prompt. El mismo modelo. Cambié cero cosas entre una corrida y la otra.
38 de 126 documentos volvieron distintos.
Mi primera reacción fue la que va a tener la mitad de los que lean esto: "ah, es la temperatura, ponele cero y listo". Lo puse en cero. Volví a correr las dos pasadas.
Dieron 39 y 38.
O sea: peor. La variación no venía de la temperatura de sampleo, venía de otro lado. Y ahí es donde cae la ficha más importante de todo el proyecto: ese número es un techo. Si un campo no se pone de acuerdo consigo mismo el 30% de las veces, no existe validación posterior que lo haga más confiable que eso. Podés detectar el desacuerdo y mandarlo a revisar. No podés arreglarlo.
Si alguna vez le prometiste a un cliente "95% de precisión" sin haber corrido tus documentos dos veces, no sabés si es verdad. Yo tampoco sabía.
Y ahora sí, los once ceros
Este es mi favorito porque es el más tonto y el más peligroso al mismo tiempo.Probá de calcular el dígito verificador de
00000000000. La suma ponderada de once ceros da cero. El dígito esperado es cero. El dígito que está es cero.Pasa. Un CUIT de once ceros es matemáticamente perfecto. Un CBU de veintidós ceros también.
¿Y por qué importa una cosa tan boluda? Porque eso es exactamente lo que devuelve un modelo cuando no puede leer un campo pero le pediste que devuelva algo. Le pusiste en el prompt "devolveme el CUIT" y el modelo, que no tiene forma de decirte "che, esta parte está quemada por el flash", te rellena con ceros. Y tu validación le da la bienvenida con una sonrisa.
No es un caso teórico que se me ocurrió tomando mate. Es una entrada real, y es la forma más común que tiene un modelo de decirte "no sé" cuando lo obligaste a contestar.
En mi código eso es una guarda explícita, con la aritmética escrita en el test al lado, para que dentro de un año nadie la borre por parecer redundante.
Las cuatro cosas que me quedaron
Después de todo eso reescribí el sistema con cuatro reglas. Van cortas:1. Primero lo determinístico, después lo caro. Dígitos verificadores, coherencia de fechas, plausibilidad de montos: todo eso es matemática pura, sin red, y cuesta microsegundos. Un documento que no pasa la aritmética no merece que le gastes una llamada a una API.
2. La verificación externa arranca sin poder decidir nada. Cuando conectás una consulta a un registro externo, no sabés cuánto va a estar en desacuerdo con vos. Si la enchufás de una y resulta que discrepa el 20% de las veces, el lunes alguien llega a una cola de revisión que se duplicó de un día para el otro. Así que la consulta corre desde el día uno y no cambia ningún resultado: solo anota. Juntás dos semanas de desacuerdos reales y recién ahí decidís cuánto tiene que pesar cada uno.
enchufás de una y resulta que discrepa el 20% de las veces, el lunes alguien llega a una cola de revisión que se duplicó de un día para el otro. Así que la consulta corre desde el día uno y no cambia ningún resultado: solo anota. Juntás dos semanas de desacuerdos reales y recién ahí decidís cuánto tiene que pesar cada uno.
Y ojo con esto, que es lo que más me costó: eso no se garantiza con un comentario que diga "cuidado, no usar acá". Se garantiza con la forma de la función. La mía recibe un resultado ya congelado y devuelve observaciones. No se le puede pasar nada que pueda modificar, y no devuelve nada que el que llama esté obligado a usar. Para romper la regla hay que cambiar la firma, y eso ya es una discusión en un code review, no un accidente de un jueves a las siete de la tarde.
3. Si la verificación se cae, el sistema sigue. Registro caído, credencial vencida, timeout, respuesta con basura: todo eso se marca como "no se pudo chequear" y el documento sigue su curso normal. Una capa de verificación que puede frenar el pipeline no es un control nuevo: es una caída nueva.
Y muy en serio con esto: cuando no se pudo consultar nada, la tasa de desacuerdo tiene que devolver "no sé", nunca cero. "Nadie estuvo en desacuerdo" y "no se le preguntó a nadie" son dos hechos distintos, y confundirlos es la forma exacta en la que una integración rota se ve verde en un tablero.
4. El umbral de aceptación automática es una decisión de recursos humanos disfrazada de constante. Ese número decide cuántos documentos le caen mañana a una persona para revisar a mano. Moverlo una décima le cambia el día a alguien. Que sea un valor con nombre, documentado, y que quien lo toca sepa lo que está haciendo.
El código
Junté todo eso en un repo público, con licencia Apache-2.0:https://github.com/leonardoprimero/verified-extraction
Lo dejo acá porque es el respaldo de todos los números que tiré arriba, no porque quiera visitas: si algo de lo que dije les parece exagerado, el test que lo demuestra está adentro.
Son unas 1.200 líneas de Python con biblioteca estándar y nada más. Sin dependencias, sin API key, sin red. Lo clonás, corrés
python -m pytest y en cinco centésimas de segundo tenés 122 tests en verde. Después corrés python examples/walkthrough.py y ves cinco documentos fallando de cinco maneras distintas, incluida la del CUIT que existe pero es de otro.Aclaración importante: el sistema de producción es privado, porque procesa comprobantes reales de un cliente real. Lo que está publicado es una reimplementación limpia del patrón. Todos los identificadores del repo son sintéticos, generados con las mismas funciones de dígito verificador. Las mediciones que cité sí son reales, pero son conteos agregados, sin ningún dato de nadie.
Para cerrar
Si estás automatizando con IA cualquier cosa que después alguien va a dar por cierta — facturas, remitos, formularios, padrones, lo que sea — te dejo el único consejo que tengo:Antes de prometer un porcentaje de precisión, pasá tus documentos dos veces y contá cuántos volvieron distintos.
Ese número no lo sabe nadie hasta que lo mide. A mí me dio 38 de 126, y yo estaba convencido de que mi sistema andaba bárbaro.
Si alguno lo corre sobre sus propios documentos, me encantaría que postee el resultado acá. Sospecho que somos varios los que venimos durmiendo tranquilos sin motivo.

