news 2026/4/23 16:20:12

Voice Agent 实战:用 OpenAI Realtime API + Twilio 复刻一个“全双工”的 AI 电话客服

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Voice Agent 实战:用 OpenAI Realtime API + Twilio 复刻一个“全双工”的 AI 电话客服

🚀 前言:为什么现在的电话机器人这么“傻”?

如果你接过营销电话,那种机械感是掩盖不住的。传统的架构是ASR (识别) -> NLP (处理) -> TTS (合成)的“回合制”游戏。

OpenAI Realtime API (GPT-4o Audio)将这三步合二为一。它直接处理音频输入,直接输出音频,中间不再有文本转换的损耗。这不仅让延迟降低到了毫秒级,更重要的是:它可以听出你的语气(愤怒、犹豫),也能用带有情感的语调回复你。

今天,我们将构建这样一个系统:

  1. 用户:拨打一个真实的电话号码。
  2. Twilio:接听电话,并将音频流(Media Stream)通过 WebSocket 推送给我们的服务器。
  3. Server:作为中继,将 Twilio 的音频转发给 OpenAI,并将 OpenAI 的回复音频转发回 Twilio。
  4. OpenAI:实时思考并说话。

🏗️ 一、 架构设计:WebSocket 是核心

这是一个典型的**双向流式(Bi-directional Streaming)**架构。

数据流向图 (Mermaid):

中继服务器 (Python/Node)

WebSocket (G.711音频)

WebSocket (PCM音频)

用户 (PSTN电话)

Twilio 电话网关

中间件逻辑

OpenAI Realtime API

关键难点

  • 协议转换:Twilio 输出的是mulaw格式音频(Base64 编码),OpenAI 需要的是pcm16
  • 打断机制 (Interruption):当用户说话时,服务器必须通过 OpenAI 的input_audio_buffer.speech_started事件,立刻发送指令告诉 Twilio“清空播放缓存”,实现打断效果。

🛠️ 二、 准备工作

  1. OpenAI API Key:需要有访问gpt-4o-realtime-preview的权限。
  2. Twilio 账号:注册并购买一个电话号码(试用号也可以,大概 $1)。
  3. 公网服务器:或者使用ngrok将本地端口暴露到公网(Twilio 需要回调)。

📞 三、 第一步:配置 Twilio TwiML

当电话打进来时,我们需要告诉 Twilio:“别自己处理,把音频流通过 WebSocket 扔给我的服务器。”

在 Twilio 后台创建一个TwiML Bin,或者直接在代码中返回以下 XML:

<Response><Connect><Streamurl="wss://your-domain.com/media-stream"><Parametername="customerId"value="12345"/></Stream></Connect></Response>

🐍 四、 第二步:编写中继服务器 (Python FastAPI)

我们需要一个能够同时处理Twilio WebSocketOpenAI WebSocket的服务。

核心代码 (server.py):

importosimportjsonimportasyncioimportwebsocketsfromfastapiimportFastAPI,WebSocketfromstarlette.websocketsimportWebSocketDisconnect# 你的 OpenAI KeyOPENAI_API_KEY=os.getenv("OPENAI_API_KEY")VOICE="alloy"# AI 的声音SYSTEM_PROMPT="你是一个专业、幽默的电话客服。请用中文简短回答。"app=FastAPI()@app.websocket("/media-stream")asyncdefhandle_media_stream(websocket:WebSocket):awaitwebsocket.accept()print("Twilio 连接成功")# 1. 连接 OpenAI Realtime APIurl="wss://api.openai.com/v1/realtime?model=gpt-4o-realtime-preview-2024-10-01"headers={"Authorization":f"Bearer{OPENAI_API_KEY}","OpenAI-Beta":"realtime=v1",}asyncwithwebsockets.connect(url,extra_headers=headers)asopenai_ws:# 2. 初始化 Sessionsession_update={"type":"session.update","session":{"voice":VOICE,"instructions":SYSTEM_PROMPT,"input_audio_format":"g711_ulaw",# Twilio 默认格式"output_audio_format":"g711_ulaw","turn_detection":{"type":"server_vad"}# 开启服务端语音活动检测}}awaitopenai_ws.send(json.dumps(session_update))# 3. 定义双向转发任务stream_sid=Noneasyncdefreceive_from_twilio():nonlocalstream_sidtry:whileTrue:message=awaitwebsocket.receive_text()data=json.loads(message)ifdata["event"]=="media":# 收到 Twilio 音频 -> 转发给 OpenAIaudio_append={"type":"input_audio_buffer.append","audio":data["media"]["payload"]}awaitopenai_ws.send(json.dumps(audio_append))elifdata["event"]=="start":stream_sid=data["start"]["streamSid"]print(f"Stream 开始:{stream_sid}")exceptWebSocketDisconnect:print("Twilio 断开连接")asyncdefreceive_from_openai():try:asyncformessageinopenai_ws:response=json.loads(message)# A. 收到 AI 音频 -> 转发给 Twilio 播放ifresponse["type"]=="response.audio.delta"andresponse.get("delta"):audio_payload={"event":"media","streamSid":stream_sid,"media":{"payload":response["delta"]}}awaitwebsocket.send_text(json.dumps(audio_payload))# B. 关键点:用户打断处理# 当 OpenAI 检测到用户开始说话时,我们需要让 Twilio 立刻闭嘴ifresponse["type"]=="input_audio_buffer.speech_started":print("检测到用户插话,清空播放缓存...")clear_msg={"event":"clear","streamSid":stream_sid,}awaitwebsocket.send_text(json.dumps(clear_msg))# 同时也告诉 OpenAI 别继续生成刚才没说完的话了awaitopenai_ws.send(json.dumps({"type":"response.cancel"}))exceptExceptionase:print(f"OpenAI 错误:{e}")# 并发运行两个任务awaitasyncio.gather(receive_from_twilio(),receive_from_openai())if__name__=="__main__":importuvicorn# 必须运行在 0.0.0.0 才能被 ngrok 访问uvicorn.run(app,host="0.0.0.0",port=5000)

⚡ 五、 关键技术点解析

1. 为什么不需要转码?

OpenAI Realtime API 最近更新支持了g711_ulaw格式。这正是传统电话网络(PSTN)使用的格式。

  • 以前:Twilio (ulaw) -> Server (转pcm) -> LLM (文本) -> TTS (pcm) -> Server (转ulaw) -> Twilio。
  • 现在:Twilio (ulaw) -> OpenAI (ulaw) -> Twilio。
    这省去了大量的编解码 CPU 开销和延迟。
2. VAD(语音活动检测)的妙用

在代码中,input_audio_buffer.speech_started是核心。
旧的电话机器人最傻的地方就是“抢话”。
有了这个事件,只要用户发出一声“哎等等”,OpenAI 毫秒级检测到,通过response.cancel和 Twilio 的clear指令,机器人会瞬间闭嘴,等待你的新指令。

3. 工具调用 (Function Calling)

Realtime API 同样支持 Function Calling。
你可以在session.update中定义一个check_order_status工具。当用户问“我的快递到哪了”,OpenAI 会暂停生成音频,向你的 Server 发送函数调用请求,你查库后返回结果,OpenAI 再把结果念给用户听。
这一切都在同一个 WebSocket 连接中完成。


🎯 总结

通过OpenAI Realtime API + Twilio,我们构建的不再是一个简单的“IVR 语音导航”,而是一个具有类人交互能力的数字员工。

它没有明显的延迟,它可以被打断,它甚至能听出你感冒了并表示关心。
语音交互(Voice UI)的 iPhone 时刻,可能真的已经到来了。

Next Step:

  1. 申请一个 Twilio 号码。
  2. ngrok http 5000把你的服务暴露出去。
  3. 把 Twilio 的 Voice Webhook 指向你的wss://xxxx.ngrok-free.app/media-stream
  4. 打个电话过去,感受一下未来的样子。
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/23 12:11:43

【OD刷题笔记】- 单词加密

📌 华为OD机试真题精选 2025B卷合集 单词加密 问题描述 1、输入一个英文句子,句子中包含若干个单词,每个单词间有一个空格。 2、需要将句子中的每个单词按照要求加密输出。 要求: 1)单词中包括元音字符(‘aeuio’、‘AEUIO’,大小写都算),则将元音字符替换成’…

作者头像 李华
网站建设 2026/4/23 13:30:09

PHP内置函数 vs 非内置函数

“PHP 内置函数 vs 非内置函数” 的差异&#xff0c;不只是“有没有 function_exists()”&#xff0c;而是性能、生命周期、错误处理、可调试性等多维度的系统级区别。理解这些&#xff0c;才能写出高性能、可维护的 PHP 代码。一、定义&#xff1a;什么是“内置函数”&#xf…

作者头像 李华
网站建设 2026/4/23 10:44:21

YOLO模型灰度流量切分:基于用户ID或地理位置的策略

YOLO模型灰度流量切分&#xff1a;基于用户ID或地理位置的策略 在智能安防摄像头遍布楼宇、工厂和街道的今天&#xff0c;一个看似微小的AI模型更新&#xff0c;可能引发连锁反应——某小区业主突然发现自家监控频繁误报“有人入侵”&#xff0c;而技术团队却在日志中找不到明确…

作者头像 李华
网站建设 2026/4/23 13:35:35

YOLO模型导出为TorchScript:提升推理稳定性的方法

YOLO模型导出为TorchScript&#xff1a;提升推理稳定性的方法 在工业自动化、智能监控和边缘计算场景中&#xff0c;目标检测系统的稳定性与部署效率直接决定了项目的成败。尽管YOLO系列模型以其卓越的实时性能成为主流选择&#xff0c;但在从训练环境迈向生产系统的过程中&…

作者头像 李华
网站建设 2026/4/23 12:16:24

YOLO在港口集装箱识别中的成功应用案例分享

YOLO在港口集装箱识别中的成功应用案例分享 在全球贸易持续扩张的背景下&#xff0c;港口作为国际物流的关键节点&#xff0c;正面临前所未有的吞吐压力。每天成千上万的集装箱在码头被装卸、转运、堆存&#xff0c;传统依赖人工记录或半自动设备识别的方式不仅效率低下&#x…

作者头像 李华
网站建设 2026/4/23 11:20:14

YOLO模型训练容器化编排:使用Helm部署K8s集群

YOLO模型训练容器化编排&#xff1a;使用Helm部署K8s集群 在智能制造工厂的视觉质检线上&#xff0c;一个常见的困境是&#xff1a;算法团队刚调优完的YOLOv8模型&#xff0c;在从本地服务器迁移到生产环境时却频频崩溃——原因竟是CUDA版本不匹配、数据路径错误&#xff0c;甚…

作者头像 李华