Introducción
Recientemente construí una recepcionista con IA 24/7 para una empresa de plomería que ahora maneja más de 200 llamadas por mes. El dueño pasó de perder el 40% de las llamadas a capturar el 98%. Tiempo total de configuración: 3 horas.
Esta guía le enseña cómo construir agentes de voz con IA listos para producción usando Vapi.ai, la plataforma que hace que el desarrollo de voz con IA sea realmente viable para equipos pequeños.
Al final, comprenderá:
- La arquitectura STT → LLM → TTS
- Cómo configurar Vapi con llamada a funciones apropiada
- Patrones de producción para manejo de errores
- Estrategias de optimización de costos
- Patrones reales de despliegue que utilizo
Comencemos.
La Arquitectura de Voz con IA
Antes de escribir código, entienda el pipeline:
Arquitectura del Pipeline de Voz con IA
Cómo la voz con IA transforma el habla en respuestas inteligentes
El Flujo:
- Speech-to-Text (STT): Vapi usa Deepgram o Whisper para transcribir el habla
- Procesamiento LLM: OpenAI, Claude o su modelo personalizado procesa la intención
- Llamada a Funciones: Sus endpoints de backend manejan la lógica de negocio
- Text-to-Speech (TTS): ElevenLabs u OpenAI convierte la respuesta a voz
- Respuesta: El que llama escucha al IA hablar
¿Por qué Vapi?
- Maneja todas las partes difíciles (latencia, interrupciones, detección de habla)
- Tiempos de respuesta de menos de 1 segundo
- Llamada a funciones integrada
- API simple
- ~$0.05-0.10 por minuto (vs construirlo usted mismo: $50k+ en tiempo de desarrollo)
Requisitos Previos
Antes de comenzar, necesitará:
- Cuenta de Vapi (nivel gratuito disponible)
- Cuenta de Twilio (para números telefónicos)
- Clave de API de OpenAI (o Anthropic para Claude)
- Conocimiento básico de TypeScript/JavaScript
- Endpoint de backend (puede ser Vercel, Railway o su servidor)
Tiempo requerido: 2-4 horas para el primer agente.
Paso 1: Configurando su Primer Asistente
Crear el Asistente
// lib/vapi.ts
import { VapiClient } from "@vapi-ai/server-sdk";
const client = new VapiClient({ token: process.env.VAPI_API_KEY });
export async function createAssistant() {
const assistant = await client.assistants.create({
name: "Plumbing Receptionist",
model: {
provider: "openai",
model: "gpt-4",
temperature: 0.7,
systemPrompt: `You are a professional receptionist for a plumbing company.
Your job:
- Greet callers warmly
- Collect their name, address, and issue description
- Determine if it's an emergency
- Schedule appointments for non-emergencies
- Capture lead information
Rules:
- Be concise (under 30 seconds per response)
- Always confirm details back to the caller
- If emergency, collect contact number and say a plumber will call within 15 minutes
- Never say "I don't know", offer to have a human call back`,
},
voice: {
provider: "elevenlabs",
voiceId: "21m00Tcm4TlvDq8ikWAM", // Rachel
},
firstMessage: "Hello! Thanks for calling. I'm here to help with your plumbing needs. What can I assist you with today?",
});
return assistant;
}Explicación de la Configuración Clave
Selección de Modelo:
gpt-4: Mejor calidad, mayor latencia (~1.5s), $0.03/1K tokensgpt-3.5-turbo: Más rápido (~0.8s), más económico, bueno para agentes simplesclaude-3-sonnet: Excelente para razonamiento complejo
Selección de Voz:
- Las voces de ElevenLabs suenan más naturales
- Las voces de OpenAI son más económicas pero más robóticas
- Pruebe múltiples voces, importa más de lo que piensa
Consejos para el System Prompt:
- Sea específico sobre el tono y la extensión
- Incluya ejemplos de lo que NO decir
- Agregue barandillas para casos extremos
- Manténgalo por debajo de 2000 tokens
Paso 2: Agregando Llamada a Funciones
Las funciones permiten que su IA interactúe con sus sistemas. Aquí hay una función de captura de leads:
// lib/vapi-functions.ts
interface LeadCaptureParams {
name: string;
phone: string;
address: string;
issue: string;
isEmergency: boolean;
}
export async function captureLead(params: LeadCaptureParams) {
// Save to your CRM (Airtable, HubSpot, etc.)
await saveToCRM({
...params,
source: "voice-agent",
timestamp: new Date().toISOString(),
});
// Send notification
if (params.isEmergency) {
await sendEmergencyAlert(params);
}
return {
success: true,
message: params.isEmergency
? "Emergency logged. A plumber will call you within 15 minutes."
: "Great! I've scheduled you for tomorrow between 2-4 PM. You'll get a confirmation text.",
};
}
// Vapi function schema
export const captureLeadSchema = {
name: "captureLead",
description: "Capture lead information and schedule appointment",
parameters: {
type: "object",
properties: {
name: {
type: "string",
description: "Caller's full name",
},
phone: {
type: "string",
description: "Caller's phone number",
},
address: {
type: "string",
description: "Service address",
},
issue: {
type: "string",
description: "Brief description of plumbing issue",
},
isEmergency: {
type: "boolean",
description: "True if water damage, no water, or sewage backup",
},
},
required: ["name", "phone", "issue", "isEmergency"],
},
};Llamada a Funciones del Agente de Voz
Cómo su IA se conecta a los sistemas reales del negocio
Registrar Funciones con el Asistente
// Update your assistant creation
const assistant = await client.assistants.create({
// ... previous config
functions: [
{
name: "captureLead",
description: captureLeadSchema.description,
parameters: captureLeadSchema.parameters,
async: true, // Your endpoint handles this
},
],
});Crear el Endpoint de Webhook
// app/api/vapi/webhook/route.ts
import { NextRequest, NextResponse } from "next/server";
import { captureLead } from "@/lib/vapi-functions";
export async function POST(request: NextRequest) {
const body = await request.json();
// Vapi sends different message types
const { message } = body;
switch (message.type) {
case "function-call":
if (message.functionCall.name === "captureLead") {
const result = await captureLead(message.functionCall.parameters);
return NextResponse.json({
result,
});
}
break;
case "end-of-call-report":
// Log call analytics
console.log("Call ended:", message);
break;
case "speech-update":
// Real-time transcription (optional)
break;
}
return NextResponse.json({ status: "ok" });
}Paso 3: Conectando un Número Telefónico
Comprar un Número en Twilio
// lib/twilio.ts
import twilio from "twilio";
const client = twilio(
process.env.TWILIO_ACCOUNT_SID,
process.env.TWILIO_AUTH_TOKEN
);
export async function buyPhoneNumber(areaCode: string) {
const numbers = await client.availablePhoneNumbers("US")
.local
.list({ areaCode, limit: 1 });
if (numbers.length === 0) {
throw new Error("No numbers available in this area code");
}
const purchased = await client.incomingPhoneNumbers.create({
phoneNumber: numbers[0].phoneNumber,
voiceUrl: "https://your-domain.com/api/vapi/inbound", // We'll create this
});
return purchased.phoneNumber;
}Crear el Handler de Llamadas Entrantes
// app/api/vapi/inbound/route.ts
import { NextRequest, NextResponse } from "next/server";
import twilio from "twilio";
const TWIML = `
<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Connect>
<Stream url="wss://your-vapi-websocket-url">
<Parameter name="assistantId" value="your-assistant-id" />
</Stream>
</Connect>
</Response>
`;
export async function POST(request: NextRequest) {
// You can add call screening logic here
// E.g., check if business hours, spam detection, etc.
return new NextResponse(TWIML, {
headers: {
"Content-Type": "text/xml",
},
});
}Usando el Número Telefónico de Vapi
Alternativamente, compre directamente a través de Vapi:
// Easier but less control
const phoneNumber = await client.phoneNumbers.create({
provider: "twilio",
areaCode: "555",
assistantId: assistant.id,
});Opciones de Configuración de Número Telefónico
Conecte su agente de voz a líneas telefónicas
Paso 4: Patrones de Producción
Manejo de Errores
Los agentes de voz fallan. Planifique para ello:
// lib/vapi-error-handler.ts
interface ErrorContext {
callId: string;
assistantId: string;
customerPhone: string;
error: Error;
transcript: string;
}
export async function handleVoiceError(context: ErrorContext) {
// 1. Log to monitoring
await logError(context);
// 2. Send fallback SMS
await sendSMS({
to: context.customerPhone,
body: "Sorry, we had a technical issue. A human will call you back within 10 minutes.",
});
// 3. Alert team
await sendSlackAlert({
channel: "#voice-agent-alerts",
text: `Voice agent failed for call ${context.callId}`,
error: context.error.message,
});
// 4. Create task for human follow-up
await createTask({
type: "voice-agent-fallback",
priority: "high",
customerPhone: context.customerPhone,
context: context.transcript,
});
}Limitación de Tasa y Prevención de Abuso
// middleware.ts or API route
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, "1 m"), // 10 calls per minute per number
});
export async function middleware(request: NextRequest) {
const ip = request.ip ?? "127.0.0.1";
const { success, limit, remaining } = await ratelimit.limit(ip);
if (!success) {
return new NextResponse("Rate limit exceeded", { status: 429 });
}
return NextResponse.next();
}Optimización de Costos
Los agentes de voz pueden volverse costosos rápidamente. Así es como mantengo los costos por debajo de $200/mes para más de 200 llamadas:
| Estrategia | Ahorro | Implementación |
|---|---|---|
| Usar gpt-3.5 para consultas simples | 60% | Enrutar basado en intención |
| Cachear respuestas comunes | 30% | Redis para FAQs |
| Acortar prompts | 20% | Mantener por debajo de 1000 tokens |
| Usar voces de OpenAI | 50% | vs ElevenLabs |
| Agrupar llamadas a funciones | 15% | Combinar operaciones |
// Smart routing example
const model = detectSimpleQuery(transcript)
? "gpt-3.5-turbo" // $0.0015/1K tokens
: "gpt-4"; // $0.03/1K tokensPaso 5: Pruebas y Monitoreo
Pruebas Automatizadas
// tests/vapi.test.ts
import { test, expect } from "@playwright/test";
test("AI receptionist captures leads correctly", async ({ page }) => {
// Mock a call
const call = await startTestCall({
phoneNumber: "+15551234567",
scenario: "emergency_leak",
});
// Simulate conversation
await call.speak("Hi, I have a water leak in my basement");
const response = await call.waitForResponse();
expect(response).toContain("emergency");
expect(response).toContain("15 minutes");
// Verify function was called
const lead = await getLastLead();
expect(lead.isEmergency).toBe(true);
expect(lead.issue).toContain("leak");
});Panel de Monitoreo
Haga seguimiento de estas métricas:
// Key metrics to log
interface CallMetrics {
callId: string;
duration: number; // seconds
cost: number; // USD
transcriptLength: number; // characters
functionsCalled: number;
errors: number;
customerSatisfaction?: number; // Post-call survey
resolved: boolean; // Did it solve the problem?
}
// Alert on:
// - >5% error rate
// - >$0.50 average cost per call
// - >10s average latency
// - <80% resolution rateEjemplo del Mundo Real: Resultados del Negocio de Plomería
La Configuración:
- 1 número telefónico (código de área local)
- GPT-4 para problemas complejos, GPT-3.5 para simples
- 3 funciones: captureLead, scheduleAppointment, checkAvailability
- Fallback a humano después de 3 errores
Resultados Después de 3 Meses:
- 247 llamadas gestionadas
- 94% capturadas (vs 60% antes)
- Duración promedio de llamada: 2m 15s
- Costo por llamada: $0.12
- 23 llamadas de emergencia fuera de horario capturadas
- Tiempo ahorrado al dueño: 15 horas/semana
Lo que Funcionó:
- Prompt de sistema específico con ejemplos
- Función de detección de emergencias
- Fallback por SMS para llamadas fallidas
- Revisión diaria de transcripciones durante las primeras 2 semanas
Lo que No Funcionó:
- Inicialmente muy verboso (corregido con la instrucción "sea conciso")
- No capturaba email (agregado a la función)
- Confundía direcciones (se agregó paso de confirmación)
Próximos Pasos
Ahora que entiende lo básico:
- Construya su primer agente: Comience simple, agregue funciones gradualmente
- Pruebe con llamadas reales: Pida a amigos/familia que llamen y den retroalimentación
- Monitoree e itere: Revise transcripciones diariamente al principio
- Escale gradualmente: Agregue características a medida que entienda los modos de falla
En el próximo artículo, cubriré patrones avanzados:
- Soporte multilenguaje
- Fine-tuning de LLM personalizado
- Integración con sistemas CRM
- Construcción de agentes de voz que suenen verdaderamente humanos
Preguntas Frecuentes
P: ¿Cuánto cuesta Vapi? R: ~$0.05-0.15 por minuto dependiendo de la configuración. Una llamada de 2 minutos cuesta aproximadamente $0.20.
P: ¿Puedo usar mi propio número telefónico? R: Sí, a través de la integración con Twilio. O compre directamente a través de Vapi.
P: ¿Qué pasa si la IA no entiende? R: Construya lógica de fallback. Después de 2 malentendidos, ofrezca transferir o tomar un mensaje.
P: ¿Es compatible con HIPAA? R: Vapi es compatible con SOC 2. Para HIPAA, necesitará un BAA y salvaguardas adicionales.
P: ¿Puede manejar acentos? R: Sí, pero pruebe con su base de clientes específica. Deepgram maneja bien la mayoría de los acentos.
¿Necesita ayuda implementando esto para su negocio? Reserve una llamada gratuita de 30 minutos y le ayudaré a diseñar su agente de voz.
Navegación de la Serie: ← Anterior: [Voice AI Architecture Overview] | Siguiente: [Advanced Vapi Patterns: Functions & Workflows] →