Artículos / LLMs & Agentes
Precisión al extraer facturas: validación de fuentes en Python
Valida importes extraídos contra líneas fuente, formatos decimales explícitos y totales. Mide precisión por campo y cobertura de revisión con documentos etiquetados.

La extracción de una factura devuelve JSON válido: subtotal 1000.00, impuesto 190.00 y total 1190.00. Los importes cuadran, pero el documento dice 1090.00. La validación del esquema y la consistencia aritmética pasaron, aunque la extracción es incorrecta.
La precisión requiere comparar con la fuente. Este tutorial añade esa capa después de la extracción estructurada con Python y Pydantic. No reemplaza la integración con el modelo ni repite el tutorial de esquemas. Construye una compuerta ejecutable posterior a la extracción y explica cómo medir errores y cobertura de revisión sin confundir una respuesta exitosa de la API con una factura precisa.
Separa cuatro comprobaciones
| Comprobación | Qué establece | Qué no establece |
|---|---|---|
| Esquema | Los campos cumplen estructura y tipos declarados | Los valores aparecen en el documento |
| Evidencia fuente | Un valor coincide con un pasaje seleccionado | El OCR es correcto o el pasaje tiene el significado esperado |
| Aritmética | Los importes concilian bajo una regla identificada | El proveedor o tratamiento fiscal es correcto |
| Evaluación etiquetada | Las predicciones coinciden con ejemplos revisados | Los futuros documentos tendrán la misma tasa de error |
El modo estricto de Pydantic reduce conversiones no deseadas, pero sigue validando tipos. Un número perfectamente tipado puede corresponder al campo equivocado. Mantén la validación de fuentes y las decisiones de negocio fuera de la afirmación de que el modelo devolvió un objeto válido.
Conserva para cada campo el ID/versión del documento, página, línea o coordenadas, texto original y valor normalizado. La aplicación debe asignar el alcance del documento. El modelo puede seleccionar una ubicación dentro de él, pero no suministrar otro documento como su propia evidencia. Los objetos de respuesta de gastos de Amazon Textract ofrecen un ejemplo concreto de separación entre tipo de campo, etiqueta detectada y valor detectado con ubicación/confianza. Una puntuación de confianza sirve como entrada a la evaluación; no es una probabilidad calibrada de que el asiento contable sea correcto.
Define un contrato explícito de normalización
El perfil sintético de abajo soporta facturas EUR con importes no negativos, dos decimales, coma decimal y puntos opcionales para miles. Valida totales de ítems, subtotal, importe de impuesto declarado y total. No calcula una tasa fiscal legalmente aplicable ni valida una factura electrónica colombiana. Notas crédito, descuentos, retenciones, monedas mezcladas y otros diseños requieren otro perfil revisado o revisión humana.
No infieras un formato de una sola cadena ambigua. 1,234 puede representar un decimal o un entero agrupado bajo convenciones distintas. El formato fuente pertenece al perfil del documento; el formato normalizado de extracción es otra representación fija, por ejemplo 1234.00.
La documentación de Decimal de Python explica por qué las cadenas decimales preservan importes monetarios con más previsibilidad que floats binarios. Construye Decimal desde cadenas y define el redondeo cuando el flujo realmente calcule importes. Este ejemplo compara exactamente importes impresos con dos decimales; no añade una tolerancia que oculte un error de extracción.
Ejecuta una compuerta de importes vinculada a la fuente
Usa Python 3.11. No hay dependencias, claves de API ni llamadas a modelos. Guarda estos bloques, en orden, como validate_invoice.py:
import re
from dataclasses import dataclass, replace
from decimal import Decimal
class ReviewRequired(ValueError):
pass
@dataclass(frozen=True)
class Amount:
value: str # Canonical extraction, for example "1234.56".
line_id: str # ID in the application's source document.
quote: str # Raw amount, for example "1.234,56".
@dataclass(frozen=True)
class Invoice:
currency: str
items: tuple[Amount, ...]
subtotal: Amount
tax: Amount
total: Amount
def checked_amount(amount: Amount, label: str,
source: dict[str, str]) -> Decimal:
if (not isinstance(amount.value, str)
or not re.fullmatch(r"(?:0|[1-9][0-9]{0,11})\.[0-9]{2}", amount.value)):
raise ReviewRequired(f"{label}: invalid normalized amount")
if (not isinstance(amount.quote, str) or len(amount.quote) > 40
or not re.fullmatch(
r"(?:0|[1-9][0-9]*|[1-9][0-9]{0,2}(?:\.[0-9]{3})+),[0-9]{2}",
amount.quote)):
raise ReviewRequired(f"{label}: unsupported source number format")
expected_line = f"{label}: EUR {amount.quote}"
if source.get(amount.line_id) != expected_line:
raise ReviewRequired(f"{label}: missing or mismatched source line")
observed = Decimal(amount.quote.replace(".", "").replace(",", "."))
predicted = Decimal(amount.value)
if predicted != observed:
raise ReviewRequired(f"{label}: prediction differs from source")
return predicted
def validate(invoice: Invoice, source: dict[str, str]) -> None:
if invoice.currency != "EUR":
raise ReviewRequired("Unsupported or missing explicit currency")
if not 1 <= len(invoice.items) <= 100:
raise ReviewRequired("Unsupported item count")
fields = (*invoice.items, invoice.subtotal, invoice.tax, invoice.total)
if len({amount.line_id for amount in fields}) != len(fields):
raise ReviewRequired("Source line reused for more than one field")
items = [checked_amount(item, "Item", source) for item in invoice.items]
subtotal = checked_amount(invoice.subtotal, "Subtotal", source)
tax = checked_amount(invoice.tax, "Tax", source)
total = checked_amount(invoice.total, "Total", source)
if sum(items, Decimal("0.00")) != subtotal:
raise ReviewRequired("Item totals do not match subtotal")
if subtotal + tax != total:
raise ReviewRequired("Subtotal plus tax does not match total")
La comprobación vincula deliberadamente etiqueta, moneda y valor original de la línea. Encontrar 1190.00 en cualquier parte permitiría que una referencia de pago u otro subtotal justificara el campo equivocado. Reutilizar una línea para dos campos también requiere revisión, en lugar de contar dos veces un ítem.
El formato esperado es deliberadamente restrictivo. Los adaptadores OCR reales deben preservar ubicaciones confiables y definir cómo se mapean a cada perfil; no relajes esta comprobación eliminando todos los caracteres que no sean dígitos. Destruirías separadores y contexto del campo. La compuerta no demuestra que se hayan extraído todos los ítems; etiqueta y prueba la completitud por separado. Un pipeline completo también necesita validar identificadores, fechas y correspondencia de proveedor/cuenta; pasar esta compuerta de importes no aprueba un pago ni autoriza una escritura contable.
Distingue una predicción incorrecta de una factura contradictoria
Añade un fixture con dos ítems:
SOURCE = {
"p1-l1": "Item: EUR 800,00",
"p1-l2": "Item: EUR 200,00",
"p1-l3": "Subtotal: EUR 1.000,00",
"p1-l4": "Tax: EUR 190,00",
"p1-l5": "Total: EUR 1.190,00",
}
GOOD = Invoice(
currency="EUR",
items=(Amount("800.00", "p1-l1", "800,00"),
Amount("200.00", "p1-l2", "200,00")),
subtotal=Amount("1000.00", "p1-l3", "1.000,00"),
tax=Amount("190.00", "p1-l4", "190,00"),
total=Amount("1190.00", "p1-l5", "1.190,00"),
)
if __name__ == "__main__":
validate(GOOD, SOURCE)
print("Amount gate passed")
wrong_prediction = replace(GOOD, total=replace(GOOD.total, value="1090.00"))
try:
validate(wrong_prediction, SOURCE)
except ReviewRequired as error:
print(f"Review: {error}")
Ejecuta python validate_invoice.py. Imprime Amount gate passed y Review: Total: prediction differs from source.
Ahora cambia el total de la fuente y su extracción a 1.090,00 / 1090.00. La coincidencia con la fuente pasa, pero la aritmética falla: subtotal más impuesto sigue siendo 1190.00. La respuesta correcta es revisar un documento contradictorio o un perfil no soportado, no reescribir silenciosamente el total impreso. Si el OCR modificó incorrectamente la imagen original, ambas comprobaciones pueden coincidir sobre un texto erróneo; muestra la página original en la interfaz de revisión.
Mide precisión con documentos revisados
Crea un conjunto etiquetado con documento original, valores revisados y ubicaciones que los justifican. Separa los documentos de desarrollo del conjunto fijo de evaluación. Divide por proveedor/plantilla y tiempo cuando sea posible: copias casi idénticas en ambos conjuntos pueden exagerar la generalización.
Reporta como mínimo estas mediciones con sus denominadores:
- Coincidencia exacta por campo: valores normalizados correctos divididos por oportunidades etiquetadas para cada campo. Define la normalización de importes, fechas e identificadores antes de puntuar.
- Tasa de campos omitidos: campos esperados ausentes en la extracción. Un impuesto faltante difiere de un impuesto cero real.
- Tasa de valores sin soporte: predicciones sin evidencia verificada, incluidos campos inventados en documentos donde no existen.
- Coincidencia exacta por documento: documentos donde todos los campos obligatorios son correctos. Un promedio alto por campo puede coexistir con muchos documentos inutilizables.
- Cobertura de la compuerta: documentos que pasan la compuerta propuesta de automatización divididos por todos los documentos evaluados.
- Tasa de error entre documentos aprobados por la compuerta: documentos que pasan y tienen un error material etiquetado, divididos por documentos que pasan. Repórtala como indefinida si ninguno pasa.
Como ilustración de juguete, si 20 documentos revisados contienen 10 que pasan la compuerta y dos de esos 10 tienen un campo obligatorio incorrecto, la cobertura es 10/20 = 50% y el error entre los que pasan es 2/10 = 20%. Son ejemplos aritméticos, no resultados de un modelo ni una meta. Una compuerta que envía todo a revisión tiene cobertura cero; no tiene una tasa de error medida entre documentos que pasan.
Revisa una muestra de los documentos que pasan, además de todos los fallos. De otro modo aprendes solamente sobre rechazos y nunca estimas errores que escaparon. Desglosa resultados por calidad del escaneo, diseño, moneda/formato, campos ausentes y documentos de varias páginas. Registra tamaño del conjunto y conteo real de errores; un porcentaje basado en una muestra diminuta tiene mucha incertidumbre.
Convierte los fallos en estados operativos explícitos
Separa schema_error, source_mismatch, arithmetic_mismatch, unsupported_profile, provider_error y ready_for_review. Repetir una llamada al modelo puede reparar un error de forma; no resuelve un separador ambiguo o una factura inconsistente. Limita reintentos y conserva documento original e intento de extracción para revisión.
El ejemplo se comprobó con importes válidos, predicciones incorrectas, evidencia ausente, líneas repetidas, monedas/formatos no soportados y discrepancias aritméticas. Estas pruebas validan el perfil sintético. No miden precisión del OCR/modelo, cumplimiento fiscal ni un proceso de pagos de producción. Necesitas documentos etiquetados para lo primero y reglas contables aplicables para lo segundo.
Para un pipeline mayor, la orquestación con Prefect puede organizar extracción y tareas de revisión. Excluye el contenido íntegro de facturas de los logs rutinarios, protege documentos fuente y conserva correcciones con procedencia. Si tu equipo necesita pasar de una demo documental a un flujo validado, el proceso de implementación de IA cubre alcance, implementación y verificación.
¿Vas a construir una plataforma de IA o de datos?
Si necesitas adaptar este enfoque a tu sistema, cuéntame qué datos tienes y qué decisión técnica necesitas resolver.