摘要
环境:Asterisk 18 LTS + app_queue,日进线约 600 通的快运企业查件队列。解决什么问题:查件来电集中在 9:00—11:00 和 14:00—16:00,默认排队语音只报人数不报时长,客户容易挂断。读完能拿到什么:可直接套用的 queues.conf 队列语音参数、一个用 AMI 查询队列状态并生成预估时长音频的 AGI 脚本、分段播报话术模板、上线测试清单。适合 5—30 坐席、单队列日进线 1000 通以内的小企业运维参考。
一、为什么查件高峰期的排队语音值得单独配置
快运查件来电的集中度很高。我们拉了两周数据,工作日 9:00—11:00 和 14:00—16:00 两个时段的查件进线量占全天六成以上,上午 9:30—10:30 是绝对峰值。默认排队语音在这个时段暴露三个问题:
只报排队人数,不报时间。客户对“前面还有 8 位”没有时间概念,焦虑感反而更强。
播报节奏和实际等待不匹配。有的系统 15 秒播一次,客户听两遍就想挂。
话术缺少业务引导。查件客户带着货物时效焦虑,语音里如果没有自助查件入口的提示,客户只能干等。
自定义排队语音的目标不是“好听”,是用可预期的信息降低挂断率。把排队位置、预估等待时长、可替代的自助查件入口三件事说清楚,客户愿意多等的概率会明显提升。我们改造后统计了两周工作日同时段数据(进线量约 600 通/日,挂断率按队列 abandon 统计),从改造前的约 18% 降到约 11%,样本有限,仅供参考。
二、排队等待语音由哪几类音频组成
在 Asterisk 的 app_queue 里,客户进入队列后听到的声音分四类,搞清楚这个分类,配置时就不会乱:
| 进队列提示音 | queue-youarenext 等内置提示音 | 客户刚排队时的第一段语音 |
| 队列位置播报 | announce-position | 告知当前排第几位,受 announce-position-limit 限制 |
| 预估等待时长播报 | announce-holdtime | 按队列平均等待时长播报,可选 yes/no/once |
| 周期播报 | periodic-announce + periodic-announce-frequency | 每隔固定秒数循环播放自定义音频 |
announce-holdtime 和 periodic-announce 的区别是:前者用系统算的平均等待时长,颗粒度粗、话术固定;后者挂自定义音频文件,内容完全可控,适合做分段话术和动态预估。我们最终关掉了 announce-position,只用 periodic-announce 承载全部播报,控制权在自己手里,避免两段音频同时触发叠音。
云呼叫中心平台的逻辑类似,只是把配置文件换成了控制台里的“队列管理—排队播报”模块,支持上传音频或选 TTS 模板。小企业没有专职运维的话,云平台控制台配置更省事;有一定技术力量的团队,用开源方案自己控音频文件,灵活度更高。下面配置示例以 Asterisk 为主。
三、配置前要准备什么
3.1 拉高峰数据,定播报节奏
按半小时粒度统计最近两周的进线量和平均排队时长。我们当时的数据是:峰值时段平均排队 47 秒,最长超过 3 分钟。这个数据决定了播报频率取 30 秒还是 45 秒,也决定了话术里“预估时长”的分档区间。
同时确认队列最大等待人数 maxlen。小企业坐席有限,队列堆到 20 人以上时预估时长会失真,语音里应该引导客户用自助查件,而不是继续报一个不准的数字。
3.2 话术设计的三条规矩
排队语音不是广告位,不要塞无关内容。我们定了几条:
-
第一遍只报“已进入查件队列 + 当前排队人数”,不报时长,刚进来时预估误差大。
-
第二遍开始报预估等待时长,按区间报,“预计等待 1 到 2 分钟”,不报精确到秒。
-
超过 2 分钟未接起的,追加一句自助查件引导,话术总时长控制在 12 秒以内。
3.3 录音还是 TTS
坐席流动快、话术调整频繁,纯录音维护成本高。我们的做法是固定话术用录音,动态数字用 TTS 拼接。比如“您前面还有”和“位客户”用录音,“X”位和“分钟”用 TTS 动态生成。如果嫌拼接麻烦,整段用 TTS 也行,现在开源 TTS 的普通话自然度对小企业场景够用。
四、Asterisk 队列语音配置实战
4.1 环境说明
-
Asterisk 18 LTS,app_queue 模块
-
音频目录:/var/lib/asterisk/sounds/kuaiyun/
-
AGI 脚本:Python 3,放 /var/lib/asterisk/agi-bin/
-
AMI 账号:在 manager.conf 里开一个只读账号,权限给 read = call,reporting
4.2 queues.conf 关键参数
编辑 /etc/asterisk/queues.conf:
ini
[check-queue]
musicclass=default
strategy=ringall
timeout=15
retry=5
maxlen=25
joinempty=yes
leavewhenempty=no
; 关闭位置播报,避免和周期播报叠音
announce-position=no
announce-holdtime=no
announce-frequency=0
; 自定义周期播报,30秒一次
periodic-announce=kuaiyun/periodic-status
periodic-announce-frequency=30
; 内置提示音替换为中文录音
queue-youarenext=kuaiyun/youarenext
queue-thereare=kuaiyun/thereare
queue-callswaiting=kuaiyun/callswaiting
queue-thankyou=kuaiyun/thankyou
关键点:announce-frequency 设 0 关掉系统默认的位置+时长播报,全部交给 periodic-announce。这样只有一条播报通道,不会出现两段音频抢同一个时间点。
4.3 音频文件准备与响度对齐
录音用 8kHz 单声道 16bit WAV,sox 转换:
bash
sox raw_youarenext.wav -r 8000 -c 1 -b 16 kuaiyun/youarenext.wav
TTS 部分用 edge-tts 生成再重采样:
bash
edge-tts –text "预计等待一到两分钟" –write-media /tmp/tts.wav
sox /tmp/tts.wav -r 8000 -c 1 -b 16 /var/lib/asterisk/sounds/kuaiyun/periodic-status.wav
注意两点:TTS 输出通常是 24kHz,不重采样直接放会变调;播报音频和保持音的 RMS 差值控制在 2dB 以内,我们第一次上线时播报比背景音乐低 6dB,客户反馈听不清,后来统一做了响度归一化。
4.4 用 AMI 查询队列状态并生成预估音频
静态音频解决不了“预估时长”,需要一个 AGI 脚本在客户进队列时算一次。核心是用 AMI 的 QueueStatus 动作拿等待人数和平均等待时长。下面用 panoramisk 库写一个可跑的示例(pip install panoramisk):
python
#!/usr/bin/env python3
import asyncio
import os
import panoramisk
from asterisk.agi import AGI
AMI_HOST = '127.0.0.1'
AMI_PORT = 5038
AMI_USER = 'queue_reader'
AMI_SECRET = 'change_me'
AUDIO_PATH = '/var/lib/asterisk/sounds/kuaiyun/periodic-status.wav'
LOCK_PATH = '/tmp/kuaiyun-tts.lock'
async def fetch_queue_info(queue_name):
manager = panoramisk.Manager(host=AMI_HOST, port=AMI_PORT,
username=AMI_USER, secret=AMI_SECRET)
await manager.connect()
try:
resp = await manager.send_action({'Action': 'QueueStatus',
'Queue': queue_name})
params = resp.get('QueueParams', {})
waiting = int(params.get('Calls', 0))
holdtime = int(params.get('Holdtime', 0))
return waiting, holdtime
finally:
await manager.close()
def build_text(waiting, holdtime):
# holdtime 是队列当前平均等待秒数,waiting 是排队人数
estimate = max(holdtime, 60) * max(waiting, 1) / max(waiting, 1)
# 简化处理:用平均等待时长做区间分档
if holdtime < 60:
return "预计等待不到一分钟"
if holdtime < 120:
return "预计等待一到两分钟"
if holdtime < 180:
return "预计等待两到三分钟"
return "预计等待超过三分钟,建议您使用自助查件"
def tts_to_wav(text, path):
tmp = '/tmp/kuaiyun-tts-raw.wav'
os.system(f'edge-tts –text "{text}" –write-media {tmp} >/dev/null 2>&1')
os.system(f'sox {tmp} -r 8000 -c 1 -b 16 {path}')
def main():
agi = AGI()
queue_name = agi.env.get('agi_arg_1') or 'check-queue'
waiting, holdtime = asyncio.run(fetch_queue_info(queue_name))
text = build_text(waiting, holdtime)
# 简单文件锁,避免并发进队列时互相覆盖
if not os.path.exists(LOCK_PATH):
open(LOCK_PATH, 'w').close()
try:
tts_to_wav(text, AUDIO_PATH)
finally:
os.remove(LOCK_PATH)
agi.verbose(f'queue={queue_name} waiting={waiting} holdtime={holdtime}s text={text}')
if __name__ == '__main__':
main()
几个说明:QueueStatus 返回的 Holdtime 是队列当前平均等待秒数,直接拿来分档比“人数×平均通话时长”更贴近实际;文件锁只防同一时刻的覆盖冲突,并发量大的场景建议按队列名分文件;生产环境把 AMI 账号权限收到最小,只给 read = call,reporting。
4.5 extensions.conf 挂载
ini
[from-trunk]
exten => 8001,1,Answer()
same => n,Playback(kuaiyun/welcome-check)
same => n,Queue(check-queue,G(kuaiyun-estimate^s^1))
same => n,Hangup()
G 参数表示客户进队列时执行一次 AGI,s 表示脚本正常返回后继续排队,1 是传给脚本的队列名参数。客户一进来 periodic-status.wav 就是按当前队列情况刷新的版本,后续 30 秒一次的周期播报读到的都是相对准确的预估。
五、话术模板与分段播报策略
不同等待阶段播不同内容,比一句话从头循环自然。我们实际用的分段策略:
| 刚进队列 | “您好,您已进入查件队列,当前前面有 X 位客户,请稍候。” | 进队列立即 |
| 等待 30 秒 | “您前面还有 X 位客户,预计等待 1 到 2 分钟。” | 第 30 秒 |
| 等待 60 秒 | “预计等待 2 到 3 分钟,您也可以关注公众号使用自助查件。” | 第 60 秒 |
| 等待 120 秒以上 | “当前排队人数较多,建议使用自助查件,或稍后再拨。” | 每 30 秒循环 |
“X 位客户”用 TTS 动态生成,其余录音拼接。录音音质稳定、语气可控,TTS 只负责数字和短词,自然度要求不高。
六、上线测试清单
配置改完别直接切生产,按这个清单走一遍:
模拟 3 个并发来电进队列,确认第一遍播报按时触发,无叠音。
queue show check-queue 核对播报的排队人数和实际是否一致。
观察 periodic-announce 是否按 30 秒循环,periodic-status.wav 是否被 AGI 正确刷新(看文件修改时间)。
挂断后查 CDR,确认 AGI 脚本异常没有导致队列卡死。
上线后盯两个指标:队列挂断率和平均等待时长。挂断率降但等待时长升,说明语音留住了人但坐席处理没跟上,要同步调排班;两个都降才是语音真正起效。
我们上线第一周每天对比改造前后同时段数据,第三天后挂断率稳定在 11% 左右才把配置固化。建议给 periodic-announce 留一个注释开关,大促或异常天气进线暴增时快速切回简化话术,避免复杂播报占用通道资源。
七、常见问题与避坑
Q1:周期播报和队列位置播报叠音怎么办?
一句话结论:关掉 announce-position,只留 periodic-announce。两套播报各有自己的频率参数,错开设置不如直接关一套,控制权集中在一个通道最稳。
Q2:TTS 音频变调、语速不对怎么办?
一句话结论:检查采样率是否统一为 8kHz。TTS 输出通常是 16kHz 或 24kHz,不重采样直接放会变调;语速用 TTS 引擎参数调,别用 sox tempo 硬拉。
Q3:多个客户同时进队列,预估音频文件互相覆盖怎么办?
一句话结论:按队列名分文件,并发大的加文件锁。小企业单队列峰值并发进队列通常个位数,上面脚本里的简单文件锁够用,再大就按 queue_name 拆文件。
Q4:云呼叫中心平台不支持 AGI 脚本怎么办?
一句话结论:用平台控制台的变量播报功能。多数云平台在队列管理里支持话术模板插入 {waiting_count}、{estimate_time} 这类变量,逻辑和开源方案一致,只是脚本换成了内置变量替换。有些团队选优音通信这类服务商提供的呼叫中心能力,控制台里同样有排队播报配置入口,按模板填变量即可,不用自己维护音频文件。
Q5:改了语音客户还是挂断,问题在哪?
一句话结论:看挂断时间点。集中在进队列 10 秒内,说明第一段话术没留住人,开头要更直接;集中在 60 秒后,说明预估时长不准或坐席接起太慢,回头核对 Holdtime 参数和排班。
八、方案边界:这套配置适合什么规模
这套方案在我们日进线 600 通左右、5—12 坐席、单查件队列的环境跑了半年,稳定性没出过问题。适合的边界大致是:
-
坐席规模 5—30 人,单队列日进线 1000 通以内,Asterisk 单机扛得住。
-
队列数不超过 5 个,每个队列独立音频文件,文件锁和刷新逻辑简单。
-
如果日进线超过 2000 通、多队列并发、需要跨地域容灾,建议上云平台或专业呼叫中心系统,自维护 Asterisk 的音频刷新和 AMI 查询会成为瓶颈。
小企业运维人手有限,配置越简单、依赖越少,长期维护成本越低。这套配置的核心就是四步:拉高峰数据定节奏、设计分段话术、用 periodic-announce 挂自定义音频、用 AMI 查队列状态动态刷新预估。配置本身半小时能改完,测试和调优留三到五天,用真实进线数据验证再固化。
九、小结
快运小企业呼叫中心排队语音自定义,技术门槛不高,难的是把“播什么、什么时候播、播多准”和实际进线数据对上。本文给的 queues.conf 参数、AMI 查询脚本、分段话术模板和测试清单可以直接复用,环境是 Asterisk 18 LTS,其它版本参数名基本一致。希望这篇配置记录能给遇到同样问题的同行省点摸索时间。



