Evaluar agentes de voz por resultados: un banco de pruebas de tres capas
Las métricas de transcripción no detectan agentes de voz que actualizan el pedido equivocado o dejan escrituras obsoletas. Monta un banco de pruebas con tres capas, seis ejes y una puerta de release.
Imagina tres llamadas que en una revisión parecen perfectas. En la primera, una clienta corrige el número de pedido a mitad de frase y el agente le lee el número ya corregido. En la segunda, el cliente interrumpe mientras el agente habla y el agente se calla limpiamente, sin que nadie pise a nadie. En la tercera, el agente dice que el pedido se ha actualizado.
Ahora mira el backend. La primera llamada actualizó el pedido equivocado, justo aquel del que la clienta acababa de desmarcarse. En la segunda, la llamada a la herramienta que estaba en curso cuando llegó la interrupción siguió ejecutándose de todos modos. En la tercera, la actualización nunca llegó y el sistema conserva el valor antiguo.
Ninguna revisión de transcripciones marcaría estos casos. Tampoco un control de WER ni una rúbrica de calidad conversacional. Y, sin embargo, las tres fallaron en lo que de verdad importaba. El resto del artículo explica cómo montar un banco de pruebas que atrape toda esta clase de fallos antes de que lo haga un cliente.
Por qué no bastan las métricas de transcripción
Las mediciones habituales de un agente de voz son la tasa de error de palabras, la latencia, la rapidez con que cede ante una interrupción, la pronunciación y la naturalidad. Todas merecen seguimiento y ninguna es errónea. Pero responden a una pregunta más estrecha que la que plantea producción. Dicen si el agente sonó bien. No dicen nada sobre si de verdad pasó algo.
Un banco de pruebas que valga la pena tiene tres capas:
- Conversación: ¿el agente oyó y respondió correctamente?
- Ejecución: ¿se llamó a las herramientas adecuadas con los argumentos adecuados?
- Estado: ¿el cambio previsto se aplicó realmente en el sistema externo?
Apliquémoslo a las tres llamadas. La primera superó la capa de conversación y falló en la ejecución. La segunda también falló en la ejecución, pero por otro motivo: nunca se canceló una llamada obsoleta. La tercera superó conversación y ejecución y aun así falló en el estado, porque la escritura en el backend no se consolidó. Tres fallos en capas distintas y todos con el mismo síntoma: una transcripción que parecía correcta.
Primero, mapea lo que puede salir mal
No se debería puntuar nada hasta tener una matriz de modos de fallo. Cuatro categorías cubren la mayor parte del terreno.
Interrupciones: el cliente corta antes de que el agente termine de hablar, mientras se está armando una llamada a una herramienta, justo después de que esa llamada empiece a ejecutarse, y también las accidentales, provocadas por ruido de fondo que nunca debería haber detenido al agente.
Correcciones: un número de pedido distinto, una nueva fecha de entrega, una cantidad cambiada y la negación, como en «envíalo» seguido de «no, mejor no».
Fallos de herramientas: desde timeouts y resultados vacíos hasta una herramienta que trae el registro equivocado, errores 500 del backend, una respuesta de éxito que llega tarde y peticiones duplicadas por un reintento no idempotente.
Conflictos de estado: ocurren cuando conviven tres versiones de la verdad. La transcripción dice una cosa, el estado vivo otra y el payload que realmente se envió a la herramienta una tercera.
En la práctica se solapan. Una corrección que llega durante el reintento tras un fallo de herramienta es un escenario en sí mismo. Aun así, agruparlas de esta forma permite probar primero cada categoría por separado.
Cómo es un caso de prueba
Cada caso necesita un escenario de entrada, una descripción de lo que un sistema correcto debería hacer en los cuatro niveles y una estructura lo bastante rígida como para puntuarla automáticamente, sin que nadie tenga que leer y juzgar a mano. Un esquema mínimo se ve así:
{
"id": "corr-02",
"category": "correction",
"setup": {
"field": "order_number",
"initial_value": "5590",
"corrected_value": "5599",
"interrupt_stage": "during_tool_prep"
},
"expected": {
"captured": true,
"active_state_correct": true,
"tool_args_correct": true,
"stale_write_prevented": true,
"backend_correct": true,
"confirmation_matches_backend": true,
"should_execute": true
}
}
El bloque setup es flexible a propósito, porque las interrupciones y los fallos de herramientas necesitan campos distintos de los de las correcciones. El bloque expected es estricto. Cada caso se puntúa en los mismos seis ejes, y eso es lo que permite que 30 escenarios muy distintos se resuman en un único cuadro de resultados.
Los seis ejes
- Evento captado. ¿El sistema llegó a notar la corrección o la interrupción?
- Estado activo correcto. Tras el evento, ¿el estado de trabajo vivo se actualizó en consecuencia?
- Argumentos de herramienta correctos. ¿Los valores de la llamada a la herramienta eran actuales y coherentes?
- Escritura obsoleta evitada. ¿Se bloqueó toda acción basada en un estado desfasado?
- Backend correcto. ¿El resultado guardado responde a lo que el cliente quería de verdad?
- Confirmación igual al backend. ¿Lo que el agente le dijo al cliente es lo que realmente quedó guardado?
Hay una regla que pesa más que las demás: estos ejes nunca se promedian en un solo número. Un sistema que acierta en cinco ejes y falla en backend_correct ha producido un resultado equivocado, y cinco marcas verdes al lado no lo compensan. Un caso solo cuenta como aprobado de verdad, lo que llamamos End-to-End Task Integrity, cuando los seis se cumplen a la vez.
Cómo es una traza que aprueba
Tomemos un escenario en el que la corrección llega pronto: aparece antes de que exista ninguna llamada a herramientas, así que no hay nada obsoleto que cancelar.
Customer says 4821 → state_version = 4
Correction: 4281 → state_version = 5
Tool call built using version 5
Backend verified = 4281
Agent confirms 4281
Cada transición de estado lleva una etiqueta de versión, y la llamada a la herramienta solo se construye cuando hay una versión estable. Nunca se construye nada sobre la versión 4, así que luego no hay nada que desechar. Esa es la forma que tiene un caso que aprueba. No es «no hubo corrección». Es «la corrección se propagó por completo antes de que nada aguas abajo se confirmara».
Ejecutarlo contra un mock defectuoso
Para aterrizarlo, toma un banco de pruebas ejecutable (fixtures, esquema y scorer) y apúntalo a un pipeline simulado basado en reglas. El mock transcribe con precisión y gestiona bien los turnos de palabra, pero nunca invalida una llamada a herramienta ya construida o enviada, y nunca revisa un resultado de herramienta salvo que lo reconozca como malformado. Es exactamente el tipo de sistema que hay detrás de los fallos de arriba, solo que explicitado para que el banco tenga un objetivo.
La ejecución usó 30 llamadas sintéticas, repartidas a partes iguales entre escenarios de corrección, interrupción y fallo de herramienta.
- Evento captado: 24 de 30
- Estado activo correcto: 24 de 30
- Argumentos de herramienta correctos: 11 de 30
- Escritura obsoleta evitada: 8 de 30
- Backend correcto: 8 de 30
- Confirmación igual al backend: 7 de 30
- Integridad de la tarea de extremo a extremo: 7 de 30
La capa de conversación parece sólida: 24 de 30 llamadas captan el evento y actualizan el estado. A partir de ahí, ejecución y estado se desploman. Solo 8 llamadas evitaron una escritura obsoleta, y apenas 7 dieron una confirmación hablada que coincidía con lo que acabó en el backend. A juzgar solo por la calidad de la transcripción, el sistema parecería listo para producción. No lo está.
Convertir los fallos en una lista de arreglos
Un resultado en rojo solo sirve si señala qué capa hay que reparar. Al ordenar los 23 casos que no aprobaron según su causa, sale este desglose:
- 10 casos: los argumentos de la herramienta se construyeron con valores obsoletos
- 4 casos: se aceptó un resultado de herramienta defectuoso sin ninguna comprobación
- 3 casos: nunca se cancelaron llamadas que ya estaban en marcha
- 2 casos: el ruido de fondo se tomó por una interrupción del cliente
- 2 casos: un error del backend combinado con un reintento que no era seguro repetir
- 1 caso: la confirmación se dijo antes de que volviera un resultado asíncrono
- 1 caso: la acción no tenía clave de idempotencia
Diez de esos 23 fallos vienen de una sola causa. Una llamada a herramienta ya estaba construida o enviada cuando llegó una corrección o interrupción, y no había ningún mecanismo aguas abajo para cancelarla. Es un único agujero de arquitectura que aparece en diez escenarios, no diez bugs, y detectarlo es precisamente el sentido de ejecutar un banco de pruebas. Un simple «la llamada falló» no le da al equipo nada con lo que trabajar, mientras que «la construcción de argumentos falló en 10 de 23 casos» fija las prioridades del próximo sprint.
Una puerta de release para acciones con consecuencias
Antes de desplegar un cambio en un agente de voz que ejecuta acciones con consecuencias, «ha mejorado la calidad de la conversación» no es listón suficiente. La puerta debería decir:
- Ningún fallo por escritura obsoleta en las suites de corrección e interrupción
- Ninguna discrepancia entre la confirmación y el backend
- Reintentos de herramientas idempotentes
- Las acciones críticas se verifican contra el backend tras ejecutarse, en lugar de darlas por buenas según el valor que devuelve la herramienta
Un agente de voz solo está listo para producción cuando la conversación, la traza de herramientas y el estado del backend coinciden en lo que pasó. La mayoría de los sistemas actuales no pueden afirmarlo, y la forma de averiguarlo es dejar de puntuar la transcripción y empezar a puntuar el resultado.