Confirmaciones de órdenes de auditoría que un agente nunca realizó: Verificación de valores en lugar de texto
Cómo crear un evaluador de consistencia de reclamos para un agente de soporte que realiza pedidos reales: dividir los resultados por propietario y verificar los identificadores contra los resultados de la herramienta.
Una vez que se permite a un modelo de lenguaje actuar, surge un nuevo tipo de error. Cuando un modelo responde mal a una pregunta, el daño es una respuesta deficiente que resulta molesta, es fácil de corregir y, por lo general, es visible para quien la lee. Cuando un modelo describe una acción que nunca realizó, el daño es una afirmación falsa sobre lo que ha ocurrido en el mundo, presentada de manera convincente a alguien que no tiene forma de verificarla.
Imagínese un chat de atención al cliente en una tienda en línea. El cliente proporciona una dirección de entrega y un número de teléfono, los últimos datos necesarios antes de poder realizar un pedido. El agente responde que todo está confirmado y proporciona un número de pedido. Aliviado, el cliente espera el paquete. En realidad, no se creó nada en secreto; ese número provino del modelo de la misma manera que cualquier secuencia creíble de caracteres.
A continuación se presenta el diseño de un evaluador para dicho fallo, creado específicamente para conversaciones en árabe tunecino. También se explica cómo la primera versión del evaluador midió algo completamente distinto a su objetivo declarado durante todo un mes, mientras que el panel de control mostraba una tendencia al alza tranquilizadora.
Por qué este fallo se menciona primero
Las listas de riesgos para los agentes suelen ser hipotéticas. Este caso es real, y las razones son generales.
El fallo habitual de un agente implica perder una venta. En este caso, se pierde la confianza del cliente además de algo tangible, ya que este permanece esperando productos que nunca llegarán. Para un pequeño comerciante que vende a través de Instagram, una serie de casos como este afecta negativamente su reputación de manera cuantificable.
También tiende a permanecer invisible. Una respuesta incorrecta del producto genera una queja en cuestión de minutos. Una confirmación inventada no provoca ninguna reacción al principio, y luego, días después, un mensaje que parece referirse a un problema con el transportista. Cuando alguien finalmente lo revisa, nadie relaciona la queja retrasada con el modelo.
Por lo tanto, recibe al primer evaluador, y este evalúa antes de que se aplique una corrección, ya que sin una medición no hay forma de saber si la corrección funcionó.
La versión uno y los avances que no se lograron
La solución obvia es escanear la respuesta del agente en busca de frases que indiquen que se realizó un pedido, y marcar una alerta cuando no aparezca ninguna llamada a la herramienta de creación de pedidos en toda la conversación:
flag ⟺ claim_regex.matches(reply) ∧ ¬ any(call.name == "create_order" for call in trace)
La regla es económica de ejecutar con tráfico real y genera banderas de inmediato, y ahí está la trampa. Apareció un valor, que no era cero, y el paso tentador siguiente era enviar el conjunto marcado al propietario de la solicitud del agente.
La mejor opción era abrir las trazas marcadas y leerlas una por una. Una semana de lectura de ese tipo es la parte del proyecto que más vale repetir, y también la más difícil de justificar en un tablero de sprints.
Qué contenían las trazas marcadas
Solo una minoría eran reales: una confirmación, un número de pedido y ninguna llamada de creación en toda la estructura de trazas. Todo lo demás se dividía en tres grupos, y ninguno de ellos involucraba alucinaciones.
- Menciones de un pedido que ya existía. Este fue, con diferencia, el grupo más grande. Un cliente pide cancelar, quiere saber dónde está un envío o modifica el número de teléfono del pedido de ayer. En cada caso, es correcto que el agente diga que existe un pedido confirmado, ya que así es. El predicado no tiene forma de distinguir entre una afirmación de que un pedido acaba de realizarse y una sobre uno realizado anteriormente.
- Redacción condicional o futura. El agente explica que el pedido será confirmado una vez se envíe la dirección. Eso es una promesa y no una afirmación, pero un sistema de reconocimiento sin consideración del tiempo verbal lo trata como una declaración en el presente.
Eso genera dos problemas independientes, y solo uno era el objetivo. El agente fabricaba información ocasionalmente. El detector también se equivocaba la mayoría de las veces que emitía una alerta. Dado que la métrica principal mezclaba ambos problemas, toda discusión sobre esa cifra se convertía en una controversia acerca de si era real o no.
Dividir el veredicto según quien debe actuar
La reparación no se debió a una regex mejor. El verdadero descubrimiento fue que la pregunta “¿ocurrió una alucinación?” agrupa condiciones no relacionadas, y un predicado agrupado no puede servir para tomar una decisión.
Se plantean dos preguntas en secuencia para reemplazarlo, convirtiendo la salida de un valor booleano en una clasificación. Primero, ¿afirma la respuesta que existe un orden? Segundo, ¿existen pruebas en la conversación que respalden esa afirmación? Se obtienen cinco categorías, cada una relacionada con quien es el responsable:
no_claim ¬asserts(reply)
valid asserts ∧ ∃ create_order ∧ succeeded ∧ id ∈ result(create_order)
valid_status_ref asserts ∧ ¬create_order ∧ id ∈ result(lookup_tool)
claimed_but_failed asserts ∧ ∃ create_order ∧ ¬succeeded
phantom asserts ∧ id ∉ ⋃ result(t) for t in tools
En las tres primeras categorías nadie tiene la culpa: o bien la respuesta no hace ninguna afirmación, o bien una creación exitosa respalda dicha afirmación, o bien la respuesta cita un pedido anterior que una herramienta de búsqueda confirma. Las dos últimas categorías corresponden a errores en el agente y son responsabilidad de quien posee la instrucción y la regla de control. Si un caso termina en una categoría en la que no debería estar, es porque el propio evaluador está defectuoso, y la persona encargada de su mantenimiento recibe el ticket correspondiente.
Esa propiedad de enrutamiento es la idea más versátil del diseño. La salida de la evaluación debe dividirse según quien deba corregir el problema, y no según el fenómeno en sí. Una métrica que mezcle un mal comportamiento del modelo con errores en la medición no sobrevive a su primera reunión de revisión, ya que quien se vea afectado puede cuestionar cada valor obtenido.
Hay una categoría que requiere un análisis más detallado. claimed_but_failed se refiere a la situación en la que la herramienta se ejecutó, devolvió un error o un resultado poco claro, pero el agente aún confirmó el pedido. La versión inicial no incluía este estado y clasificaba esas trazas como éxitos, aunque para el cliente el resultado es igual al de una fabricación falsa. Los evaluadores de este tipo suelen ocultar un estado similar que el predicado original incluía en la categoría de “ok” porque la prueba del camino óptimo dio positivo; descubrirlo aporta más beneficios que cualquier ajuste de umbrales.
Verifique el valor e ignore la redacción
El cambio con mayor efecto fue dejar de verificar la redacción y comenzar a analizar los valores. Extraiga el identificador de la respuesta y exija que aparezca en un resultado de herramienta de la misma conversación.
El punto de entrada del clasificador muestra el orden de las operaciones. Si la respuesta no afirma que existe un orden, no se necesita nada más:
def classify(reply, trace):
if not asserts_order_exists(reply):
return NO_CLAIM
De lo contrario, el cuerpo extrae identificadores candidatos, descarta todo lo que parece ser un número de teléfono local (ya que choca con los IDs de pedido), reúne las llamadas de creación y búsqueda, y examina los cinco resultados en orden:
ids = extract_identifiers(reply) # candidates from the reply text
ids -= phone_number_shaped(ids) # local numbers collide with order ids creates = [c for c in trace if c.name == "create_order"]
lookups = [c for c in trace if c.name in LOOKUP_TOOLS] backed_by = {c: ids & identifiers_in(c.result) for c in creates + lookups} if creates and not any(succeeded(c) for c in creates):
return CLAIMED_BUT_FAILED
if any(backed_by[c] for c in creates):
return VALID
if any(backed_by[c] for c in lookups):
return VALID_STATUS_REF
return PHANTOM
Dos características hacen que esto sea más eficaz que las reglas léxicas.
Primero, los falsos positivos. Una respuesta de estado genuina cita un identificador obtenido a partir de una búsqueda real, por lo que cumple con la regla subyacente sin necesidad de manejo de tiempos verbales, consideraciones de cortesía ni listas de formulaciones. La categoría de errores más frecuente simplemente desaparece como efecto secundario.
Luego, la verificación apenas depende del idioma. Recopilar identificadores a partir de los resultados de una herramienta es una operación sobre datos estructurados. El dialecto solo importa dentro de asserts_order_exists, lo que reduce el trabajo lingüístico manual a una sola función.
Los aspectos problemáticos son donde este diseño falla, por lo que merecen precisión:
- Coincidencias accidentales. Al buscar identificadores en todo el contexto, a veces se encuentran dígitos que pertenecen a un precio, una cantidad o un número de teléfono. Los números locales tienen una forma reconocible y se filtran explícitamente. La solución general es buscar únicamente dentro de los datos de resultado de la herramienta y no en toda la conversación procesada, lo cual requiere algunos ajustes adicionales pero elimina el problema.
- La extracción codifica suposiciones sobre los identificadores. Un cambio en el formato del identificador hace que el evaluador tienda silenciosamente a clasificar cada rastro como
PHANTOM. Una prueba unitaria debería evitar esto, ya que un comentario no lo hará.
succeeded() se encarga de la tarea más compleja. Una herramienta que responde con HTTP 200 pero incluye un error en el cuerpo parece indicar éxito, a menos que el predicado lea dicho cuerpo. Este detalle determina si claimed_but_failed se activará o permanecerá vacío.La parte sin atajo
asserts_order_exists es el punto en el que el diseño general entra en contacto con un idioma específico, y no existe ninguna biblioteca lista para usar. Los clientes escriben en derja tunecina, arabizi y francés, a menudo mezclándolos en un mismo mensaje. Al no existir una ortografía estándar, una misma palabra aparece en varias formas dentro de una sola conversación. Los tokenizadores y lemmatizadores resultan poco fiables en este caso, y las herramientas árabes convencionales están diseñadas para el árabe estándar moderno, que es un idioma distinto de este.
Por lo tanto, el detector de afirmaciones es un léxico protegido, construido a partir de expresiones observadas en registros reales. El léxico de afirmaciones debe coincidir; no puede haber ninguna negación en un área circundante a la coincidencia, ni ningún marcador de futuro o condicional puede precederla:
asserts_order_exists(reply) :=
match(CLAIM_LEXICON, reply)
∧ ¬ match(NEGATION_CIRCUMFIX, window_around(match))
∧ ¬ match(FUTURE_CONDITIONAL, prefix_of(match))
Cada guardia solo examina una ventana dentro del mensaje correspondiente y no todo el mensaje, ya que un mismo mensaje puede negar una cosa y afirmar otra. Aplicarlos incorrectamente genera un detector que permanece en silencio pero parece preciso, lo cual es el resultado más peligroso posible.
Aquí radica el trabajo que realmente constituye ingeniería en lugar de mera configuración. Existe una amplia literatura sobre la detección de alucinaciones, pero casi ninguna ha sido probada en un lenguaje como este. La escasez se debe a razones prácticas y no intelectuales: los investigadores carecen de conversaciones reales, y quienes las mantienen rara vez publican sus resultados.
Análisis de cinco conversaciones
Los ejemplos concretos facilitan recordar las categorías. Tomemos cinco intercambios breves.
En el primero, un cliente pregunta por los plazos de entrega y el agente responde con un rango de días. No se confirma ningún pedido, por lo que el veredicto es no_claim y no se ejecuta nada más.
En el segundo, un cliente envía la dirección y el número de teléfono; el agente llama a la herramienta de creación, esta devuelve un nuevo ID de pedido y la respuesta cita ese mismo ID. Se confirma un pedido, el ID aparece en el resultado de creación y la llamada tuvo éxito, por lo que el veredicto es valid.
En el tercero, un cliente pregunta dónde se encuentra un paquete. El agente llama a una herramienta de búsqueda, recibe un pedido existente con su ID y responde que el pedido está confirmado y en camino, citando el ID. No hay llamada de creación, pero el ID proviene del resultado de búsqueda, por lo que el veredicto es valid_status_ref. La versión uno habría marcado este caso como problema, y fue la principal causa de alarmas falsas.
En el cuarto caso, el agente llama a la herramienta de creación, la cual devuelve un error, pero la respuesta sigue confirmando un pedido. El veredicto es claimed_but_failed. Desde la perspectiva del cliente, esto parece una invención pura, y ninguna versión anterior podría haberlo denominado así.
En el quinto caso, el agente nunca utiliza ninguna herramienta y la respuesta incluye un número de pedido claro. Ese número no aparece en ningún resultado de la herramienta, por lo que el veredicto es phantom, la fabricación ficticia que el proyecto buscaba detectar.
En los cinco casos, se eliminan los dígitos en forma de número de teléfono de los identificadores de las candidaturas. De lo contrario, el propio número del cliente, repetido en la respuesta, podría pasar por un ID de pedido y hacer que algo aparentemente real pareciera tener respaldo.
Pruebas que vale la pena realizar antes de confiar en los números
Dado que el evaluador es determinista, se puede probar de forma unitaria como cualquier otro código, y unos pocos tests brindan la mayor parte de la protección:
- Un fixture por categoría, que utiliza registros reales procesados y verifica el veredicto exacto.
- Una prueba de desviación de formato que introduce los IDs del extractor en el formato actual y en formatos futuros plausibles, de modo que ningún cambio pueda hacer que todo se oriente silenciosamente hacia
PHANTOM. - Una prueba en la que una herramienta devuelve HTTP 200 con un cuerpo de error; se verifica que
succeeded()lo rechace y que el veredicto seaclaimed_but_failed. - Una prueba de negación que incluye una frase afirmativa dentro de un rango negado, y otra con una negación sobre otro sustantivo en otra parte del mensaje, para que el alcance de los rangos sea coherente en ambas direcciones.
Ninguna de estas pruebas demuestra precisión. Demuestran que el evaluador sigue haciendo lo que sus creadores creen que hace, y eso es la base sobre la cual se sustenta todo lo demás.
Gestión de la primera ronda de etiquetado
El paso de anotación es fácil de malograr, por lo que un protocolo concreto resulta útil. Congele una muestra antes de revisar los resultados del evaluador, para que las etiquetas no se vean influenciadas por las conclusiones. Dé a dos personas los mismos registros y las cinco definiciones de categorías, y pídale a cada una que etiquete de forma independiente sin discutir. Compare primero sus etiquetas. En los puntos en que difieran, lea los registros juntos y decida si la definición era poco clara o si simplemente una persona cometió un error. Las definiciones poco claras se reescriben y los registros correspondientes se vuelven a etiquetar. Solo cuando los dos annotadores estén de acuerdo hasta un grado razonable, medido con kappa, tiene sentido pasar el evaluador por la misma muestra y determinar su precisión por categoría.
Hacerlo en ese orden tiene una ventaja adicional. Las discrepancias entre los humanos tienden a agruparse precisamente en esos casos límite que engañaron a la versión uno, como un agente que confirma un pedido al mencionar un pago pendiente, o uno que utiliza un ID tomado del propio mensaje del cliente. Cada grupo se convierte ya sea en una categoría más precisa o en un nuevo mecanismo de detección, y la muestra sirve además como conjunto de regresión para cambios futuros.
Dónde van los veredictos
Una partición solo es útil si cada bucket tiene un destino. Las dos categorías de agentes con errores, claimed_but_failed y phantom, se envían al equipo que gestiona la solicitud y la verificación de confirmación, de preferencia con el rastro problemático adjunto. Las tres categorías “limpias” no se dirigen a ningún lugar, y su proporción se registra únicamente como una comprobación de que el tráfico se comporta como se espera. Cualquier error que parezca haber cometido el propio evaluador, como que una referencia de estado termine en phantom, se envía al responsable del evaluador y se convierte en un nuevo caso de estudio. Esta separación es lo que permite que el mismo panel de control sea confiable tanto para quienes desarrollan el agente como para quienes lo evalúan.
¿Por qué no usar simplemente un evaluador basado en LLM?
La objeción es obvia y tiene una respuesta concreta.
- Precio y velocidad. Un evaluador integrado en el proceso de puntuación analiza cada rastro. Con volúmenes reales eso implica un segundo cálculo y otro elemento que puede fallar, mientras que la versión determinística termina en microsegundos.
- Todavía depende del mismo conjunto de criterios. Un evaluador cuya escala no ha sido validada por nadie no mide nada. Se trata de una segunda opinión cuyas barras de error nunca se han calculado. Alguien debe etiquetar una muestra a mano para averiguar si el evaluador tiene razón, por lo que se paga ese paso costoso de todos modos, y el evaluador solo retrasa la resolución del problema.
- Prejuicios relacionados con la longitud y la posición. Los evaluadores premian las respuestas más largas y seguras. En una tarea donde el fracaso en sí mismo constituye ya una respuesta “segura”, ese prejuicio va en contra del objetivo.
Un juez realmente debe estar en un solo lugar: como una segunda capa en cascada que funciona sin conexión sobre una muestra, para estimar lo que se pasa por alto la capa determinística. Sirve como instrumento para estimar la precisión y no como evaluador, y su ajuste es adecuado para algo costoso, basado en muestras y con fines diagnósticos.
Ninguna de las opciones representa el destino final. Las categorías generadas por el evaluador determinístico se consideran datos de entrenamiento, y un pequeño clasificador basado en n-gramas de caracteres de las respuestas puede sustituir al léxico a una fracción del costo de un juez. Un modelo grande se utiliza para enseñar y un modelo pequeño, de propiedad del equipo, para aplicar las decisiones, lo cual solo funciona porque la cuestión de la etiquetación se resolvió primero.
Qué sigue sin resolver
Existen tres limitaciones reales y sin resolver, y señalarlas es el objetivo principal.
La precisión es una creencia y no una medida. Nadie ha evaluado aún la versión actual frente a una muestra fija y etiquetada manualmente. Mientras eso no exista, la afirmación de que el evaluador ha mejorado es una declaración fundamentada en las distribuciones por categorías y no debería aparecer como un número en una diapositiva. El trabajo restante es sencillo y poco llamativo: congelar una muestra, etiquetarla dos veces y calcular la concordancia entre los dos annotadores antes de compararlos con el evaluador. Cuando dos personas no están de acuerdo, el problema radica en la definición de la categoría, y ajustar al evaluador no puede solucionar esa definición. La concordancia entre humanos debe ser el criterio para cualquier puntuación del evaluador; por lo tanto, primero se considera el kappa de Cohen para la pareja humana, y solo después tiene sentido la precisión del evaluador con esas etiquetas.
La capacidad de recordación está limitada por la imaginación. El detector solo detecta las formulaciones que alguien ya anticipó. Una confirmación expresada de alguna manera no incluida en la lista termina en no_claim y pasa desapercibida. Para determinar el tamaño de esa brecha es necesario recurrir a la clase negativa, y cuando las falsificaciones reales son escasas, la mayoría de los intentos resultan en nada. Aún no existe una solución adecuada para este límite.
La detección es el aspecto más débil. Cada categoría se calcula después de que el cliente ya ha leído el mensaje. El evaluador cuenta las mentiras pero no impide ninguna de ellas.
La corrección estructural hace que sea imposible expresar la fabricación. Cuando la confirmación se genera a partir de un plantilla y el espacio del identificador solo puede llenarse con un objeto de resultado de herramienta, el modelo no tiene oportunidad de inventar un número. No se le prohíbe simplemente hacerlo; es incapaz, porque el valor nunca fluye a través del modelo:
# instead of: model writes the confirmation, evaluator checks it afterwards
result = create_order(...)
if result.ok:
reply = CONFIRM_TEMPLATE.render(order_id=result.order_id) # model never holds the pen
else:
reply = model.compose(FAILURE_CONTEXT) # nothing to fabricate
Para su propósito original, la mayor parte del evaluador resulta redundante, lo cual es el resultado deseado. Cambia de función: de ser un mecanismo de seguridad, pasa a ser el instrumento que indica si la verdadera protección sigue funcionando, además de suministrar etiquetas para el clasificador que elimina el léxico.
Las mismas piezas, una al lado de la otra
Dado que el diseño cuenta con varias partes móviles, una referencia compacta resulta útil. La regla de marcado de la versión uno, el predicado ingenuo cuyas deficiencias dieron lugar a este problema, dice lo siguiente:
flag ⟺ claim_regex.matches(reply) ∧ ¬ any(call.name == "create_order" for call in trace)
Los cinco resultados, nuevamente en orden y asociados a quién tiene la culpa:
no_claim ¬asserts(reply)
valid asserts ∧ ∃ create_order ∧ succeeded ∧ id ∈ result(create_order)
valid_status_ref asserts ∧ ¬create_order ∧ id ∈ result(lookup_tool)
claimed_but_failed asserts ∧ ∃ create_order ∧ ¬succeeded
phantom asserts ∧ id ∉ ⋃ result(t) for t in tools
La entrada de protección del clasificador:
def classify(reply, trace):
if not asserts_order_exists(reply):
return NO_CLAIM
El organismo que verifica los valores en relación con las pruebas del instrumento:
ids = extract_identifiers(reply) # candidates from the reply text
ids -= phone_number_shaped(ids) # local numbers collide with order ids creates = [c for c in trace if c.name == "create_order"]
lookups = [c for c in trace if c.name in LOOKUP_TOOLS] backed_by = {c: ids & identifiers_in(c.result) for c in creates + lookups} if creates and not any(succeeded(c) for c in creates):
return CLAIMED_BUT_FAILED
if any(backed_by[c] for c in creates):
return VALID
if any(backed_by[c] for c in lookups):
return VALID_STATUS_REF
return PHANTOM
El detector de afirmaciones protegidas:
asserts_order_exists(reply) :=
match(CLAIM_LEXICON, reply)
∧ ¬ match(NEGATION_CIRCUMFIX, window_around(match))
∧ ¬ match(FUTURE_CONDITIONAL, prefix_of(match))
Y el patrón de prevención, en el que el identificador fluye desde el resultado de la herramienta hacia un plantilla:
# instead of: model writes the confirmation, evaluator checks it afterwards
result = create_order(...)
if result.ok:
reply = CONFIRM_TEMPLATE.render(order_id=result.order_id) # model never holds the pen
else:
reply = model.compose(FAILURE_CONTEXT) # nothing to fabricate
Al leerlo en ese orden, la secuencia muestra todo el proceso: una regla barata, una partición, una verificación de valores, una protección específica para el lenguaje y, finalmente, un diseño en el que no puede ocurrir fallo alguno.
Cinco aspectos que resistieron
- Lea los registros en sí mismos. Omita el resumen elaborado por el modelo de una muestra y lea las conversaciones manualmente, con cifras poco claras. El descubrimiento más útil fue una regularidad en el formato de los identificadores fabricados que ningún resumen agregado podía revelar ni ninguna pregunta previa podía predecir.
- Divida los resultados según quien los corrige. Una métrica que combina defectos del agente y defectos en las mediciones se desintegra en cuanto el equipo responsable la examina.
Crear el evaluador fue la parte fácil. La verdadera tarea consistió en definir una respuesta correcta, en un lenguaje sin herramientas adecuadas, de manera lo suficientemente precisa como para que una máquina pudiera probarla y dos personas pudieran ponerse de acuerdo al respecto.