Voice Agents: Sådan Byggede Vi en HR-assistent med Twilio og OpenAI Realtime

Kasper Kristian RasmussenVoice Agents: Sådan Byggede Vi en HR-assistent med Twilio og OpenAI Realtime
Voice agents—AI der taler og lytter i realtid—har taget et spring fremad med modeller som OpenAI Realtime API. I denne artikel gennemgår vi hvordan vi byggede en telefonbaseret HR-assistent i projektet HelpDeskAI, med kun omkring 130 linjer kode.
Hvad Er En Voice Agent?
En voice agent er en AI der kan:
- Tale (TTS – text-to-speech)
- Lytte og forstå (STT – speech-to-text)
- Tænke og handle (LLM med tools)
- Samtale i realtid med lav latenstid
Tidligere krævede det typisk at samle flere tjenester (Whisper, en LLM, en TTS-leverandør, telephony) og bygge en kompleks pipeline. Nu kan OpenAI Realtime API håndtere tale-ind, tænkning og tale-ud i et samlet flow. Det gør implementationen markant simplere.
Arkitektur: Fra Telefon til AI
Overordnet flow:
Telefonopkald (PSTN)
↓
Twilio Voice (webhook)
↓ TwiML med <Stream url="wss://.../media-stream"/>
Twilio åbner WebSocket → /media-stream
↓
TwilioRealtimeTransportLayer (adapter)
↓ konverterer Twilio Media Streams ↔ OpenAI Realtime WebSocket
RealtimeSession (RealtimeAgent + tools + guardrails)
↓
OpenAI Realtime API
1. Indkommende Opkald (Twilio Webhook)
Når nogen ringer, kalder Twilio vores webhook (/incoming-call). Vi returnerer TwiML der forbinder opkaldet til en WebSocket-URL:
<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Connect>
<Stream url="wss://din-server.dk/media-stream" />
</Connect>
</Response>
Twilio åbner herefter en WebSocket til vores /media-stream-endpoint og streamer audio i begge retninger.
2. Transport Layer: Twilio ↔ OpenAI
Twilio bruger Media Streams med et binært protokol. OpenAI Realtime API bruger sin egen WebSocket-format. TwilioRealtimeTransportLayer fra @openai/agents-extensions fungerer som adapter mellem dem:
- Konverterer lydformater
- Håndterer Twilio
mark-events til at fange afbrydelser (brugertaler mens AI taler) - Holder forbindelsen mellem Twilio og OpenAI Realtime API
STT og TTS sker begge i OpenAI Realtime modellen—der er ingen separate tjenester.
3. Agenten: RealtimeAgent med Tools
Vi definerer en RealtimeAgent med instruktioner og tools:
const agent = new RealtimeAgent({
name: "HR-assistent",
instructions: `Du er HR-assistent for organisationen.
Du hjælper med spørgsmål om job og ledige stillinger, kontakt,
kurser, selvbetjening og generelle HR-henvendelser. Svar altid på dansk.
Hold svar korte og venlige. Henvis til hjemmesiden til selvbetjening.`,
tools: [getVacancyInfoTool, getContactInfoTool, getSelfServiceInfoTool],
});
Tools giver agenten mulighed for at hente konkrete data:
| Tool | Formål |
|---|---|
get_vacancy_info |
Information om ledige stillinger |
get_contact_info |
Kontaktoplysninger og åbningstider |
get_self_service_info |
Selvbetjening, guides, kurser, råd (med topic-parameter) |
Når opkalderen fx spørger "Hvordan ansøger jeg til en stilling?", kalder agenten get_vacancy_info og svarer baseret på det.
4. Guardrails: Sikkerhed omkring output
Agenten må ikke love ansættelse eller dele privat information. Vi bruger output guardrails:
const guardrails = [
{
name: "HR blocklist",
async execute({ agentOutput }) {
const blocklistTerms = [
"du er ansat",
"du fik jobbet",
"vi lover",
"garanteret stilling",
"privat telefon",
"personlig email",
];
const triggered = blocklistTerms.some((term) =>
agentOutput.toLowerCase().includes(term),
);
return {
tripwireTriggered: triggered,
outputInfo: { blocklistTermsInOutput: triggered },
};
},
},
];
Hvis agentens output indeholder disse termer, kan man logge det eller stoppe responsen.
5. WebSocket-Handler: Sammenmontering
Selve forbindelsen sættes op i WebSocket-handleren:
fastify.get("/media-stream", { websocket: true }, async (connection) => {
const transport = new TwilioRealtimeTransportLayer({
twilioWebSocket: connection,
});
const session = new RealtimeSession(agent, {
transport,
outputGuardrails: guardrails,
});
await session.connect({ apiKey: OPENAI_API_KEY });
});
RealtimeSession forbinder agenten, transporten og guardrails til OpenAI Realtime API.
Tech Stack
| Lag | Teknologi |
|---|---|
| HTTP/WebSocket server | Fastify v5 |
| Telefoni | Twilio Voice + Media Streams |
| Voice AI | OpenAI Realtime API via @openai/agents |
| Transport | TwilioRealtimeTransportLayer (@openai/agents-extensions) |
| Validering | Zod |
Deployment og Krav
- Server på en port (fx 5050)
- HTTPS/WSS offentligt endpoint (fx via ngrok eller cloud)
- Twilio Voice webhook:
https://din-host/incoming-call OPENAI_API_KEYmed adgang til Realtime API- Node.js 22+ anbefales
Konklusion
Voice agents behøver ikke være komplekse. Med Twilio Media Streams og OpenAI Realtime API kan man bygge en telefonbaseret AI-assistent med:
- Ingen separat STT/TTS
- Få linjer kode (~130)
- Tools til strukturerede data
- Guardrails til mere sikker output
Projektet HelpDeskAI viser en konkret implementation til HR-support—et mønster der kan genbruges til kundeservice, booking, FAQ og lignende.
Vil du udforske voice agents eller andre AI-løsninger til din virksomhed? Kontakt os og lad os snakke om hvad der giver mening for dig.