Un bot de documentación puede equivocarse durante semanas sin que nadie lo note. Responde con un párrafo bien escrito, el usuario asiente y sigue.
Con la facturación eso no pasa. El suscriptor que pregunta “¿por qué me llegó más alta la factura este mes?” tiene la factura abierta en el mismo teléfono y va a comparar. Una cifra equivocada dicha con seguridad termina en un reclamo y en un cliente que confía menos en el WhatsApp de la empresa.
Construimos un asistente de WhatsApp para ABDO 77, un proveedor de internet de Güigüe, en Carabobo, que responde preguntas de planes y facturación. El RAG correctivo (CRAG) es generación aumentada por recuperación con un punto de control: antes de que el modelo escriba nada, un evaluador califica si los documentos recuperados responden la pregunta, y si la nota es mala el sistema hace otra cosa en lugar de contestar.
El asistente consulta dos fuentes, y cada una necesita su propia defensa. Los documentos markdown (planes, tarifas, políticas) pueden devolver la página equivocada, así que pasan por RAG correctivo con una rama que deriva la conversación a una persona. La API de cuentas devuelve datos correctos, pero el modelo puede describirlos mal, así que cada cifra se calcula en código y el modelo solo redacta la frase.
Por qué el RAG simple falla con facturas
El flujo de manual no duda nunca: convierte la pregunta en un embedding, trae los k fragmentos más parecidos, los mete en el prompt y genera. Top-k siempre devuelve k resultados, aunque ninguno responda la pregunta, y el modelo contesta igual, armando algo verosímil con lo que tenga cerca. En un bot de documentación eso da respuestas vagas. En facturación da un número concreto que el usuario puede desmentir.
Las preguntas de facturación también se llevan mal con la similitud semántica. “¿Por qué subió mi factura?” se parece por igual a la página de tarifas, a la de promociones, a los términos del servicio y a la política de prorrateo. Las cuatro hablan de cobros. Una sola explica el aumento de este suscriptor, y muchas veces ninguna, porque la razón está en su cuenta.
Primero, lo básico:
- Fragmentar por las secciones del documento. Una sección completa (“Cargos prorrateados al cambiar de plan”) funciona mejor que una ventana fija de tokens que corta una regla por la mitad.
- Guardar metadatos en cada fragmento (archivo, sección, fecha de actualización) para saber qué quedó viejo cuando cambia una tarifa.
- Usar búsqueda híbrida. Nombres de plan, códigos y montos son cadenas exactas, y la búsqueda vectorial es floja con coincidencias exactas. Combínala con búsqueda por palabras clave (BM25), une los dos rankings con reciprocal rank fusion y después reordena los resultados con un modelo de reranking.
Nada de esto le permite al sistema decir “no sé”. Eso lo aporta el RAG correctivo.
¿Qué es el RAG correctivo (CRAG)?
El paper Corrective Retrieval Augmented Generation (CRAG), de Shi-Qi Yan, Jia-Chen Gu, Yun Zhu y Zhen-Hua Ling (2024), mete un evaluador entre la búsqueda y la generación. En el paper es un T5-large ajustado (unos 0,77 mil millones de parámetros) que califica cada documento recuperado frente a la pregunta. Con dos umbrales, esas calificaciones se convierten en una de tres acciones:
- Correct (correcto): al menos un documento supera el umbral superior. Los documentos se depuran con un paso de descomponer y recomponer que se queda con lo relevante y descarta el resto.
- Incorrect (incorrecto): todos quedan por debajo del umbral inferior. El paper descarta lo recuperado y sale a buscar en la web.
- Ambiguous (ambiguo): cualquier caso intermedio. El paper combina ambas fuentes.
Para producción, lo valioso es que el flujo ahora tiene una rama donde puede vivir el “no sé”, y qué pones ahí es una decisión de producto.
Para facturación, la salida que propone el paper ante un mal veredicto no sirve. La razón por la que subió una factura no está en internet: está en el tarifario de este operador y en la cuenta de este suscriptor. Por eso, en nuestro caso, la rama Incorrect no busca más lejos. Se detiene y pasa la conversación a una persona.
Antes de confiar en el evaluador, lee la reproducción independiente y de código abierto de Yalavarthi (2026). Su análisis de explicabilidad encuentra que el evaluador T5 se apoya sobre todo en la coincidencia de entidades nombradas, más que en la similitud semántica, y que rinde peor fuera de su dominio de entrenamiento. Los mensajes de facturación casi no traen entidades (“¿por qué salió más alta?”), así que evalúa con mensajes reales de suscriptores, no con preguntas bien formuladas que nombran el plan.
Consultar una cuenta no es recuperar documentos
El error que más me gustaría ahorrarte es tratar la API de cuentas como un buscador más.
Los documentos se buscan por similitud, así que pueden devolver un documento vecino pero equivocado: la página de promociones cuando la pregunta era sobre prorrateo. Es una falla de relevancia, y calificar relevancia es justo lo que hace el evaluador.
La API del suscriptor se consulta por clave. Mandas el identificador de la cuenta y recibes esa cuenta, o un error. No hay relevancia que calificar ni estado intermedio.
Pero la API trae su propia falla, difícil de ver porque los datos entran correctos: el modelo puede contar mal números que son exactos. Si le pasas a un LLM el saldo actual, el anterior y tres conceptos de la factura, y le pides que explique la diferencia, le estás pidiendo aritmética en prosa. Va a sonar igual de seguro acierte o no, y el evaluador no ayuda, porque no hubo ninguna búsqueda mala.
| Documentos markdown | API del suscriptor | |
|---|---|---|
| Cómo se consulta | Búsqueda por similitud | Consulta por clave |
| Cómo falla | Trae un documento cercano pero equivocado | Trae datos correctos que el modelo describe mal |
| Qué lo frena | Evaluador y rama de abstención | Calcular las cifras en código; el modelo solo redacta |
La regla que aplicaría a cualquier asistente de facturación: los números que vienen de un sistema de registro le llegan al cliente tal cual. Diferencias, totales y comparaciones de fechas son cálculos, y los cálculos van en código que se puede probar. Cambiar a un modelo más potente deja el problema donde está, y la aritmética equivocada suena más convincente.
El enrutamiento, en código
Una versión simplificada del flujo en TypeScript. Muestra la forma, no es el código de producción.
type Veredicto = 'correct' | 'ambiguous' | 'incorrect';
async function responder(msg: MensajeEntrante): Promise<Respuesta> {
// Si pide una persona, se la damos. Sin "déjame intentar una vez más".
if (pideHumano(msg.texto)) return traspasar(msg, 'lo_pidio');
const ruta = await clasificarIntencion(msg.texto); // 'cuenta' | 'politica' | 'otro'
if (ruta === 'otro') return traspasar(msg, 'fuera_de_alcance');
if (ruta === 'cuenta') {
const cuenta = await apiFacturacion.resumen(msg.suscriptorId);
if (!cuenta) return traspasar(msg, 'consulta_fallida');
return explicarCuenta(cuenta);
}
const docs = await busquedaHibrida(msg.texto, { k: 20 }).then(reordenar);
const veredicto: Veredicto = await evaluarRecuperacion(msg.texto, docs);
if (veredicto === 'incorrect') return traspasar(msg, 'baja_confianza');
if (veredicto === 'ambiguous') return pedirAclaracion(msg, docs);
return responderConDocs(msg.texto, depurar(docs));
}
Y la parte que saca la aritmética del modelo. Los montos van en centavos enteros, la diferencia se calcula aquí, y el borrador del modelo se descarta si contiene un número que no se le dio:
function explicarCuenta(c: ResumenFactura): Respuesta {
const diferencia = c.totalActualCentavos - c.totalAnteriorCentavos;
const cambiados = c.conceptos.filter((x) => x.montoCentavos !== x.montoAnteriorCentavos);
const hechos = {
diferencia: formatearMonto(diferencia),
conceptos: cambiados.map((x) => ({
nombre: x.nombre,
antes: formatearMonto(x.montoAnteriorCentavos),
ahora: formatearMonto(x.montoCentavos),
})),
};
return redactarConModelo('diferencia_cuenta', hechos, {
// Todo número del borrador debe salir de `hechos`; si no, plantilla fija.
validar: (borrador) => numerosEn(borrador).every((n) => numerosPermitidos(hechos).has(n)),
alternativa: () => plantilla('diferencia_cuenta', hechos),
});
}
La validación es tosca a propósito y atrapa la falla que importa: el modelo inventando una cifra.
Lo que WhatsApp no te deja hacer
En una web, la incertidumbre se muestra con interfaz: citas, panel de fuentes, etiquetas de confianza. WhatsApp te da una burbuja de texto, sin notas al pie, y un enlace largo en medio de una frase parece spam. La única señal honesta es si el mensaje afirma un número o no, así que la decisión de confianza se toma antes de generar. Ahí está el evaluador.
La plataforma sí ofrece dos cosas que vale la pena aprovechar:
- Botones de respuesta. La Cloud API permite botones de respuesta interactivos: hasta tres por mensaje, con títulos de máximo 20 caracteres. Encajan en la rama Ambiguous (“¿es sobre tu plan, un cobro o una promoción?” en tres toques) y en un botón de “Hablar con alguien” debajo de cada respuesta sobre la cuenta.
- La ventana de atención. Según la documentación de Meta, cada mensaje del usuario abre una ventana de 24 horas para responder libremente. Cuando se cierra, solo puedes enviar plantillas aprobadas. Un traspaso que espera en cola hasta el día siguiente puede perder esa ventana. El horario de soporte, y una plantilla lista para respuestas tardías, pasan a ser parte de la arquitectura.
Abstenerse también es parte del producto
La tentación es tratar las abstenciones como tasa de error y bajarlas a cero. Cada una que eliminas aflojando el evaluador se vuelve una respuesta segura sobre una factura, y algunas van a estar mal.
El trabajo del asistente es resolver lo que sí tiene respuesta en los documentos (comparar planes, explicar un concepto de la factura, decir cuándo cierra el ciclo o cuándo vence una promoción) y derivar el resto de forma ordenada. En el código de arriba el traspaso ocurre en cuatro casos: el usuario pide una persona, la pregunta está fuera de alcance, la consulta a la API falla o el evaluador rechaza lo recuperado.
El primer caso es el que más se discute: intentar una vez más mejora la métrica de contención, pero quien escribió “quiero hablar con una persona” recibe un párrafo que no pidió y aprende que el bot le estorba.
El traspaso tiene que llevar contexto: la pregunta, la ruta, el veredicto, los documentos o datos de cuenta usados. Si el agente humano arranca con “¿en qué te puedo ayudar?”, el suscriptor tiene que explicar todo dos veces.
Qué medir desde el primer día
- Un conjunto de evaluación fijo de preguntas reales con su ruta esperada, que corres de nuevo con cada cambio de prompt, modelo o búsqueda.
- Una traza por conversación con ruta, veredicto, fragmentos recuperados, latencia y costo, para saber qué paso falló.
- La tasa de abstención como perilla que ajustas según los reclamos, no como número a minimizar.
- Respuestas que soporte marcó como equivocadas, el número que siente el cliente.
- Desacuerdos del evaluador: casos donde el veredicto y la resolución humana no coinciden. Ahí suele estar el próximo arreglo.
Esto es un sistema para un operador, contado como arquitectura, así que no hay gráfico de tasa de resolución. La misma forma sirve para cualquier operación con tarifas documentadas, las mismas preguntas una y otra vez y un canal donde se espera a una persona: servicios públicos, aseguradoras, clínicas o administradoras de inmuebles.
Si vas a construir uno, arma primero el conjunto de evaluación fijo con mensajes reales copiados de tu bandeja de soporte, antes de tocar un prompt. Si quieres ayuda para separar qué preguntas necesitan datos de cuenta, escríbeme.