Skip to content

Experimentos A/B de la voz IA y alertas de deriva ​

English: Voice AI A/B experiments and drift alerts.

Un experimento compara de dos a cinco variantes de un mismo agente de IA en estado live: su voz, un párrafo de estilo, su modelo y sus ajustes de razonamiento. Cada llamada se asigna a una variante con una regla fija, su desenlace se atribuye a esa variante y core publica un veredicto calculado desde el libro. Junto a él, un evaluador de deriva vigila las mismas métricas en cubos de una hora y avisa cuando una variante o un modelo se sale de su banda. Las dos cosas están siempre encendidas: no hay ninguna bandera de tenant que las apague. Un agente sin experimento en marcha se comporta exactamente como antes.

La guía hermana para campañas es Experimentos A/B de campaña; las rutas de abajo siguen su misma forma. Las rutas, los cuerpos, los envoltorios y los códigos de estado están copiados de los esquemas AiAgentExperiment* y AiDrift* y de las operaciones listAiAgentExperiments, createAiAgentExperiment, getAiAgentExperiment, updateAiAgentExperiment, deleteAiAgentExperiment, startAiAgentExperiment, stopAiAgentExperiment, getAiAgentExperimentResults y listAiAgentDrift del openapi/dialer-v1.yaml de core.

Quién puede hacer qué ​

Las escrituras (POST, PATCH, DELETE, start, stop) las hace una llave dd_ o un inicio de sesión del panel con el rol account_admin. Un inicio de sesión member recibe 403 role_forbidden. Las lecturas (listar, leer, resultados, deriva) están abiertas a cualquier token con alcance de tenant. Los recursos de otro tenant y los identificadores desconocidos devuelven 404.

Crear un borrador ​

POST /v1/ai/agents/{id}/experiments acepta name, primary_metric y variants. primary_metric es una de ptp_rate, contained_rate, handoff_rate, first_audio_p95_ms o cost_per_call, y después no se puede cambiar. El arreglo variants tiene de 2 a 5 entradas, cada una con un name, un weight de 1 a 1,000 y exactamente una entrada con is_control: true. El control es el agente base, así que no envía ajustes. El agente debe estar live; cualquier otro agente da 409.

bash
curl -sS -X POST "$API/v1/ai/agents/$AGENT_ID/experiments" \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Model and tone",
    "primary_metric": "handoff_rate",
    "variants": [
      {"name": "Control", "weight": 50, "is_control": true, "overrides": {}},
      {"name": "Larger model, direct tone", "weight": 50, "overrides": {
        "model": "openai/gpt-oss-120b",
        "prompt_style": "Be direct and brief; confirm the amount before proposing a plan."
      }}
    ]
  }'

La respuesta es 201 con { "experiment": ... }, en estado draft. Un cuerpo mal formado es 400. Un experimento ilegal es 422: cada campo rechazado vuelve en una sola respuesta, con su ruta en error.details, y no se guarda nada (createAiAgentExperiment).

Qué puede cambiar una variante ​

El objeto overrides es una lista cerrada. Sólo se aceptan estas claves:

  • voice: { "voice_id": "...", "pace": 0..1 }, al menos una de las dos. voice_id debe estar en el catálogo de voces (GET /v1/ai/voices). El aviso se pre-renderiza con la voz de cada variante antes de marcar;
  • prompt_style: una línea (sin salto de línea) de 1 a 600 caracteres, que se añade después de todo lo que el agente compone. No puede sustituir nada de ello;
  • model: un modelo del catálogo de modelos de la plataforma. En la especificación vendorizada el catálogo es openai/gpt-oss-20b, openai/gpt-oss-120b, llama-3.3-70b-versatile y llama-3.1-8b-instant;
  • llm: { "reasoning_effort": "low" | "medium" | "high", "prompt_form": "compact" | "full" }, al menos una de las dos.

Cualquier otra clave se rechaza en vez de ignorarse, con un 422:

json
{
  "error": {
    "code": "unprocessable",
    "message": "variants[1].overrides has keys outside the closed list: disclosure; allowed: voice, prompt_style, model, llm",
    "details": [
      { "path": "variants[1].overrides", "message": "has keys outside the closed list: disclosure; allowed: voice, prompt_style, model, llm" }
    ]
  }
}

Lo que nunca varía ​

Nada legal está en la lista. Esto es igual en todas las variantes y no se puede poner en un experimento: el disclosure (texto, bloqueo y audio pre-renderizado), la baja hablada, las handoff_rules incluida la regla de «quiero hablar con una persona», la policy y sus tools, los límites de negociación (por ejemplo max_settlement_pct) y las frases prohibidas. Los gates de cumplimiento de la llamada corren antes de resolver la variante y nunca se enteran del experimento: una llamada bloqueada por un gate no crea conversación y no cuenta como exposición de ninguna variante.

Consultar, editar y borrar ​

  • GET /v1/ai/agents/{id}/experiments lista todos los estados, el más nuevo primero, con las variantes en orden de position.
  • GET /v1/ai/agents/{id}/experiments/{experiment_id} devuelve un experimento.
  • PATCH /v1/ai/agents/{id}/experiments/{experiment_id} acepta name y/o variants; reemplazar variants reemplaza el conjunto entero. primary_metric sólo puede repetirse, no cambiarse.
  • DELETE /v1/ai/agents/{id}/experiments/{experiment_id} borra sólo un borrador y devuelve 204.

Cuando un experimento está en marcha, o terminó, sus variantes quedan congeladas: editarlo o borrarlo es 409. Para probar otro estilo, otra voz u otro modelo, crea otro experimento.

Arrancar y terminar ​

bash
curl -sS -X POST \
  "$API/v1/ai/agents/$AGENT_ID/experiments/$EXPERIMENT_ID/start" \
  -H "Authorization: Bearer $TOKEN"

POST .../start pasa de draft a running y vuelve a validar las variantes contra los catálogos de voces y de modelos tal como están ahora: es 422 con details si un modelo salió del catálogo desde que se escribió el borrador. Es 409 si el agente no está live, si el agente ya tiene otro experimento en marcha o si el experimento no es un borrador.

bash
curl -sS -X POST \
  "$API/v1/ai/agents/$AGENT_ID/experiments/$EXPERIMENT_ID/stop" \
  -H "Authorization: Bearer $TOKEN"

POST .../stop pasa de running al terminal ended, y el agente vuelve a responder como agente base; cualquier otro estado es 409. ended_reason es null en una parada manual; cuando core termina el experimento por su cuenta (ver la parada por cumplimiento) dice por qué.

Asignación y atribución ​

Core asigna cada unidad a una variante con la misma función pura que los experimentos de campaña, sobre los pesos de las variantes en orden de position. La unidad es la deuda de la llamada cuando la tiene y la propia llamada cuando no, así que la misma unidad recibe la misma variante mientras el experimento corre. A una deuda que ya tiene un intento de llamada sellado se le sirve la variante de ese intento. La conversación guarda su experimento y su variante al abrirse y nunca se reasigna. Una variante que no se puede resolver (un ajuste que ya no valida, un modelo retirado del catálogo) no se sirve: la llamada usa el agente base.

Resultados y veredicto ​

GET /v1/ai/agents/{id}/experiments/{experiment_id}/results devuelve { "results": ... }. Se recalcula desde el libro en cada lectura (sin contadores) y leer nunca cambia el experimento. Los errores son 401, 404 y 429.

bash
curl -sS \
  "$API/v1/ai/agents/$AGENT_ID/experiments/$EXPERIMENT_ID/results" \
  -H "Authorization: Bearer $TOKEN"
  • variants[]: por variante assigned_units, conversations, closed, valid, failed, ptp, handoffs, contained, violating y las tasas ptp_rate, contained_rate, handoff_rate, failed_rate, compliance_violation_rate y provider_error_rate (cada una null cuando su denominador es cero). Una conversación válida es una cerrada cuyo desenlace no es failed ni voicemail; las tasas de las métricas principales son sobre conversaciones válidas, y failed_rate y compliance_violation_rate sobre todas las cerradas.
  • metrics_coverage (with_metrics sobre closed) dice cuántas conversaciones cerradas publicaron métricas por llamada: un runtime que aún no publica se ve aquí y no como un cero limpio.
  • first_audio_ms_p95 lleva n, mean, variance, p50_mean y p95.
  • consumption lleva minutes_total y minutes_per_call.
  • cost.status es priced, partial o unpriced, con n y unpriced_calls. El importe del coste de proveedor nunca se publica: es coste de la plataforma, no un precio, y el tenant paga por minuto.
  • unattributed_conversations es distinto de cero cuando el sello de una conversación y el experimento no coinciden.

El veredicto ​

verdict.status es insufficient_data, no_significant_difference o winner.

  • insufficient_data: algún brazo tiene menos de 100 conversaciones válidas o menos de 10 éxitos en una tasa, o menos de 100 llamadas con la medida en una media. verdict.sample_floor da min_n, min_successes (null para una media) y met. Un reason de unpriced significa que la métrica principal es cost_per_call y algún brazo tiene una llamada sin tarifa.
  • Las tasas usan la prueba z de dos proporciones, las medias (latencia, coste) la prueba de Welch en aproximación normal; las dos son bilaterales, alfa 0.05, con ajuste de Bonferroni sobre los retadores. ptp_rate y contained_rate mejoran al subir; handoff_rate, first_audio_p95_ms y cost_per_call mejoran al bajar.
  • winner lleva variant_id, versus_variant_id, el p_value ajustado, difference, ci_low y ci_high; comparisons[] tiene una entrada por retador. Con cost_per_call el veredicto conserva su significancia y su ganador pero no las magnitudes (value, difference, ci_low y ci_high no vienen).
  • allocation_warning es true cuando el reparto observado de unidades se desvía de los pesos (chi-cuadrado, alfa 0.001). Mientras sea true no se emite winner, y verdict.withheld_by es allocation_warning cuando se retuvo un ganador.

La parada por cumplimiento ​

safety.breaches[] lista las variantes cuya compliance_violation_rate es significativamente mayor que la del control (prueba z, alfa 0.05, al menos 100 conversaciones cerradas en los dos brazos). Cuando ocurre, core termina el experimento por su cuenta, escribe la evidencia en ended_reason y el agente vuelve a su configuración base. Esta parada no tiene interruptor.

Alertas de deriva ​

El evaluador de deriva corre para todos los tenants cada 15 minutos y no cambia nada por sí solo: no pausa un agente, no termina un experimento y no toca un gate. Lee cubos de una hora (UTC, por la hora en que cerró la llamada) por ámbito, (agente, variante | ninguna) y (modelo), para cinco métricas (AiDriftMetric): first_audio_p95_ms, handoff_rate, provider_error_rate, failed_rate y cost_per_call. El ámbito model cuenta todas las llamadas que usaron ese modelo, con o sin variante, así que una degradación del proveedor se ve aunque ninguna variante haya cambiado. Todas las métricas alertan sólo hacia arriba.

Dos reglas las juzgan (rule):

  • zscore.v1: una banda móvil sobre los 7 días anteriores, con al menos 24 cubos de base y 30 llamadas en el cubo. Alerta cuando el cubo supera baseline_mean + k·max(baseline_stddev, stddev_floor), con k = 3.0.
  • budget.v1: el p95 del primer audio del cubo por encima del presupuesto de latencia, 900 ms. Una banda móvil no ve una degradación lenta que ya es el nuevo normal; el presupuesto sí.
bash
curl -sS "$API/v1/ai/agents/drift?state=open&limit=50" \
  -H "Authorization: Bearer $TOKEN"

GET /v1/ai/agents/drift responde { "drift": [...], "coverage": [...], "next_cursor": ... }, el más nuevo primero. Filtra por agent_id, variant_id (un id de variante, o base para el agente sin variante), model, metric y state (open o cleared); limit es 50 por defecto y 200 como máximo, y cursor recibe el next_cursor anterior (null en la última página). model no se puede combinar con agent_id ni con variant_id (400). Los errores son 400, 401 y 403.

Cada entrada de drift[] (AiDriftEvent) es su fila opened con la evidencia con que se escribió: observed, baseline_mean, baseline_stddev, stddev_floor, threshold, k, sample_n, baseline_n y la series que miró la regla, de la más antigua a la más reciente. En budget.v1 los campos de base son null. Una fila cleared posterior que cita la alerta pone state en cleared y rellena resolution.

coverage[] (AiDriftCoverage) dice qué se está juzgando: una serie cuyo último cubo tuvo menos de min_sample llamadas es insufficient_sample, y una sin min_baseline cubos de base es insufficient_baseline. Una lista drift vacía para una serie que no está ok significa «aún no hay datos suficientes», nunca «todo bien».

El evento ai.drift.detected ​

Core también anuncia cada alerta abierta por los webhooks de salida, con el evento ai.drift.detected. data es el AiDriftEvent de arriba sin resolution (AiDriftDetectedData), con state siempre open. Se envía una vez por alerta abierta; el cierre no se anuncia, así que léelo con GET /v1/ai/agents/drift?state=cleared. Como en cualquier evento, deduplica por el event_id del cuerpo firmado.

Límites declarados ​

  • Un párrafo de estilo no se puede demostrar jurídicamente neutro con una regla léxica. Lo acotan las guardas deterministas que corren por encima del modelo y no leen el estilo, y la parada por cumplimiento de arriba.
  • Una persona con dos deudas puede oír dos variantes (la unidad es la deuda); en una llamada entrante la unidad es la llamada, así que quien vuelve a llamar puede caer en otra variante.
  • La prueba es de horizonte fijo: no hay ganador anticipado.
  • El coste de proveedor sólo existe donde el administrador de la plataforma cargó una tarifa; sin ella el coste es unpriced.
  • La deriva se ve por sus efectos (latencia, traspasos, errores, coste), no comparando el sentido de las respuestas del modelo.

Nothing in these docs is legal advice — always confirm compliance posture with your own counsel.