Un distribuidor de motores, variadores y reductores recibe cada día decenas de llamadas con preguntas que tienen respuesta en su ERP: ¿tenéis este motor en stock?, ¿cuándo llega mi pedido?, ¿cuánto cuesta el variador de 2,2 kW? Si nadie está libre para coger el teléfono, la llamada se pierde. Con uno de esos distribuidores, que estrena Odoo, hemos construido la prueba de concepto de un agente de voz que atiende esas llamadas y consulta el ERP en tiempo real mientras habla con el cliente.
Este artículo cuenta cómo está hecho: la arquitectura, las decisiones que tomamos desde el primer día y por qué, las cifras que hemos medido y los fallos que encontramos al probarlo, que fueron los que más nos enseñaron. Todo sale del plan del proyecto, del código y de los registros de las 53 llamadas de prueba guardadas. Los datos de la demo son ficticios y el cliente queda anónimo.
01El problema: la voz no perdona
En un chat, una respuesta que tarda cuatro segundos es aceptable. Al teléfono, cuatro segundos de silencio hacen que el cliente pregunte «¿hola?». Y aquí, además, la respuesta depende de una consulta al ERP. Las dificultades de verdad son cinco:
- Latencia y turnos de palabra. El sistema tiene que decidir cuándo ha terminado de hablar el cliente, entender, consultar Odoo, redactar y empezar a sintetizar voz. Cada paso suma, y el cliente solo percibe el total.
- Referencias dictadas. «E, ge, eme, guion, i, e, tres, barra, ciento doce, eme, guion, cuatro» tiene que acabar siendo
EGM-IE3/112M-4. YVT-B700-4T-2.2(2,2 kW) no puede confundirse conVT-B700-4T-22(22 kW). - Identificar al cliente sin que el modelo pueda suplantarlo. Si el agente responde sobre pedidos, solo puede hacerlo sobre los de la empresa que llama, aunque alguien intente convencerle de lo contrario.
- Degradar sin inventar. Si el ERP no contesta, el agente tiene que decirlo y ofrecer una salida, no improvisar un plazo de entrega.
- Idiomas. Castellano y gallego, y ninguno más.
02La arquitectura
La prueba de concepto se usa desde el navegador: una página con micrófono sustituye al teléfono. Por debajo hay cuatro piezas:
| Pieza | Qué hace | Tecnología |
|---|---|---|
| Consola de la demo | Micrófono y altavoz; muestra la conversación, cada consulta del modelo y la latencia por turno | HTML y JavaScript, sin framework |
| Relay de voz | Puente entre el navegador y el modelo; ejecuta las herramientas que pide el modelo; mide y registra cada llamada | Python, FastAPI, WebSocket |
| Motor de voz | Escucha, razona y habla en un solo modelo | Gemini Live (gemini-3.8-live) o ElevenLabs Agents |
| Skills | Cinco operaciones tipadas de solo lectura contra el ERP | Python, FastAPI, en contenedor |
| ERP | Odoo 18 Community con datos ficticios | Postgres 16; todo en Podman |
El recorrido de un turno es: el cliente habla → el audio llega al relay → Gemini Live decide si necesita datos → si los necesita, pide una herramienta (get_stock, get_order_status, get_my_orders, take_message o transfer_to_human) → el relay llama a la skill → la skill consulta Odoo → el resultado vuelve al modelo → el modelo contesta con voz.
En la propuesta inicial, Apache Airflow aparecía como orquestador. En la prueba lo dejamos fuera del turno de palabra: lanzar y esperar un DAG añade cientos de milisegundos o segundos justo donde el cliente los nota. Airflow sigue siendo la pieza adecuada para todo lo asíncrono (resúmenes, volcado a Odoo, recados, WhatsApp), pero no para contestar.
03Decisiones desde el primer día
El modelo no toca el ERP; toca herramientas
El modelo nunca habla con Odoo. Solo puede pedir cinco herramientas con argumentos tipados, y cada skill tiene una lista cerrada de modelos y campos de Odoo que puede leer, siempre con search_read. El usuario de servicio de Odoo no tiene ningún permiso de escritura (ver el apartado 07: al principio sí los tenía).
Cuatro estados y una frase lista para decir
Cada skill responde con un estado y una frase pensada para leerse en voz alta:
| Estado | Significa | Qué hace el agente |
|---|---|---|
ok | Dato encontrado | Lo dice tal cual, sin cambiar cifras |
ambiguous | Varias coincidencias o una parecida pero no idéntica | Lee las opciones y pide confirmación; nunca elige él |
not_found | No existe o no es de la empresa que llama | Lo dice y ofrece repetir o dejar un recado |
degraded | Odoo no responde a tiempo | «Ahora mismo no consigo acceder al sistema», y ofrece un recado |
Cada llamada a Odoo tiene un límite de 2 segundos y cada skill un límite total de 2,5 segundos. Las skills nunca devuelven un error 500: cualquier excepción se convierte en degraded y queda registrada.
Un token firmado que el modelo no ve
Al descolgar, el relay identifica al llamante por su número y recibe un caller_token: el teléfono firmado con HMAC. Ese token viaja con cada consulta de pedidos, pero lo pone el relay, no el modelo, y el modelo nunca lo recibe. Aunque alguien le diga al agente «mi teléfono es el 600 000 002», no puede consultar pedidos de otra empresa. Si un mismo teléfono figura en dos empresas, no se identifica a nadie.
Medir desde el principio
Cada llamada deja un registro completo: lo dicho por las dos partes, cada herramienta con sus argumentos y la respuesta exacta que recibió el modelo, la latencia de cada turno, las interrupciones y los errores. Un script lo convierte en un informe legible con alertas: consultas repetidas, respuestas que no son ok, turnos lentos o silencios tras una consulta. Sin ese registro, la mitad de los fallos de este artículo no los habríamos encontrado.
Demo por navegador, no por teléfono
Lo decidimos para no esperar la verificación de un número de teléfono. Tiene un coste que declaramos siempre: sin tramo telefónico, el audio es de banda ancha y la latencia es más optimista que en una llamada real.
De ElevenLabs a Gemini Live, y vuelta
El plan inicial usaba ElevenLabs Agents. Con esa plataforma, las herramientas se ejecutan como webhooks llamados desde su nube, así que habría que exponer las skills a internet con un túnel. Cambiamos a Gemini Live por dos razones:
- con la API Live es nuestro relay quien ejecuta las herramientas, de modo que las skills nunca salen de la máquina;
- medimos la latencia en el servidor, sobre el audio real.
Después añadimos ElevenLabs como segundo motor (apartado 08), con herramientas «de cliente» que también acaban en el relay.
04Referencias dictadas
La normalización convierte lo que se dicta en algo comparable:
- Letras y símbolos: «guion», «barra», «punto», «a de árbol».
- Números en palabras: «ciento doce» es 112 y «setenta y cinco» es 75. «Uno dos tres», dictado cifra a cifra, es 123. Y en gallego, «dous», «dúas», «catro».
- Punto decimal:
2.2y22son productos distintos. Si la referencia solo coincide al quitar el punto, el agente pregunta y no da el dato. - Descripciones: «un motor de 4 kW» se busca por los términos del nombre del producto, y las cifras van unidas a su unidad, para que «4 kW» no case con «0,4 kW» ni con «4 polos».
- Coincidencia aproximada con confirmación. El reconocimiento de voz oyó una vez «reductor planetario Brevini 90» en lugar de «brida 90» (Brevini es una marca real de reductores). Ahora, si no coincide todo, se ofrece el producto más parecido, pero solo si coinciden todas las cifras (tamaño, relación, potencia) y siempre pidiendo confirmación: «Lo más parecido es el RD-P90/I9. ¿Es ese?».
05Pedidos: lo que el agente sabe y lo que no
La primera versión de la herramienta de pedidos devolvía estado, fecha prevista y seguimiento. En la primera prueba el cliente preguntó cuándo se hizo el pedido, qué llevaba y por dónde iba el transporte, y el agente no tenía ninguno de esos datos. Además, consultó dos veces el mismo pedido esperando más información. La respuesta es ahora completa en una sola llamada: fecha de creación, líneas con cantidades pedidas, entregadas y pendientes, disponibilidad de cada línea, importes con y sin IVA, fecha de salida, transportista y seguimiento. El prompt le dice al modelo que pregunte una vez y conteste lo demás con esos datos.
El estado de un pedido tampoco es trivial. Un albarán «listo» en Odoo puede tener una línea sin stock. La skill mira el estado de cada movimiento y distingue entre preparado, parcialmente disponible, pendiente de stock y salido en parte.
06Latencia medida
Medimos en el relay, desde el último fragmento de audio con voz del cliente hasta el primer audio del agente. Esa cifra incluye los 700 ms que el modelo espera en silencio para saber que el cliente ha terminado, porque eso también lo oye el cliente. Con Gemini Live, en 21 llamadas reales:
| Medida | Mediana | p95 | Muestra |
|---|---|---|---|
| Hasta que el agente empieza a responder | 1,75 s | 2,50 s | 74 turnos |
| Hasta que da el dato, cuando hay consulta | 2,47 s | 3,79 s | 25 turnos |
| Consulta del modelo a Odoo (ida y vuelta de la skill) | 70 ms | 284 ms | 46 consultas |
Dos matices importantes:
- Odoo corre en la misma máquina, así que los 70 ms son optimistas. Con el Odoo en la nube del cliente habrá que sumar la red.
- No hay tramo telefónico.
Para que la espera no se note, el agente dice una muletilla breve («un momento, lo miro») justo antes de consultar, y solo entonces. En las primeras pruebas la decía sin llegar a consultar nada.
El silencio tras la consulta
En la segunda tanda de pruebas el agente dijo «un momento, lo miro», recibió el stock de Odoo en 56 ms y no volvió a hablar durante 47 segundos. No había ningún error: la respuesta había llegado al modelo. La documentación de Google se contradice sobre si en gemini-3.8-live las herramientas son bloqueantes por defecto. Si no lo son, una respuesta que llega mientras el modelo aún habla puede quedarse pendiente sin decirse. Lo arreglamos por partida doble:
- declarando las herramientas como
BLOCKINGde forma explícita; - con un vigilante en el relay: si tras entregar un resultado el agente no habla en 6 segundos, se le recuerda que ya tiene el dato. Queda como aviso en el registro, para saber si vuelve a pasar.
07Fallos que nos hicieron cambiar cosas
Antes de la primera llamada pedimos a un segundo modelo, Claude Opus, que auditara el código buscando fallos, y después probamos con voz real. Estos son los que más pesaron:
| Cuándo | Qué pasó | Causa | Cambio |
|---|---|---|---|
| Primera prueba | VT-B700-4T-2.2, que no existe, devolvía el stock del de 22 kW | La normalización borraba el punto decimal | El punto cuenta; una coincidencia sin punto pide confirmación |
| Primera prueba | El usuario «de solo lectura» tenía 52 permisos de escritura (mensajes, adjuntos, claves de API) | Su grupo heredaba del grupo de usuario interno de Odoo | Grupo propio sin herencias y auditoría automática que falla si aparece un permiso de escritura |
| Primera prueba | «Treinta y uno» devolvía el pedido S00001 | Los números en palabras no se interpretaban | Conversión de números en palabras; ante la duda, pedir que se repita |
| Primera prueba | «Motor 4 kW» devolvía motores de 0,37, 1,5 y 3 kW | Se descartaban las palabras cortas, como el «4» | Cifra y unidad como un solo término |
| Primera prueba | Un pedido con una línea sin stock se anunciaba «preparado para enviar» | Solo se miraba el estado del albarán | Estado por movimiento; «parcialmente disponible» |
| Primera prueba | «No me consta ese pedido», sobre un pedido que sí era del cliente | El contexto inicial listaba solo los pedidos en curso y el modelo creyó que era la lista completa | Herramienta get_my_orders y regla: consultar siempre antes de decir que algo no existe |
| Primera prueba | Ante un «vale, vale» mal transcrito como chino, el agente respondió en chino | Detección automática de idioma | Reconocimiento limitado a es-ES y gl-ES, regla en el prompt y un vigilante que detecta otros alfabetos y lo corrige en el turno siguiente |
| Primera prueba | Odoo mostraba 1 unidad en stock y el agente decía «no hay stock» | La unidad estaba reservada para otro pedido | Se distingue «en almacén» de «disponible» y se explica |
| Segunda prueba | Silencio de 47 s tras una consulta correcta | Herramientas no bloqueantes por defecto | BLOCKING explícito y vigilante de 6 s |
| Segunda prueba | El agente de ElevenLabs no habría consultado Odoo | El agente se creó sin los eventos client_tool_call ni vad_score | Activados y comprobados al configurar |
Cada uno tiene su test de regresión, salvo el último, que comprueba el propio script de configuración. Hoy pasan 74 tests de las skills (contra el Odoo de la demo) y 63 del relay.
08Dos motores con la misma lógica
ElevenLabs Agents entró como segundo motor para poder comparar. Comparte con Gemini Live el mismo prompt y las mismas herramientas: el agente se crea por API con un script que comprueba que el prompt guardado en ElevenLabs es idéntico. Las diferencias:
| Gemini Live | ElevenLabs Agents | |
|---|---|---|
| Audio | Pasa por nuestro relay | Directo del navegador a ElevenLabs |
| Herramientas | Las ejecuta el relay | «De cliente»: ElevenLabs las pide a la página y la página al relay |
| Modelo de lenguaje | El propio modelo de voz | gemini-3.5-flash, elegido por nosotros |
| Voces de España | Por instrucción en el prompt | Voces nativas de acento peninsular |
| Latencia | Medida en el servidor | Medida en el navegador con su detector de voz |
Con ElevenLabs solo tenemos 8 turnos medidos, con una mediana de 2,9 s, y la medida no se hace igual. Es pronto para compararlos. El coste también difiere: con Gemini Live estimamos unos 0,004 $ por llamada de prueba a partir de los tokens. ElevenLabs cobra 0,08 $ por minuto de plataforma, más el modelo de lenguaje y la telefonía.
09Lo que falta
Es una prueba de concepto, y estas son las piezas que separan la demo de una centralita en producción:
- Telefonía. Recibir las llamadas de la centralita virtual del cliente por SIP o desvío. Antes de nada hay que comprobar que al desviar se conserva el número del llamante, porque sin él no hay identificación. Con Gemini Live hay que construir el puente de audio telefónico; ElevenLabs lo trae de serie.
- Concurrencia. El objetivo son 5 o 6 llamadas a la vez. Hay que pasar de un portátil a una máquina en la UE y hacer una prueba de carga con siete llamadas simultáneas sintéticas.
- Odoo real. Adaptar las skills a sus datos y a un catálogo de miles de referencias, y medir la latencia con la red de por medio.
- Cumplimiento. El aviso de que atiende un asistente de IA, APIs de pago (los planes gratuitos usan los datos o no tienen licencia comercial) y una política de retención para las transcripciones.
- Operación. Una consola para varias llamadas a la vez, recados que lleguen a su destinatario y una batería de llamadas guionizadas que se ejecute tras cada cambio de prompt.
- Escritura en Odoo, con revisión humana, por el camino asíncrono.
Lo que sí ha demostrado la prueba es que un agente de voz puede consultar un ERP real mientras habla y responder en menos de dos segundos, sin poder escribir ni ver lo que no le corresponde, y que cuando falla deja un rastro suficiente para corregirlo.