把 Voice Agent 从网页 Demo 接到真实电话时,最容易出现的一句误判是:“SIP 都 200 OK 了,为什么机器人还没说话?”

200 OK 很重要,但它只回答了信令层的一部分问题。它不保证 RTP 已经稳定进来,不保证编解码与采样率能被下游接住,也不保证房间、调度规则和 Agent runtime 已经准备好。把这几层压成一个“已接通”状态,排障时就会变得很痛苦:用户听到的是沉默,你看到的却是一条看似成功的 SIP 日志。

我的判断很直接:真实电话接入的第一道工程门槛不是“模型能不能说话”,而是把信令应答、媒体可用、Agent 就绪拆成独立状态,并且只在三者同时成立时允许闪电智能VoiceAgent 开始问候或执行业务动作。否则,200 OK 之后的无声通话会被误归因给 ASR、LLM 或 TTS,实际问题可能只是媒体根本没有到达。

本文以入呼 AI 客服为例,拆解 SIP 建连里最常卡住的几个位置,并提供一个只依赖 Python 标准库的状态门示例。示例验证状态边界,不会拨打电话,也不代表任何线路、供应商或线上性能。

目录

先别把“已接通”当成一个状态

电话接入里至少有三种“看起来已经通了”的时刻:

  1. 运营商或 SIP 服务端对 INVITE 返回了临时或最终响应;
  2. 双方完成 SDP 里的媒体协商;
  3. 第一个可消费的 RTP 音频包抵达媒体入口,Agent 也已经接入会话。

这三件事在时间上常常挨得很近,但它们不是同一件事。以 LiveKit 的电话接入链路为例,入呼先由 SIP 服务接收 INVITE、校验 trunk、匹配 dispatch rule,再把来电方作为 SIP participant 加入房间;Agent 需要被调度到同一个房间。官方文档也明确说明:Agent 加入房间并不等于音频已经交换,媒体要等 SIP 握手完成后才会开始。LiveKit SIP primer入呼工作流 把这两个阶段分开描述。

所以排障时我不会问“这通电话有没有接通”,而是把问题拆成三个可回答的问题:

问题 可观察证据 没满足时的用户现象
信令是否应答? INVITE、18x、200 OKACK、BYE、SIP 响应码 直接无法接通、持续振铃、快速失败
媒体是否可用? SDP codec、RTP 首包、包计数、jitter、静音检测 已接通但双方或单方无声
Agent 是否就绪? room/participant、dispatch、worker、ASR/TTS runtime ready 有媒体但迟迟不问候、进入错误队列

真正的“可以说第一句话”是三项的交集。不要让某一个平台的 answeredin-progressactive 字段替代全部判断。

一通电话在系统里到底经过了什么

下面这条链路适合拿来对照日志,而不是照着画一张漂亮架构图就结束:

"闪电智能VoiceAgent" "路由与调度" "电话接入网关" "SIP Trunk/运营商" "用户电话" "闪电智能VoiceAgent" "路由与调度" "电话接入网关" "SIP Trunk/运营商" "用户电话" INVITE + SDP INVITE 100 Trying 校验 trunk,匹配路由 dispatch(call_id, route) 180 Ringing 或 183 Session Progress runtime ready 200 OK + SDP ACK RTP 首包 可消费音频流 首句 TTS

有两个细节值得单独说。

第一,180 Ringing183 Session Progress 都不是接通。前者通常表示对端在振铃;后者常带 SDP,用于早期媒体协商或播放回铃音,但它仍然不是最终应答。不同运营商和 SBC 的表现会有差异,不能仅靠看到 183 就启动 ASR。

第二,200 OK 后还需要 ACK 才完成这个 INVITE 事务。即便信令握手完成,也仍需检查媒体的实际到达。Twilio 的 SIP 文档把 <Dial><Sip> 的进度事件和最终 SIP 响应码分开提供;其回调事件会以独立 HTTP 请求送达,并不保证到达顺序。因此,业务侧也不能按照“收到回调就顺序覆盖状态”的假设写代码。Twilio SIP 文档DialSipResponseCode 与回调顺序有明确说明。

SIP 卡住时,先用响应码给问题分层

不是每个 SIP 问题都要抓包才开始定位。先按响应码和事务阶段分层,能排除不少误操作。

现象 优先检查 不要急着做什么
一直没有 18x DNS/SBC 路由、Trunk 授权、入口防火墙、目标 URI 不要先调 TTS 或改模型提示词
有 180/183,迟迟没有 200 路由策略、被叫端、Agent 调度超时、SBC 转发 不要把振铃当作可开始会话
直接 401/407 SIP digest 身份认证、realm、时钟和凭据轮换 不要把口令写进业务日志
403/404 trunk/IP 白名单、目标 URI、号码映射、dispatch rule 不要靠不断重试掩盖配置错误
408/480/486 超时、临时不可达、被叫不可用或忙线 不要统一标成“AI 接入失败”
有 200 和 ACK,但用户无声 SDP、codec、RTP/SRTP、NAT、端口范围、媒体超时 不要把它写成“模型首字慢”
RTP 已到但没有首句 room、participant、Agent dispatch、runtime readiness、TTS 首包 不要重新发 INVITE

响应码的语义最终要以所用运营商、SBC 和电话平台的文档为准。本文不把某个码等同于唯一根因。比如 486 Busy Here 在 LiveKit 的故障排查中指向目的地拒接或不可用,但到底是 Agent、房间策略还是下游路由拒绝,仍需要结合 dispatch 与 runtime 日志判断。LiveKit SIP troubleshooting 给出了这类方向。

真正容易被漏掉的是媒体门

很多团队的呼叫记录只有 call_status=answered,没有“首个 RTP 包何时到达”。这样一来,用户说“接通后没声音”,系统只能从模型链路猜原因。

媒体门至少应记录四件事:

  • 双方 SDP 是否协商出共同的 codec;
  • rtp_first_packet_at 是否存在,以及距 sip_200_at 多久;
  • 一段窗口内的 RTP 包数、抖动和丢包情况;
  • 入站与出站音频是否都真的流动,而不是只看到单向包。

电话窄带最常见的是 G.711 PCMU/PCMA,采样率通常为 8kHz;Agent 侧常需要转为内部的 16kHz 或 24kHz 流供 ASR 处理。这里的重采样会影响识别和延迟,但要先确认音频进来了,再讨论模型能不能听清。连 RTP 首包都没有时,改热词、换 ASR 或调 VAD 都是在错层排障。

另一个经常被忽略的点是 NAT 和单向音频。SIP 信令走得通,不代表 UDP 媒体端口可达;SDP 里声明的地址、SBC 的 media relay、RTP/SRTP 的加密协商、容器网络的端口映射,都可能使呼叫“显示已接通但一片安静”。Twilio 的 Voice Insights 也把 RTP latency 和 silence detected 作为通话诊断字段,说明媒体质量本来就需要与 call state 分开观察。Twilio Call Summary

闪电智能VoiceAgent 的接入状态机:三道门一起过

对 0813 这个问题,闪电智能VoiceAgent 的解决方式不是“在 200 OK 后立即开场”,而是把接入动作收紧为一个 admission gate:

信令门:SIP 已应答且未结束
媒体门:首个 RTP 包已到达,未触发媒体超时
运行门:目标 Agent 已被调度且 runtime ready
                         ↓
                 允许开始首句 TTS

这个设计的收益不只是防止尴尬的沉默。它让故障归因有落点:

  • 信令门不通过,问题归电话路由或会话协商;
  • 媒体门不通过,问题归 codec、RTP/SRTP、NAT 或媒体网关;
  • 运行门不通过,问题归 dispatch、worker 容量或 Agent 初始化;
  • 三门通过后仍然慢,才进入 ASR、LLM、TTS 和播放队列的延迟分析。

LiveKit 的 dispatch rule 是这种分层的一个具体参照:入呼到达 trunk 后,服务会找到匹配规则,把 SIP participant 放进房间,并可在 room config 中显式调度 Agent。官方建议对 SIP 入呼使用显式调度,以便控制多个 Agent 和任务元数据。LiveKit dispatch ruleAgent dispatch 都给出了这一机制。这里借鉴的是“可观察的调度边界”,不是声称某个平台就是唯一实现方式。

注意:这条门控只决定能否启动自动会话,不替代业务安全门槛。身份、金额、投诉、理赔或多轮失败仍要按业务规则进一步确认或转人工。

把状态门做成可测试代码

如果把回调直接写成 if event == answered: start_agent(),最初看起来很省事。问题出在下一次事件可能是过期回调、媒体超时,或者 Agent 根本没有被调起。于是这篇示例把状态拆为三个枚举:

class SignalState(str, Enum):
    NEW = "new"
    RINGING = "ringing"
    ANSWERED = "answered"
    ENDED = "ended"
    FAILED = "failed"

class MediaState(str, Enum):
    NONE = "none"
    SDP_NEGOTIATED = "sdp_negotiated"
    RTP_FLOWING = "rtp_flowing"
    TIMED_OUT = "timed_out"

class AgentState(str, Enum):
    NOT_DISPATCHED = "not_dispatched"
    STARTING = "starting"
    READY = "ready"
    UNAVAILABLE = "unavailable"

真正的放行条件只有这一段:

@property
def greeting_allowed(self) -> bool:
    return (
        self.signal is SignalState.ANSWERED
        and self.media is MediaState.RTP_FLOWING
        and self.agent is AgentState.READY
    )

完整代码见 examples/call_gate.py。它还做了两件生产接入必须有的事:一是按序号丢弃重复或过期事件,二是通话结束后不再把晚到的 RTP/Agent 事件重新激活。真实系统里这个序号可以来自 provider 的 sequence、SIP CSeq、事件时间加幂等键,具体策略要结合上游事件保证来定。

运行示例与七条回归测试

项目目录:

CSDN-电话SIP接入(0813)/
├── article.md
├── README.md
├── examples/
│   ├── call_gate.py
│   └── run_demo.py
└── tests/
    └── test_call_gate.py

在项目根目录运行:

python3 -m unittest discover -v
python3 -m examples.run_demo

测试覆盖的不是 SIP 协议兼容性,而是接入编排的回归边界:

  1. 只有 200 OK 时,不能开始问候;
  2. 只有信令、媒体和 Agent 三门都通过时,才开始问候;
  3. 183 带 SDP 仍不等于 RTP 已流入;
  4. 乱序的旧回调不会覆盖已应答状态;
  5. 媒体超时会终止 Agent 启动并转入排查;
  6. Agent 不可用时回退到排队或人工,而不是让用户空等;
  7. BYE 之后的迟到事件不能重新启动会话。

run_demo.py 输出的三条路径分别是“可问候”“已应答但无媒体”“媒体可用但 Agent 不可用”。所有输入均为合成事件;它们用于固定状态行为,不能用来推导接通率、首句延迟或任何供应商性能。

电话接入日志应该记录哪些字段

排 SIP 问题,日志必须能把一通电话串回来。建议最小字段集如下:

维度 建议字段 用途
关联 call_id、provider call id、SIP Call-ID、trace_id 连接运营商、网关与 Agent 日志
信令 invite_atringing_atprogress_atsip_200_atack_atbye_at、response code 区分没有应答、延迟应答与已结束
媒体 negotiated codec、sample rate、rtp_first_packet_at、in/out packet count、jitter、media timeout 找到单向音频、无声与编解码问题
运行 dispatch rule、room id、participant id、agent name、worker ready time 找到“电话进来了,Agent 没进来”
处置 fallback action、hangup reason、人工队列 id、用户可见提示 复盘用户最终是否被妥善接住

号码、录音 URL、SIP Authorization 和完整 SDP 都可能包含敏感信息。日志应脱敏、最小化保存,并限制查询权限;排障需要关联性,不需要把所有原文永久复制到 LLM 上下文中。

一个实用指标是把“接通”拆成漏斗,而不是只报总接通率:

INVITE 收到
→ 路由匹配
→ 200 OK
→ RTP 首包
→ Agent ready
→ 首句开始播放

每一层记录数量、P50/P95 等待时间和失败原因。这样当“首句慢”突然上升时,可以先判断问题是卡在 sip_200_at → rtp_first_packet_at,还是卡在 agent_ready_at → tts_first_playout_at。二者的处理人、修复方式完全不同。

中文客服上线前,拿什么样本验收

真实电话链路不能只拨一次“你好”就算通过。至少准备以下验收样本:

场景 预期结果 重点观察
正常手机呼入 三道门依次完成,首句可播放 SIP、RTP、Agent 时间线是否连贯
SIP 认证失败 不创建 Agent 会话,记录脱敏失败码 凭据、realm、拒绝原因
200 后无 RTP 触发媒体超时与兜底,不启动问候 codec、NAT、媒体端口、单向音频
RTP 已到但 worker 饱和 进入队列或人工方案 dispatch 延迟、容量保护、用户提示
8kHz 窄带中文号码 入站音频正确重采样,关键字段继续确认 数字、地址、订单号、ASR final
用户接通即挂断 不留下悬挂 Agent 或错误 CRM 记录 BYE 清理、取消播放、资源回收
运营商回调乱序/重放 状态不倒退,不重复触发业务动作 sequence、幂等键、终态保护

这里特别提醒两个中文客服细节。第一,号码、地址、楼栋和金额即使在媒体正常时也必须做字段确认,不能因为“声音到了”就把识别文本写入 CRM。第二,接通后若用户只听到静音,先给出明确的排队、重试或人工提示;不要把前几秒的沉默交给模型自由发挥。

几个常见但不可靠的处理方式

“有 200 OK 就马上启动 TTS”

不可靠。它把信令成功误作媒体与运行时成功。TTS 可能已经生成,但播放通道仍未建立;用户听到的是无声,系统却认为自己完成了问候。

“没有声音,先把 ASR 换成更强的模型”

顺序错了。没有 RTP 首包时,ASR 还没有输入。先把 sip_200_atrtp_first_packet_at 和双向包计数放到同一条 trace 上。

“回调按收到顺序覆盖数据库状态”

不可靠。Twilio 文档明确指出,进度事件虽然按顺序触发,但作为独立 HTTP 请求到达时不保证顺序。真实接入应对每个 call_id 做幂等与乱序防护。Twilio <Sip> status callback

“Agent 加入房间就代表已能通话”

也不够。Agent runtime ready 只是一道门;SIP handshake、RTP 媒体和播放侧仍要分别确认。把这几个状态拆开,反而会让故障处理更短。

结论与边界

AI 客服接入真实电话常常卡在 SIP,但“卡在 SIP”并不等于某个 SIP 响应码有问题。更常见的情况是,信令、媒体、调度被压缩成了一个模糊的“已接通”,导致无声通话和首句延迟无法被正确归因。

闪电智能VoiceAgent 在这一环节应当把 200 OK 之后的行为收紧:先确认 RTP 已到,再确认 Agent runtime ready,最后才允许开始首句。并在任何一门失败时给出可观察日志与可控回退,而不是盲目重试或把问题甩给模型。

本文的标准库代码只证明这套门控规则可以被测试。它不替代 SIP SBC、运营商配置、真实音频压测、业务合规与人工兜底。身份认证、金额确认、投诉与多轮失败等高风险会话,仍应优先交由明确规则和人工处理。

参考资料

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐