Appearance
Experimentos A/B de la voz IA y alertas de deriva
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_iddebe 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 esopenai/gpt-oss-20b,openai/gpt-oss-120b,llama-3.3-70b-versatileyllama-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}/experimentslista todos los estados, el más nuevo primero, con las variantes en orden deposition.GET /v1/ai/agents/{id}/experiments/{experiment_id}devuelve un experimento.PATCH /v1/ai/agents/{id}/experiments/{experiment_id}aceptanamey/ovariants; reemplazarvariantsreemplaza el conjunto entero.primary_metricsólo puede repetirse, no cambiarse.DELETE /v1/ai/agents/{id}/experiments/{experiment_id}borra sólo un borrador y devuelve204.
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 varianteassigned_units,conversations,closed,valid,failed,ptp,handoffs,contained,violatingy las tasasptp_rate,contained_rate,handoff_rate,failed_rate,compliance_violation_rateyprovider_error_rate(cada unanullcuando su denominador es cero). Una conversación válida es una cerrada cuyo desenlace no esfailednivoicemail; las tasas de las métricas principales son sobre conversaciones válidas, yfailed_rateycompliance_violation_ratesobre todas las cerradas.metrics_coverage(with_metricssobreclosed) 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_p95llevan,mean,variance,p50_meanyp95.consumptionllevaminutes_totalyminutes_per_call.cost.statusespriced,partialounpriced, connyunpriced_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_conversationses 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_floordamin_n,min_successes(nullpara una media) ymet. Unreasondeunpricedsignifica que la métrica principal escost_per_cally 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_rateycontained_ratemejoran al subir;handoff_rate,first_audio_p95_msycost_per_callmejoran al bajar. winnerllevavariant_id,versus_variant_id, elp_valueajustado,difference,ci_lowyci_high;comparisons[]tiene una entrada por retador. Concost_per_callel veredicto conserva su significancia y su ganador pero no las magnitudes (value,difference,ci_lowyci_highno vienen).allocation_warningestruecuando el reparto observado de unidades se desvía de los pesos (chi-cuadrado, alfa 0.001). Mientras seatrueno se emitewinner, yverdict.withheld_byesallocation_warningcuando 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 superabaseline_mean + k·max(baseline_stddev, stddev_floor), conk = 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.