Asterisk对接OpenAI Realtime API实现低延迟语音交互

发布时间:2026/7/19 8:17:00
Asterisk对接OpenAI Realtime API实现低延迟语音交互 1. 项目概述让电话系统真正“听懂人话”的实战路径你有没有遇到过这样的场景客户打来电话客服系统只能机械地播放预设语音、转接分机或者干脆挂断重拨更糟的是当客户说“我上个月的账单好像多收了20块”系统却只识别出“账单”两个字然后循环播放“请按1查询余额按2办理业务”——这种体验不是智能化是智能化的反面。而今天要聊的这个项目就是用一套可落地、可复现、不依赖黑盒云服务的方案把 Asterisk 这个运行在企业机房或私有服务器上的老牌开源电话交换系统PBX真正变成一个能实时听、实时想、实时答的 AI 语音代理。核心不是堆砌大模型 API而是打通 OpenAI Realtime API 的流式音频通道与 Asterisk 的 SIP 信令和 RTP 媒体流之间的底层握手逻辑。它不走 WebRTC 中转不依赖前端页面而是让 Asterisk 的通话通道原生承载 AI 语音交互——这意味着你能把它嵌入到任何已有电话系统中比如银行呼叫中心的 IVR 层、物业报修热线、甚至工厂内部调度电话网。关键词很明确OpenAI Realtime API、Asterisk、SIP、RTP、Python、语音流双向透传、低延迟响应。这不是一个玩具 Demo而是一套经过生产环境压力测试的通信架构重构方案。如果你正在维护一套 Asterisk 系统又不想把语音数据全部上传到第三方云平台同时需要真正具备上下文理解能力的语音交互能力那么这个项目就是为你量身写的实操手册。它不讲大道理只告诉你每一步为什么这么写、参数为什么取这个值、哪个函数调用会卡住整个通话流、以及 Asterisk 配置里哪一行少了个分号就会导致媒体流静音——这些都是我在三轮灰度上线、七次通话链路抓包、二十多个深夜调试后记下的真实笔记。2. 整体架构设计与技术选型逻辑拆解2.1 为什么必须绕开 WebRTC直连 SIP/RTP很多初学者看到“AI 语音代理”第一反应是搞个网页用 WebRTC 拿麦克风再调 OpenAI Realtime API。这在演示 POC 时没问题但一旦放到真实电话系统里立刻崩盘。原因有三第一WebRTC 是为浏览器设计的它的 ICE 协商、STUN/TURN 穿透机制在企业内网防火墙、NAT 设备、运营商级 SIP 中继面前极其脆弱第二WebRTC 的音频编码如 OPUS虽然高效但 Asterisk 默认的 SIP 信令协商流程并不天然兼容其复杂 SDP 交换强行对接会导致媒体流无法建立或单向静音第三也是最关键的一点WebRTC 引入了额外的网络跳转和 JS 执行层端到端延迟轻松突破 800ms而语音交互的临界容忍值是 300ms——超过这个值用户就会明显感觉到“对方在思考”对话节奏被彻底破坏。所以本项目采用“SIP 信令 RTP 媒体直通”模式Asterisk 作为 SIP UASUser Agent Server接收呼入完成鉴权、路由后不终止通话而是将原始 RTP 流G.711 μ-law 或 OPUS直接转发给 Python 后端服务Python 服务一边解码音频帧一边通过 OpenAI Realtime API 的 WebSocket 流式接口发送语音片段同时接收 API 返回的文本流和合成语音流再实时编码回 RTP 格式推回 Asterisk 的 RTP 接收端口。整个过程没有中间缓存、没有格式转换瓶颈、没有浏览器沙箱限制实测平均端到端延迟稳定在 220ms 左右完全满足自然对话要求。2.2 为什么选择 OpenAI Realtime API 而非 WhisperTTS 组合有人会问既然要控制链路为什么不自己搭 Whisper ASR 和 Coqui TTS这样数据完全不出内网。这个想法很理想但现实很骨感。Whisper 的 CPU 推理延迟极高即使 tiny 模型单句也要 400ms而 Realtime API 的流式语音识别是毫秒级 chunk 处理支持真正的“边说边识别”Coqui TTS 的语音合成质量虽好但缺乏 Realtime API 内置的语义停顿、情感语调、上下文语气建模能力合成语音听起来像机器人念稿。更重要的是Realtime API 的input_audio_buffer_committed事件能精准标记用户语音结束点这是传统 ASR 模型靠 VAD语音活动检测永远做不到的精度。我们实测对比过在客户说“帮我查一下张三的订单状态”这句话时WhisperVAD 往往在“张三”后就误判为结束导致后续“订单状态”被截断而 Realtime API 能完整捕获整句话并在最后一个音节结束后 120ms 内触发response.audio.delta流合成语音同步输出。这不是功能差异是交互范式的代差。当然我们保留了本地 ASR/TTS 的插槽设计——在config.py里只需切换ASR_BACKEND openai或whisper所有接口保持一致为未来合规性升级留足余地。2.3 Asterisk 版本与模块依赖的硬性约束本项目严格适配 Asterisk 20.x推荐 20.8.0 及以上原因在于Asterisk 18 及更早版本的chan_pjsip模块对 RTP 重传RTX和 FEC前向纠错的支持不完善而 Realtime API 对音频丢包极其敏感Asterisk 19 开始引入rtp_engine抽象层但默认res_rtp_asterisk实现对 OPUS 编码的动态负载类型dynamic payload type协商存在 bug会导致 Asterisk 发送的 RTP 包被 Python 服务拒绝解析。Asterisk 20.8 修复了该问题并新增rtp_keepalive参数可强制维持 NAT 映射表项这对部署在云服务器上的场景至关重要。此外必须启用res_http_websocket模块编译时加--with-websockets因为 Python 后端需要通过 HTTP 接口向 Asterisk 发送Originate动作来主动外呼而该模块提供了/ari/REST 接口的基础支撑。我们曾踩过一个深坑在 CentOS 7 上用源码编译 Asterisk 20.5 时未安装openssl-devel导致res_http_websocket编译失败但make过程无报错直到 ARI 接口返回 404 才发现——这个细节我会在实操章节重点标出。2.4 Python 服务的核心职责边界划分很多人误以为 Python 服务是个“万能胶”既要处理 SIP 信令又要管理 RTP 流还要调大模型 API。这是架构灾难的起点。本项目采用清晰的职责分离Asterisk 全权负责 SIP 信令处理INVITE/ACK/BYE、用户认证pjsip.conf中的authsection、呼叫路由extensions.conf中的 dialplan、以及 RTP 媒体流的收发与编解码协商Python 服务仅承担三个原子任务1作为 RTP 客户端从 Asterisk 指定端口接收原始音频帧并解码为 PCM2作为 WebSocket 客户端与 OpenAI Realtime API 建立长连接发送音频、接收文本与语音 delta3作为 RTP 服务端将 API 返回的语音 delta 实时编码为 OPUS 并推送给 Asterisk 的 RTP 接收端口。三者之间通过标准 RTP 协议通信零耦合。这种设计带来两大好处一是 Asterisk 可以独立升级、重启不影响 AI 服务二是 Python 服务可以水平扩展——同一台服务器跑多个 Python 实例每个实例监听不同 RTP 端口Asterisk 根据dialplan中的变量动态分配端口实现并发通话数线性增长。我们在某物流公司的部署中单台 8C16G 服务器稳定支撑 42 路并发语音交互CPU 峰值使用率 68%内存占用 3.2GB全部来自 Python 进程Asterisk 进程仅占 12%。3. 核心细节解析与实操关键点3.1 Asterisk 配置文件的魔鬼细节Asterisk 的配置不是填空题而是精密仪器的校准。以下是你必须逐字核对的pjsip.conf和extensions.conf关键段落任何遗漏都会导致媒体流中断。首先pjsip.conf中的传输配置必须显式声明rtp_ipv4和rtp_ipv6即使你只用 IPv4[transport-udp] typetransport protocoludp bind0.0.0.0:5060 rtp_bind_address0.0.0.0 rtp_ipv4yes rtp_ipv6no注意rtp_bind_address必须是0.0.0.0不能是具体 IP。我们曾因写成192.168.1.100导致 Asterisk 只监听该 IP 的 RTP 端口而 Python 服务尝试连接127.0.0.1失败抓包显示No route to host。其次终端配置endpoint中media_encryption必须设为nortp_symmetric必须为yes[ai-agent] typeendpoint contextai-dialplan disallowall allowulaw,alaw,opus rtp_symmetricyes media_encryptionno force_rportyes rewrite_contactyesrtp_symmetricyes是强制 Asterisk 使用对称 RTP即发送和接收使用同一端口对这是与 Python 服务直连的前提media_encryptionno是因为 Realtime API 不支持 SRTP开启加密会导致 Asterisk 自动降级为 G.711而 G.711 带宽是 OPUS 的 8 倍极易引发网络拥塞。最后extensions.conf中的 dialplan 是整个流程的触发开关[ai-dialplan] exten _X.,1,NoOp(Entering AI Agent for ${EXTEN}) same n,Set(AI_RTP_PORT${RAND(10000,60000)}) same n,Set(AI_RTP_IP127.0.0.1) same n,Set(CHANNEL(rtpqos)1) same n,Stasis(ai_agent_stasis) same n,Hangup()这里RAND(10000,60000)生成随机 RTP 端口避免多路通话端口冲突CHANNEL(rtpqos)1启用 RTP QoS 标记确保 Linux 内核对 RTP 包进行优先队列调度最关键的是Stasis(ai_agent_stasis)它将通话接入 Asterisk 的 Stasis 应用框架而非传统Dial()这样才能在通话建立后通过 ARI 接口动态修改 RTP 目标地址。如果你漏掉这一行Asterisk 会默认将 RTP 发往呼叫方而不是 Python 服务。3.2 Python RTP 流处理的实时性保障Python 的 GIL全局解释器锁是实时音频处理的天敌。本项目采用asynciouvlooppydubopuslib的组合拳破局。核心逻辑不在主线程而在async def handle_rtp_stream()协程中import asyncio import uvloop from opuslib import Decoder, Encoder from pydub import AudioSegment # 初始化 OPUS 编解码器采样率 16kHz单声道比特率 24k decoder Decoder(16000, 1) encoder Encoder(16000, 1, audio) encoder.bitrate 24000 async def handle_rtp_stream(rtp_reader, rtp_writer): # 创建无锁环形缓冲区容量 200ms 音频3200 字节 audio_buffer bytearray(3200) buffer_pos 0 while True: try: # 非阻塞读取 RTP 包超时 50ms data await asyncio.wait_for( rtp_reader.read(2048), timeout0.05 ) if not data: break # 解析 RTP 头固定 12 字节提取 payload payload data[12:] # 解码 OPUS 到 PCM16-bit signed int pcm decoder.decode(payload, 160) # 10ms 帧 # 写入环形缓冲区自动覆盖旧数据 remaining len(pcm) while remaining 0: write_len min(remaining, 3200 - buffer_pos) audio_buffer[buffer_pos:buffer_poswrite_len] pcm[:write_len] buffer_pos (buffer_pos write_len) % 3200 remaining - write_len pcm pcm[write_len:] # 每积累 20ms320 字节PCM触发一次 OpenAI 流式发送 if buffer_pos 320: chunk bytes(audio_buffer[:320]) await openai_ws.send_audio(chunk) # 移动缓冲区准备下一段 audio_buffer audio_buffer[320:] bytearray(320) buffer_pos - 320 except asyncio.TimeoutError: continue except Exception as e: logger.error(fRTP handling error: {e}) break这段代码的关键在于1asyncio.wait_for强制 50ms 超时防止网络抖动导致协程挂起2环形缓冲区设计避免内存频繁分配3decoder.decode(payload, 160)的160参数必须精确匹配 Asterisk 发送的 OPUS 帧长10ms 16kHz 160 samples否则解码会崩溃。我们曾因 Asterisk 的opus_ptime参数设为2020ms 帧而 Python 解码器仍用160导致解码输出全为静音——这个参数必须在pjsip.conf的endpoint中统一[ai-agent] ... ; 强制 OPUS 帧长为 10ms opus_ptime103.3 OpenAI Realtime API 的 WebSocket 连接稳定性策略Realtime API 的 WebSocket 连接不是“连上就完事”而是需要持续心跳、异常重连、状态同步的精密维护。我们采用三级保活机制第一级WebSocket 协议层心跳。在websocket-client库基础上重写run_forever()方法注入ping_interval15和ping_timeout5参数ws websocket.WebSocketApp( wss://api.openai.com/v1/realtime?...modelgpt-4o-realtime-preview-2024-10-01, on_openself.on_open, on_messageself.on_message, on_errorself.on_error, on_closeself.on_close ) ws.run_forever( ping_interval15, # 每15秒发一次 ping ping_timeout5, # ping 响应超时5秒 reconnect5 # 断连后5秒重试 )第二级应用层心跳。在on_open回调中立即发送session.update消息设置input_audio_transcription为true并启动一个asyncio.create_task(self.keep_alive())协程每 30 秒发送一次空response.create请求防止服务端因空闲关闭连接。第三级状态镜像。定义一个SessionState类记录session_id、conversation_id、turn_id、last_activity_ts四个字段。每次收到response.audio.delta时更新last_activity_ts在keep_alive()协程中若time.time() - last_activity_ts 60则主动触发session.interrupt后重建会话。这套机制让我们在连续 72 小时压测中连接中断率为 0最长单次会话达 48 分钟远超官方文档标注的 30 分钟上限。3.4 音频编解码参数的物理意义与实测调优音频质量不是“越高越好”而是要在带宽、延迟、计算资源间找黄金平衡点。本项目最终确定的参数组合是基于 200 小时真实通话录音的 AB 测试结果参数取值物理意义实测效果采样率16kHz每秒采集 16000 个音频样本覆盖人声主要频段300Hz-3.4kHz比 8kHz 更自然比 48kHz 节省 66% 带宽OPUS 比特率24kbit/s每秒传输 24000 比特音频数据在 10ms 帧长下单帧约 30 字节网络抖动容忍度最佳低于 16kbit/s 会出现高频丢失高于 32kbit/s 带宽压力陡增帧长10ms每次编码/解码 10 毫秒音频与人类语音感知最小时间单位匹配延迟最低20ms 帧长虽节省 50% 包数但单次处理延迟翻倍VAD 阈值-35dBFS音频能量低于此值判定为静音Asterisk 默认 VAD 过于敏感-45dBFS导致用户换气时被误切调整至此值后静音检测准确率从 82% 提升至 97%这些参数不是凭空设定而是通过sox工具生成标准测试音1kHz 正弦波 白噪声用Wireshark抓包分析 RTP 包大小分布用ffmpeg计算实际比特率最终在Asterisk CLI中执行rtp set debug on实时验证。例如要验证 OPUS 比特率是否生效可在 Asterisk CLI 中输入*CLI rtp set debug on *CLI core set verbose 3然后发起一次通话观察日志中RTP Transmission行的bytes字段10ms 帧长下24kbit/s 对应bytes30误差超过 ±2 字节即需重新校准。4. 实操过程与核心环节实现4.1 环境初始化从裸机到可运行系统的完整步骤我们以 Ubuntu 22.04 LTS 为例全程使用 root 权限操作生产环境建议创建专用用户此处为简化说明第一步安装 Asterisk 20.8.0# 更新系统并安装编译依赖 apt update apt install -y build-essential libssl-dev libxml2-dev libsqlite3-dev \ libcurl4-openssl-dev uuid-dev libjansson-dev libpjproject-dev python3-dev \ libsrtp2-dev libspandsp-dev libiksemel-dev libiksemel-utils libunbound-dev \ libpopt-dev libgsm1-dev libspeex-dev libspeexdsp-dev libvorbis-dev \ libogg-dev libopus-dev libresample1-dev libsnmp-dev libneon27-dev \ libiksemel-dev libiksemel-utils libunbound-dev libpopt-dev libgsm1-dev \ libspeex-dev libspeexdsp-dev libvorbis-dev libogg-dev libopus-dev \ libresample1-dev libsnmp-dev libneon27-dev # 下载并解压 Asterisk 20.8.0 cd /usr/src wget https://downloads.asterisk.org/pub/telephony/asterisk/asterisk-20.8.0.tar.gz tar xzf asterisk-20.8.0.tar.gz cd asterisk-20.8.0 # 配置编译选项关键必须包含 websockets ./configure --with-pjproject-bundled --with-websockets --with-sslopenssl # 编译安装-j$(nproc) 加速 make -j$(nproc) make install make config make samples # 启动 Asterisk 并检查模块 systemctl start asterisk asterisk -rx module show like res_http_websocket # 应返回 res_http_websocket.so HTTP Websocket Support 2.0.0提示如果module show未列出res_http_websocket说明--with-websockets未生效需检查configure输出中是否有checking for OpenSSL... yes和checking for libwebsockets... yes。常见原因是未安装libwebsockets-dev包。第二步配置 Asterisk 核心文件编辑/etc/asterisk/pjsip.conf添加前述[transport-udp]和[ai-agent]段落编辑/etc/asterisk/extensions.conf添加[ai-dialplan]最后启用 ARI 接口# /etc/asterisk/http.conf [general] enabledyes enablestaticyes bindaddr0.0.0.0 bindport8088 # /etc/asterisk/ari.conf [general] enabledyes allowed_originshttp://localhost,https://your-domain.com重启 Asterisksystemctl restart asterisk然后执行asterisk -rx http show status确认HTTP Server Enabled为Yes。第三步部署 Python 服务# 创建虚拟环境 python3 -m venv /opt/ai-voice-agent/venv source /opt/ai-voice-agent/venv/bin/activate pip install --upgrade pip pip install websocket-client aiohttp pydub opuslib numpy uvloop python-dotenv # 克隆 GitHub 仓库假设已创建 git clone https://github.com/yourname/asterisk-openai-realtime.git /opt/ai-voice-agent cd /opt/ai-voice-agent # 配置环境变量 cp .env.example .env nano .env # 修改 OPENAI_API_KEY、ASTERISK_HOST、ASTERISK_PORT、ASTERISK_USERNAME、ASTERISK_PASSWORD.env文件内容必须包含OPENAI_API_KEYsk-... ASTERISK_HOST127.0.0.1 ASTERISK_PORT8088 ASTERISK_USERNAMEari_user ASTERISK_PASSWORDari_pass RTP_LISTEN_PORT10000 RTP_SEND_PORT10001第四步创建 ARI 用户在 Asterisk CLI 中执行*CLI ari create user ari_user ari_pass *CLI ari set permission ari_user all这一步至关重要否则 Python 服务无法通过 ARI 接口控制通话。4.2 主服务启动与通话链路验证Python 主服务main.py的核心逻辑是监听 Asterisk 的 Stasis 事件并在通话进入时动态接管 RTP 流import asyncio from aiohttp import web import aiohttp import json # 全局变量存储当前通话的 RTP 端口映射 active_calls {} async def stasis_start_handler(request): 处理 Stasis 启动事件 data await request.json() channel_id data[channel][id] # 从 dialplan 中获取随机 RTP 端口 rtp_port int(data[channel][dialplan][exten]) # 通过 ARI 接口修改 RTP 目标地址 async with aiohttp.ClientSession() as session: async with session.post( fhttp://{ASTERISK_HOST}:{ASTERISK_PORT}/ari/channels/{channel_id}/rtp, authaiohttp.BasicAuth(ARI_USER, ARI_PASS), params{rtp_port: rtp_port, rtp_ip: 127.0.0.1} ) as resp: if resp.status ! 200: logger.error(fFailed to set RTP for {channel_id}) return web.Response(status500) # 启动 RTP 处理协程 asyncio.create_task(handle_rtp_stream(channel_id, rtp_port)) active_calls[channel_id] rtp_port return web.Response() # 启动 aiohttp Web 服务器监听 Stasis 事件 app web.Application() app.router.add_post(/stasis-start, stasis_start_handler) if __name__ __main__: web.run_app(app, host0.0.0.0, port8000)启动服务cd /opt/ai-voice-agent source venv/bin/activate python main.py验证链路是否打通在另一台机器上用 Zoiper 或 Linphone 拨打 Asterisk 分机如 1001Asterisk CLI 中执行core set verbose 3观察日志 Using SIP RTP CoS mark 5 Using SIP RTP CoS mark 5 -- Executing [1001from-internal:1] NoOp(PJSIP/1001-00000001, Entering AI Agent for 1001) in new stack -- Executing [1001from-internal:2] Set(PJSIP/1001-00000001, AI_RTP_PORT12345) in new stack -- Executing [1001from-internal:3] Set(PJSIP/1001-00000001, AI_RTP_IP127.0.0.1) in new stack -- Executing [1001from-internal:4] Set(PJSIP/1001-00000001, CHANNEL(rtpqos)1) in new stack -- Executing [1001from-internal:5] Stasis(PJSIP/1001-00000001, ai_agent_stasis) in new stack查看 Python 服务日志应出现RTP stream started for channel PJSIP/1001-00000001 on port 12345用tcpdump抓包验证 RTP 流tcpdump -i lo -n port 12345 -w rtp.pcap用 Wireshark 打开rtp.pcap过滤rtp应看到连续的 RTP 包Payload Type 为 120OPUS。4.3 OpenAI Realtime API 会话管理与上下文注入Realtime API 的强大之处在于conversation.item.create可以在会话中注入系统指令、历史消息、甚至自定义工具。本项目在session.update阶段完成三重上下文构建def build_session_update(): return { type: session.update, session: { input_audio_format: s16le, output_audio_format: s16le, input_audio_transcription: { model: whisper-1 }, turn_detection: { type: server_vad, threshold: 0.5, prefix_padding_ms: 300, silence_duration_ms: 1000 }, tools: [ { type: function, name: get_account_balance, description: Get the current balance of the callers account, parameters: {type: object, properties: {}} } ], tool_choice: auto, instructions: ( You are a customer service agent for Acme Corp. Your name is Alex. Speak in a friendly, professional tone. If the user asks about their account balance, call get_account_balance. Always confirm actions before executing them. Keep responses under 20 seconds. ), voice: nova, temperature: 0.7, max_response_output_tokens: 1024 } } # 在 WebSocket 连接建立后立即发送 await ws.send(json.dumps(build_session_update()))这里instructions字段是 AI 的“人格设定”tools是可调用的后端服务接口turn_detection的silence_duration_ms1000表示用户静音 1 秒即判定为说完比默认的 500ms 更符合中文口语习惯中文停顿通常更长。我们还实现了get_account_balance工具的本地模拟async def handle_tool_call(tool_call): if tool_call[name] get_account_balance: # 模拟数据库查询 balance random.randint(1000, 50000) return { type: tool_result, tool_call_id: tool_call[id], result: fYour current balance is ${balance:.2f}. }当 AI 在response.text中生成Ill check your account balance now.时会自动触发该工具并将结果注入下一轮对话实现真正的“AI业务系统”闭环。4.4 生产环境部署与性能调优单机部署只是开始生产环境需考虑高可用、监控、日志归集。我们采用如下架构进程管理用systemd替代screen或nohup确保崩溃自动重启# /etc/systemd/system/ai-voice-agent.service [Unit] DescriptionAI Voice Agent Service Afternetwork.target asterisk.service [Service] Typesimple Userasterisk WorkingDirectory/opt/ai-voice-agent ExecStart/opt/ai-voice-agent/venv/bin/python /opt/ai-voice-agent/main.py Restartalways RestartSec10 EnvironmentFile/opt/ai-voice-agent/.env [Install] WantedBymulti-user.target启用服务systemctl daemon-reload systemctl enable ai-voice-agent systemctl start ai-voice-agent日志切割在main.py中配置RotatingFileHandler按大小切割10MB保留 7 份handler RotatingFileHandler( /var/log/ai-voice-agent/app.log, maxBytes10*1024*1024, backupCount7 )性能监控集成psutil实时采集指标暴露/metrics端点供 Prometheus 抓取app.get(/metrics) async def metrics(request): cpu_percent psutil.cpu_percent(interval1) memory psutil.virtual_memory() return web.Response(textf # HELP ai_voice_agent_cpu_usage CPU usage percentage # TYPE ai_voice_agent_cpu_usage gauge ai_voice_agent_cpu_usage {cpu_percent} # HELP ai_voice_agent_memory_usage_bytes Memory usage in bytes # TYPE ai_voice_agent_memory_usage_bytes gauge ai_voice_agent_memory_usage_bytes {memory.used} )网络优化在/etc/sysctl.conf中追加net.core.rmem_max 16777216 net.core.wmem_max 16777216 net.ipv4.udp_mem 65536 131072 262144运行sysctl -p生效大幅提升 UDP 缓冲区容量应对突发 RTP 包洪峰。5. 常见问题与排查技巧实录5.1 通话建立但无语音RTP 流中断的五大根因这是最常被问到的问题我们整理了真实排障记录按发生频率排序现象根因排查命令解决方案Asterisk 日志显示RTP Transmission但 Python 无日志Python 服务未监听正确端口netstat -tuln | grep :10000检查RTP_LISTEN_PORT是否与 dialplan 中RAND()生成的端口一致确认iptables -L未拦截该端口Wireshark 抓到 RTP 包但 Python 解码后为静音OPUS 帧长不匹配tcpdump -i lo -n port 10000 -c 10 -w test.pcap sox test.pcap -r 16000 -b 16 -c 1 test.wav用sox转换后播放若无声则 Asterisk 发送的 OPUS 帧长与 Python 解码器参数不符统一设为10msPython 日志显示RTP stream started但 OpenAI 无响应WebSocket 连接未