Proyecto final: un agente que revisa facturas
Herramientas sobre texto de OCR, un bucle con presupuesto y guardia contra repeticiones, y un informe legible. Tres facturas, dos con problema.
Combina los 7 ejercicios del bloque Agentes y percepción. Se desbloquea al terminarlos, pero puedes intentarlo ya.
INVOICES son tres facturas tal como las leyó el OCR. parseToolCall, extractFields, validateCIF, validateNIF y checkTotals ya están escritas (son las de los ejercicios anteriores). Cada herramienta cuesta lo que dice COST. Escribe cuatro funciones:
makeTools(invoices): devuelve { listar, leer, revisar }. listar() da los números separados por ", "; leer({ id }) da el texto de esa factura o "error: no existe " + id; revisar({ id }) devuelve, como texto JSON, { id, cif_ok, nif_ok, totales_ok, diff } usando las funciones ya escritas (o el mismo error si no existe).
agentLoop(model, task, tools, maxSteps, budget): como en el ejercicio del agente, con tres reglas más. Una herramienta desconocida devuelve "error: herramienta desconocida: " + nombre y no cuesta nada. Una llamada idéntica a otra anterior (misma herramienta y mismos argumentos) devuelve "error: llamada repetida", no cuesta nada y suma uno a repeats. Si el coste acumulado más el de la llamada supera budget, para antes de ejecutarla con stopped: "budget". Devuelve { answer, steps, stopped, cost, repeats }; en steps, cada herramienta ejecutada guarda { tool, args, result }.
finalReport(steps): de los resultados de revisar que sean JSON, { revisadas: [ids en orden], problemas: [{ id, motivo }] } con motivo "cif", "nif" o "totales" (una entrada por motivo fallido, en ese orden), sin repetir facturas.
runAudit(model, invoices): ejecuta agentLoop con TASK, makeTools(invoices), 12 pasos y presupuesto 20, y devuelve { answer, stopped, cost, report } con report = finalReport(steps). La vista previa muestra la transcripción, el coste y el informe.
Retos 0/6
- Las herramientas: listar, leer y revisar (con el error de un id inexistente)
- El presupuesto para el bucle antes de la llamada que lo supera
- Una llamada repetida y una herramienta desconocida no cuestan ni rompen
- El informe sale de los resultados de revisar
- La auditoría completa: respuesta, coste e informe
- Un modelo que no concluye nunca se queda sin pasos, con el informe parcial
function makeTools(invoices) {
// listar: los ids unidos por ", "; leer: el texto OCR o "error: no existe " + id
// revisar: texto JSON de { id, cif_ok, nif_ok, totales_ok, diff } con extractFields / validateCIF / validateNIF / checkTotals
return { listar: () => '', leer: () => '', revisar: () => '' };
}
function agentLoop(model, task, tools, maxSteps, budget) {
// como en el ejercicio del agente, más: herramienta desconocida -> "error: herramienta desconocida: " + nombre (gratis)
// llamada repetida (misma herramienta + args) -> "error: llamada repetida" (gratis, repeats + 1)
// cost + COST[tool] > budget -> para antes de ejecutarla, stopped: "budget"
return { answer: null, steps: [], stopped: 'max', cost: 0, repeats: 0 };
}
function finalReport(steps) {
// { revisadas: [ids], problemas: [{ id, motivo }] } a partir de los resultados de revisar que sean JSON
return { revisadas: [], problemas: [] };
}
function runAudit(model, invoices) {
// agentLoop con TASK, makeTools(invoices), 12 pasos, presupuesto 20; report = finalReport(steps)
return { answer: null, stopped: 'max', cost: 0, report: finalReport([]) };
}
console.log(runAudit(scriptedModel(SCRIPT), INVOICES));La idea
Un agente en producción no es «el modelo con herramientas». Es el modelo, más las herramientas, más tres defensas que escribes tú: un tope de pasos, un presupuesto y una guardia contra repeticiones. Las tres existen porque los modelos pequeños hacen tres cosas con frecuencia: siguen pidiendo herramientas sin concluir, repiten la misma llamada y, de vez en cuando, inventan una herramienta.
Las herramientas son el lugar donde vive la verdad. revisar no «cree» que el CIF esté mal: lo calcula con la suma de control, y devuelve un JSON que tu informe puede leer sin interpretar prosa. Lo que el modelo diga en su respuesta final es un resumen para la persona; el informe sale de los resultados de las herramientas, y por eso es fiable aunque el modelo se exprese mal.
Lo que medimos: las tres facturas vienen de tesseract.js sobre imágenes sintéticas nuestras (290–475 ms cada una en el DGX Spark, 18-10-2026). El guion de este ejercicio refleja lo que hace un modelo de 8B en nuestras pruebas con tareas así: listar, revisar una a una, repetir una revisión y concluir. En la revisión repetida la guardia ahorra 2 de coste; en una tarea con cien facturas, ahorra el presupuesto entero.
Si llevas esto a tu empresa, la decisión importante es qué permite cada herramienta. revisar solo lee. Una herramienta que «paga» o «envía» necesita una confirmación humana fuera del bucle, y eso no es una limitación de la IA, es diseño.
Hazlo en tu máquina
Con Python, sobre el lote de facturas leídas por el OCR (CC BY 4.0). Las mismas comprobaciones, sin modelo:
curl -O https://vorluxai.com/data/labs/facturas-ocr-lote.json
python3 -c "
import json, re
def cif_ok(c):
s = sum((lambda n: n // 10 + n % 10)(int(d) * 2) if i % 2 == 0 else int(d) for i, d in enumerate(c[1:8]))
return c[8] == str((10 - s % 10) % 10)
amt = lambda t, k: float(re.search(k + r'[^\n]*?(\d[\d.]*,\d{2})', t, re.I).group(1).replace('.', '').replace(',', '.'))
for id, t in json.load(open('facturas-ocr-lote.json'))['invoices'].items():
cif = re.search(r'\b([A-HJ-NP-SUVW]\d{7}[0-9A-J])\b', t).group(1)
diff = round(amt(t, 'Base imponible') + amt(t, 'IVA') - amt(t, 'TOTAL'), 2)
print(id, 'cif ok' if cif_ok(cif) else 'CIF MAL', 'totales ok' if diff == 0 else f'TOTALES MAL ({diff})')
" Para profundizar: la referencia de Ollama (en inglés) →
¿Esto en producción, con tus datos? Hablemos 15 minutos →