Introduction
J'ai récemment construit un réceptionniste IA 24h/24 pour une entreprise de plomberie qui gère désormais plus de 200 appels par mois. Le propriétaire est passé de 40 % d'appels manqués à 98 % d'appels captés. Temps de mise en place : 3 heures.
Ce guide vous apprend à construire des agents vocaux IA prêts pour la production avec Vapi.ai, la plateforme qui rend le développement d'IA vocale réellement faisable pour les petites équipes.
À la fin, vous comprendrez :
- L'architecture STT → LLM → TTS
- Comment configurer Vapi avec un function calling approprié
- Les patterns de production pour la gestion des erreurs
- Les stratégies d'optimisation des coûts
- Les patterns de déploiement réels que j'utilise
Allons-y.
L'architecture de l'IA vocale
Avant d'écrire du code, comprenez le pipeline :
Architecture du pipeline d'IA vocale
Comment l'IA vocale transforme la parole en réponses intelligentes
Le flux :
- Speech-to-Text (STT) : Vapi utilise Deepgram ou Whisper pour transcrire la parole
- Traitement LLM : OpenAI, Claude ou votre modèle personnalisé traite l'intention
- Function calling : Vos endpoints backend gèrent la logique métier
- Text-to-Speech (TTS) : ElevenLabs ou OpenAI convertit la réponse en voix
- Réponse : L'appelant entend l'IA parler
Pourquoi Vapi ?
- Gère toutes les parties difficiles (latence, interruptions, détection de parole)
- Temps de réponse inférieur à 1 seconde
- Function calling intégré
- API simple
- ~0,05-0,10 $ par minute (vs construction en interne : 50 000 $+ de temps de développement)
Prérequis
Avant de commencer, vous aurez besoin de :
- Compte Vapi (offre gratuite disponible)
- Compte Twilio (pour les numéros de téléphone)
- Clé API OpenAI (ou Anthropic pour Claude)
- Connaissances de base en TypeScript/JavaScript
- Endpoint backend (peut être Vercel, Railway ou votre serveur)
Temps d'investissement : 2-4 heures pour le premier agent.
Étape 1 : Configurer votre premier assistant
Créer l'assistant
// 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;
}Explication de la configuration clé
Sélection du modèle :
gpt-4: Meilleure qualité, latence plus élevée (~1,5 s), 0,03 $/1 000 tokensgpt-3.5-turbo: Plus rapide (~0,8 s), moins cher, adapté aux agents simplesclaude-3-sonnet: Excellent pour le raisonnement complexe
Sélection de la voix :
- Les voix ElevenLabs sonnent le plus naturel
- Les voix OpenAI sont moins chères mais plus robotiques
- Testez plusieurs voix, cela compte plus que vous ne le pensez
Conseils pour le prompt système :
- Soyez spécifique sur le ton et la longueur
- Incluez des exemples de ce qu'il NE FAUT PAS dire
- Ajoutez des garde-fous pour les cas limites
- Restez sous 2 000 tokens
Étape 2 : Ajouter le function calling
Les fonctions permettent à votre IA d'interagir avec vos systèmes. Voici une fonction de capture 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"],
},
};Function calling de l'agent vocal
Comment votre IA se connecte aux systèmes métier réels
Enregistrer les fonctions avec l'assistant
// 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
},
],
});Créer l'endpoint 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" });
}Étape 3 : Connecter un numéro de téléphone
Acheter un numéro sur 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;
}Créer le handler d'appels entrants
// 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",
},
});
}Utiliser le numéro de téléphone Vapi
Alternativement, achetez directement via Vapi :
// Easier but less control
const phoneNumber = await client.phoneNumbers.create({
provider: "twilio",
areaCode: "555",
assistantId: assistant.id,
});Options de configuration du numéro de téléphone
Connectez votre agent vocal aux lignes téléphoniques
Étape 4 : Patterns de production
Gestion des erreurs
Les agents vocaux échouent. Préparez-vous :
// 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,
});
}Rate limiting et prévention d'abus
// 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();
}Optimisation des coûts
Les agents vocaux peuvent devenir chers rapidement. Voici comment je maintiens les coûts sous 200 $/mois pour plus de 200 appels :
| Stratégie | Économie | Implémentation |
|---|---|---|
| Utiliser gpt-3.5 pour les requêtes simples | 60 % | Routage basé sur l'intention |
| Mettre en cache les réponses courantes | 30 % | Redis pour les FAQ |
| Raccourcir les prompts | 20 % | Rester sous 1 000 tokens |
| Utiliser les voix OpenAI | 50 % | vs ElevenLabs |
| Regrouper les appels de fonction | 15 % | Combiner les opérations |
// Smart routing example
const model = detectSimpleQuery(transcript)
? "gpt-3.5-turbo" // $0.0015/1K tokens
: "gpt-4"; // $0.03/1K tokensÉtape 5 : Tests et monitoring
Tests automatisés
// 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");
});Tableau de bord de monitoring
Suivez ces métriques :
// 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 rateExemple concret : Résultats pour l'entreprise de plomberie
La configuration :
- 1 numéro de téléphone (indicatif local)
- GPT-4 pour les problèmes complexes, GPT-3.5 pour les simples
- 3 fonctions : captureLead, scheduleAppointment, checkAvailability
- Bascule vers un humain après 3 erreurs
Résultats après 3 mois :
- 247 appels gérés
- 94 % capturés (vs 60 % avant)
- Durée moyenne d'appel : 2 min 15 s
- Coût par appel : 0,12 $
- 23 appels d'urgence hors horaires captés
- Temps économisé pour le propriétaire : 15 heures/semaine
Ce qui a fonctionné :
- Prompt système spécifique avec des exemples
- Fonction de détection d'urgence
- SMS de secours pour les appels échoués
- Revue quotidienne des transcriptions les 2 premières semaines
Ce qui n'a pas fonctionné :
- Initialement trop verbeux (corrigé avec l'instruction "soyez concis")
- Ne capturait pas l'email (ajouté à la fonction)
- Confondait les adresses (ajout d'une étape de confirmation)
Prochaines étapes
Maintenant que vous comprenez les bases :
- Construisez votre premier agent : Commencez simple, ajoutez des fonctions progressivement
- Testez avec de vrais appels : Faites appeler des amis/famille et demandez du feedback
- Surveillez et itérez : Revoyez les transcriptions quotidiennement au début
- Montez en charge progressivement : Ajoutez des fonctionnalités au fur et à mesure que vous comprenez les modes de défaillance
Dans le prochain article, je couvrirai les patterns avancés :
- Support multilingue
- Fine-tuning de LLM personnalisé
- Intégration avec les systèmes CRM
- Construire des agents vocaux qui sonnent vraiment humains
FAQ
Q : Combien coûte Vapi ? R : ~0,05-0,15 $ par minute selon la configuration. Un appel de 2 minutes coûte environ 0,20 $.
Q : Puis-je utiliser mon propre numéro de téléphone ? R : Oui, via l'intégration Twilio. Ou achetez directement via Vapi.
Q : Et si l'IA ne comprend pas ? R : Intégrez une logique de secours. Après 2 incompréhensions, proposez de transférer ou de prendre un message.
Q : Est-ce conforme HIPAA ? R : Vapi est certifié SOC 2. Pour HIPAA, vous aurez besoin d'un BAA et de garde-fous supplémentaires.
Q : Peut-elle gérer les accents ? R : Oui, mais testez avec votre base de clients spécifique. Deepgram gère bien la plupart des accents.
Besoin d'aide pour implémenter cela dans votre entreprise ? Réservez un appel gratuit de 30 minutes et je vous aiderai à concevoir votre agent vocal.
Navigation dans la série : ← Précédent : [Vue d'ensemble de l'architecture IA vocale] | Suivant : [Patterns avancés Vapi : fonctions et workflows] →