把语音智能体从"网页里试听"搬到真实电话线上,问题往往会集中出现在三个地方:外呼根本发不出去、接通了却没有声音、能通话但用户体验很差。下面按"现象 → 原因 → 逐条排查 → 兜底 → 预防"的顺序,把一条真实外呼链路从 SIP 中继到合规要求走一遍。
报错现象
在测试环境以网页或 WebRTC 方式对话一切正常,把出口换到真实电话线后,出现三类典型症状。
第一类:任务下发后立刻失败,电话根本没有振铃。
```
[ERR] originate failed: SIP/2.0 403 Forbidden
INVITE sip:138xxxxxxxx@trunk.example.net SIP/2.0
[ERR] originate failed: SIP/2.0 503 Service Unavailable
[ERR] originate failed: SIP/2.0 408 Request Timeout
```
也常见 404 Not Found、484 Address Incomplete、486 Busy Here、480 Temporarily Unavailable、603 Decline。表现是任务表里状态直接变 failed,attempts 加一,但被叫方手机没有任何反应。
第二类:接通了,但单向或双向无声。
典型的 SDP 长这样:
```
m=audio 0 RTP/AVP 0 8 101
```
或者 offer 里 c=IN IP4 10.0.3.17,是私网地址。表现是对方"喂"了几声就挂断,或者智能体自己说完开场白后一直等不到回应,2 秒后被对端 BYE。
第三类:通话能建立,但体验不成立。
用户插话时智能体继续说;用户说"等一下",声音还要拖 1~2 秒才停;打断后残留半句话再接着播;同一号码短时间内被反复拨打;上线当天收到投诉说"不知道是机器人"。
影响范围通常是全量新任务,也可能是某个中继、某个号段、某个时段的偶发问题。
可能原因
按线上出现概率从高到低排:
1. 中继鉴权或主叫号码未报备:账号密码错、IP 白名单没配、主叫号码未在运营商侧报备或不允许外呼显示,对端直接 403/404。
2. 出口 NAT 导致 SDP 携带私网地址:信令能通、媒体不通,表现为单通或无声。
3. 号码资质与运营商侧限呼:号段用途不符、被标记为营销外呼、触发频次管控,表现为 480/603 或接通即挂。
4. 编解码协商失败:对端只支持 PCMA/PCMU,本地只 offer Opus,回来 488 或 m=audio 0。
5. 外呼参数错误:被叫号码带空格横线、缺国家码、主叫号码写成非报备号、并发超过中继上限。
6. VAD 打断链路缺失:TTS 整段生成整段下发、ASR 结果回传延迟,导致无法中断。
7. 失败重拨策略缺失或过激:408 不重试、486 疯狂重试,形成骚扰式拨打。
8. 合规要素缺失:没有录音告知、没有拒呼名单、没有频次与时段限制。
逐条排查与解决
第 1 条:中继与号码资质
先看网关状态。以 FreeSWITCH 为例:
```bash
fs_cli -x "sofia status"
fs_cli -x "sofia status gateway gw1"
```
State 应为 REGED。若为 NOREG,检查用户名密码、realm、传输协议(UDP/TCP/TLS)与端口。
再看信令。开 sip trace 并用 sngrep 观察完整对话:
```bash
fs_cli -x "sofia profile external siptrace on"
sudo tcpdump -i any -n -s0 -w /tmp/sip.pcap 'port 5060 or port 5061'
```
判断方法:
- 抓包里 INVITE 已发出、对端明确回
403 Forbidden→ 鉴权或主叫号码白名单问题,与网络无关。 - 完全没有响应、本地超时成
408→ 网络不通或 IP 白名单未加。用下面两条命令验证通路:
```bash
nc -vz trunk.example.net 5060
openssl s_client -connect trunk.example.net:5061 -servername trunk.example.net </dev/null
```
号码资质部分要主动向中继商确认三件事:主叫号码是否已完成企业实名与用途报备;该号码是否允许作为外呼显示号码(有些号码只可被叫);目标号段是否在允许外呼的范围内。具体材料清单与审核口径以运营商和当地通信管理部门的要求为准。
第 2 条:SDP 与 RTP 单通
看抓包里 INVITE 和 200 OK 的 c= 行。如果是 10.x、192.168.x、172.16~31.x,说明本机把私网地址发出去了。
解决方式是把公网地址显式写进 SDP。FreeSWITCH 在 vars 里配置:
```xml
<X-PRE-PROCESS cmd="set" data="external_rtp_ip=203.0.113.10"/>
<X-PRE-PROCESS cmd="set" data="external_sip_ip=203.0.113.10"/>
```
同时确认防火墙放行 RTP 端口区间(FreeSWITCH 默认 16384–32768,可在 switch.conf 调整),UDP 双向放行,并关掉路由器上的 SIP ALG。
判断是不是这条原因,最直接的办法是看 RTP 双向包量:
```bash
sudo tcpdump -i any -n -c 200 'udp and portrange 16384-32768'
```
只有一个方向有包,基本可以确认是 NAT 或端口未放行。
第 3 条:编解码
```bash
fs_cli -x "show codec"
```
抓包看 SDP 的 m=audio 行:RTP/AVP 8 0 101 表示 PCMA/PCMU/telephone-event,是电话线最常见的组合。若对端回 488 Not Acceptable Here,或 m=audio 0,就是协商失败。
处理方式是在中继网关上限定编码,避免 offer 里出现对端不支持的项:
```
codec-prefs=PCMA,PCMU
```
智能体侧注意采样率:电话线是 8k,TTS 输出若是 16k 或 24k,需要在送入 RTP 前重采样,否则会出现变速、破音。
第 4 条:外呼参数与并发
先用一条最小命令验证单通外呼:
```bash
fs_cli -x "originate {origination_caller_id_number=010xxxxxxxx,origination_caller_id_name='Service',ignore_early_media=true,originate_timeout=45}sofia/gateway/gw1/8613800000000 &park()"
```
要点:被叫号码用 E.164 格式,去掉空格、括号、横线;国内固话带区号;主叫号码必须是已报备的那个,不要临时换号。
判断方法:
404 Not Found→ 路由或被叫号码格式问题。484 Address Incomplete→ 号码不完整。503且并发高峰出现 → 大概率是中继并发上限,跟运营商确认并发数,用fs_cli -x "show channels count"对照当前占用。
第 5 条:VAD 打断
打断的本质是:上行 RTP → 解码 → 流式 ASR → 判定"用户在说话" → 立刻停止 TTS 播放并清空待发队列 → 把输入权交给用户。
三个关键点:
一是分片下发。TTS 不要一次性合成整段再播放,而是按 20ms 一帧流式推送,这样队列才可被清空。整段下发是"打断延迟 1~2 秒"的最常见原因。
二是起始确认窗。连续 3~5 帧(约 60~100ms)判定为语音才触发打断,避免一个咳嗽就掐断播报。
三是双讲检测。电话线存在侧音与回声,智能体自己的声音会通过回声路径回到 ASR,造成"自己打断自己"。需要保留一份播放参考信号做回声消除,或只在 ASR 置信度高于阈值时才允许打断。
伪代码示意:
```python
async def on_asr_partial(text, is_speech, tts, asr):
if is_speech and tts.is_playing() and not tts.in_grace(300):
tts.stop() # 停止播放并清空待发队列
await asr.flush() # 丢弃 TTS 播放期间的残余识别结果
```
判断方法:用户没说话却频繁被打断 → 查回声与阈值;用户说了"等一下"仍要等很久才停 → 查是否整段下发、是否 stop 只停了播放器没清队列。打断后建议给一句短确认("嗯,您说"),避免冷场。
第 6 条:失败重拨
按 SIP 响应码分类处理,策略写进任务表:
| 响应码 | 含义 | 处理 |
|---|---|---|
| 486 / 480 / 603 | 忙、暂时不可用、拒接 | 隔较长时间(如次日或半天)再试,最多 1~2 次 |
| 408 / 503 / 500 | 网络或平台侧异常 | 指数退避重试 |
| 404 / 484 | 号码错误或不完整 | 不重拨,标记无效 |
| 接通但 < 5 秒挂断 | 未形成有效触达 | 不计有效,换时段再试 |
表结构至少包含 attempts、next_retry_at、last_sip_code:
```sql
UPDATE tasks
SET attempts = attempts + 1,
next_retry_at = now() + (interval '1 minute' * power(2, attempts))
WHERE id = $1 AND attempts < 2;
```
同一号码在一小时内重复拨打,既伤体验也容易触发投诉。
第 7 条:合规要素
技术跑通之后,合规是必须落地的一环,缺一项都可能让整个项目停摆:
- 身份与录音告知:开场白明确说明这是智能语音助手,通话可能被录音。具体表述与是否需要事前同意,以当地法规为准。
- 拒呼名单:识别"不要再打了""退订""删除我的号码",立即写入黑名单,并在每次外呼前校验。
- 频次限制:同一号码每日、每周设上限,具体数值依据当地规范与运营商要求,以官方页面为准。
- 时段限制:默认避开休息时段(如 21:00 至次日 9:00,各地不同)。
- 真实主叫:主叫号码必须可回拨,并提供人工或留言通道。
- 录音管理:加密存储、设定保留期限、记录访问审计。
- 人工兜底:用户说"转人工"时必须能转,且有明确的转接失败处理。
都不管用时的兜底方案
1. 用软电话做隔离测试。用同一个中继账号在软电话上注册,拨打自己的手机。软电话也不通 → 问题在中继或号码侧,不在智能体代码。
2. 绕过 ASR/TTS。用 Playback 播一段固定音频,验证媒体通路本身是否正常。能播通说明是智能体链路的问题,播不通说明是 SIP/RTP 的问题。
3. 换中继、换主叫号码做 A/B。能快速区分"号码被限呼"和"平台侧故障"。
4. 向运营商要话单和对端 release cause。对端记录的挂断原因,往往比自己抓包更准确。
5. 降级为回拨模式。先振铃挂断再回拨,或先发短信再引导呼入,在合规前提下降低被拦截的概率。
6. 灰度放量。先用 20 个内测号码跑通全流程,再逐步扩量,避免一次性触发风控。
如何预防再次发生
- 建立拨测号码池:覆盖本网、异网、固话、移动、空号、关机、忙线、拒接、彩铃等场景,每次变更后跑一遍。
- 盯住几个核心指标:SIP 失败码分布、接通率(ANSWER/ATTEMPT)、平均振铃时长、RTP 双向包量比(单向音频告警)、打断延迟 P95、重拨占比、退订与投诉数。
- 变更管理:换中继、改 SDP、改 codec、改开场白话术,都视为需要回归的变更。
- 日志留全:call-id、origination_uuid、主被叫、SIP 响应码、录音路径,缺一项回溯成本就翻倍。
- 合规进流程:新场景上线前过一遍告知话术、频次策略、黑名单机制与录音保留策略,而不是等投诉来了再补。
从一条命令能拨通,到一百条任务稳定跑完不被投诉,中间隔着的正是上面这些细节。先把单通电话跑通,再逐项把打断、重拨和合规补上,链路才算真正可用。
