Asterisk呼叫中心开发告别样板代码,注解快速实现IVR机器人
文章目录
P.S. 推荐一个大神的教程给想要了解或者学习人工智能知识的读者,这个教程里内容讲解通俗易懂且风趣幽默,对我帮助很大。我想与大家分享这个宝藏教程,请点击下方链接查看, 传送门https://blog.csdn.net/qq_74013365
前言
干过呼叫中心、智能外呼、IVR 机器人的兄弟,先别急着划走。
我知道你们看到标题里"Asterisk"三个字,眉头已经皱起来了。
当年是谁,对着一屏 if (event instanceof XxxEvent) 调到凌晨三点?又是谁,自己 new 了个线程跑 AGI Server,然后服务器一重启,通话全断,客户电话打进来听到的是"您拨打的用户已忙"——其实是你的线程又死了。
今天这篇,就聊聊怎么把这摊苦活,变成几个注解的事儿。
1. 先说说这玩意儿是啥
有个开源项目叫 SmartCall,底下垫着一个 jar,叫 jpower-core-asterisk。
你可以把它理解成:你本来要自己去菜市场买菜、洗菜、切菜、炒菜、洗碗,它直接给你端上来一份——还把锅帮你刷了。
技术栈摆一下:Java 17、Spring Boot 3.5、Spring Cloud 2025、MyBatis-Flex、Asterisk(PJSIP)、Spring AI,前端 Vue 3。
架构是微服务和单体双模,想咋部署咋部署。协议 Apache 2.0,商用不花钱,老板看了流泪,产品看了沉默。
2. 接入?比点外卖还快
先引个依赖,就一行的事儿:
<dependency>
<groupId>top.jpower</groupId>
<artifactId>jpower-core-asterisk</artifactId>
</dependency>
然后写段配置,Spring Boot 自动装配,连 @Bean 都不用你手写:
asterisk:
ami: # AMI:监听全局事件、主动外呼
host: 127.0.0.1
port: 5038
username: admin
password: ******
event: true
agi: # AGI:IVR 交互
port: 4573
context:
in: inbound-dialer
out: outbound-dialer
exten: robot
voice-root-dir: /data/ # 录音根目录
启动之后,它偷偷帮你干完了这些脏活:
登录 AMI,把 ManagerConnection 和 AsteriskServer 两个 Bean 给你建好;拉起 FastAGI Server 监听 4573,跟着 Spring 生命周期一起启停;内置挂机事件管理器、TTS/ASR 的媒体流通道。
你要干啥?你啥都不用干。你就写业务。
我知道你不信。当年我接入个短信 SDK 都得读三天文档,你告诉我这玩意儿配完 yml 就能跑?
真的。它把那些你本来要写、又写不对、写完还得半夜爬起来改的代码,全吞了。
3. AGI:一个注解搞定一路电话
3.1 先看看你以前是怎么写的
原生 asteriskjava,大概长这样:
// 1. 实现 AgiScript
public class MyScript implements AgiScript {
public void service(AgiRequest request, AgiChannel channel) {
try {
channel.answer();
channel.exec("Playback", "hello");
// 挂机检测?自己轮询 channel.getStatus()...
} catch (AgiHangupException e) {
/* 到处 try-catch */
}
}
}
// 2. 自己 new DefaultAgiServer,注册 MappingStrategy,起线程,管理生命周期
// 3. TTS 放音、ASR 识别、录音、收号?全部自己造轮子
看出问题没?
你是个写业务的,结果你一半的代码在跟线程谈恋爱,另一半在跟 try-catch 拜把子。
挂机检测那块最绝——你得自己轮询通道状态,跟等女朋友回消息似的,每隔两秒看一眼"她挂了没她挂了没她挂了没"。
3.2 现在呢?
@Slf4j
@Agi("call.agi") // ← 一个注解,绑定 dialplan 脚本名
@Component // ← 交给 Spring,自动注册进 AGI 路由表
public class AgiHandler extends AgiAbstractScript {
@Override
public void service(AgiSupport support) {
support.answer(); // 接听
support.playback("您好,欢迎致电"); // TTS 实时合成放音
String said = support.radio("请说出您的需求"); // ASR 识别(可被打断)
log.info("用户说:{}", said);
support.hangup(); // 挂断
}
@Override
public void hangup(AgiSupport support) {
// 挂机回调:框架已用「AMI 事件驱动 + 轮询兜底」保证幂等,你只管清理
support.thread().interrupt();
}
}
没了。
没有线程管理,没有生命周期,没有 instanceof,没有挂机轮询,没有 try-catch 地狱。
你就写了一个"接听起来,说句话,听听用户说啥,挂了"。完事。
我第一次看到这写法的时候,以为自己在写一个 Hello World。
3.3 dialplan 也就一行
extensions.conf 里把电话交过来:
[inbound-dialer]
exten = _X.,1,AGI(agi://127.0.0.1/call.agi?type=1) ; 名字对上 @Agi("call.agi") 即可
same = n,Hangup()
@Agi 里写的 value,跟这里的脚本名对上就行。框架启动时自己建好"脚本名 → Bean"的路由表。
你不用管它怎么路由的,就像你点外卖不用管骑手走哪条路。
3.4 AgiSupport:一行代码一个能力
service(AgiSupport support) 里这个 support,是通话级对象——每来一通电话,框架给你 new 一个新的。
它把 IVR 里最常用的动作全封成方法了:
| 一行代码 | 干的事 |
|---|---|
support.answer() / support.hangup() | 接听 / 挂断 |
support.playback("文本") | TTS 实时合成并播放 |
support.radio("提示语") | 边播边 ASR 识别,用户一说话立即打断(barge-in) |
support.received("请输入工号", "#") | 放音后 DTMF 收号 |
support.exec("queue", "kf", ...) | 执行任意 dialplan 应用(转人工队列等) |
support.mixMonitor(url) / stopMixMonitor(id) | 双向录音并自动上传 |
support.getVariable(k) / setVariable(k,v) | 读写通道变量 |
support.streamSession() | 拿全双工裸音频流,对接端到端语音大模型 |
重点夸一下 radio()。
TTS 播报和 ASR 识别是全双工并行的——机器人还在说话,用户一开口,立刻打断。
做过语音机器人的都知道,"能被打断"这四个字,能让你少掉一半头发。以前要自己控制音频流、自己判断 VAD、自己处理抢占,纯 Java 侧控制还不能落磁盘。现在一个方法。
就像你本来要自己给话痨朋友递话筒让他闭嘴,现在他自己识趣。
3.5 想定制?继承一下
@Agi(value = "call.agi", support = AgiSupportImpl.class)
public class AgiSupportImpl extends AgiSupport {
public AgiSupportImpl(AgiChannel channel, AgiRequest request, Thread thread) {
super(channel, request, thread);
// support 是框架反射 new 的,用 SpringUtil 取依赖即可
this.processService = SpringUtil.getBean(IProcessService.class);
}
@Override protected AsrClient initAsr() { /* 按流程配置构建 ASR */ }
@Override protected TtsClient initTts() { /* 构建 TTS */ }
@Override public void realtimeData(Boolean isBot, byte[] pcm, String content) {
// 每一句话(含音频)都回调这里:实时推前端、落库做通话转写
}
}
想换 ASR 厂商?想接自己的 TTS?想把每句话实时推给前端看?
继承,重写,完事。
4. AMI:两个注解,告别 instanceof 地狱
4.1 先看看以前的写法有多离谱
connection.addEventListener(event -> {
if (event instanceof NewChannelEvent e) { /* ... */ }
else if (event instanceof HangupEvent e) { /* ... */ }
else if (event instanceof AgentConnectEvent e) { /* ... */ }
// ...几十行 else if,还得自己管理连接、重连、线程
});
这串 else if 我看着都累。
像不像公司年会抽奖,主持人念一个名字你站起来一次,念一个你站起来一次,最后你自己都忘了自己抽没抽中。
而且连接断了重连?自己写。线程崩了重启?自己写。事件顺序乱了?自己背锅。
4.2 现在的写法
@Slf4j
@Component
@AmiListener({NewChannelEvent.class, HangupEvent.class}) // ← 类上声明「我关心哪些事件」
public class MyListener extends EventAbstractListener {
@AmiEvent(NewChannelEvent.class) // ← 方法上声明「我处理哪个事件」
public void onNewChannel(NewChannelEvent event) {
log.info("新通道:{}", event.getChannel());
}
@AmiEvent(HangupEvent.class)
public void onHangup(HangupEvent event) {
log.info("挂断:{},原因:{}", event.getChannel(), event.getCauseTxt());
}
}
框架自动干了三件事:
第一,把所有带 @AmiListener 的 Spring Bean 收集起来;
第二,按注解里的事件类型过滤——只有你订阅的事件才投递给你,别想骚扰你;
第三,反射派发到对应的 @AmiEvent 方法。
你只写"处理逻辑"。订阅、过滤、分发、连接管理、重连,全是框架的事。
你就像个只负责点菜的顾客,厨房是人家的。
4.3 两个注解怎么分工
@AmiListener({...}) 打在类上,是订阅白名单——这个监听器关心哪些事件,留空就收全部。
@AmiEvent(Xxx.class) 打在方法上,是分发路由——事件进来,自动调到标注了它的方法。
一个类可以同时处理十几种事件,各自独立成方法。
举个真实例子,话单监听器 RecordCallListener:
@Component
@Order(20)
@AmiListener({NewChannelEvent.class, NewCallerIdEvent.class, QueueCallerJoinEvent.class,
AgentConnectEvent.class, VarSetEvent.class, HangupRequestEvent.class, HangupEvent.class})
public class RecordCallListener extends LinkedEventListener {
@AmiEvent(NewChannelEvent.class)
public void newChannel(NewChannelEvent e) { /* 建话单 */ }
@AmiEvent(QueueCallerJoinEvent.class)
public void join(QueueCallerJoinEvent e) { /* 转人工入队 */ }
@AmiEvent(AgentConnectEvent.class)
public void connect(AgentConnectEvent e) { /* 坐席接通 */ }
@AmiEvent(HangupEvent.class)
public void hangup(HangupEvent e) { /* 结算话单 */ }
}
一个方法管一个事件,清爽得像刚整理完的桌面。
4.4 想回看这通电话之前发生了啥?
业务经常有这种破事:判断到底是用户先挂的,还是坐席先挂的?
你得回头看这通电话的完整事件链。
把基类从 EventAbstractListener 换成 LinkedEventListener,就能拿到按 linkedId 聚合的整条事件链:
// CACHE 里是这通电话按时序排列的所有事件,随便回溯
boolean userHungFirst = CACHE.get(event.getLinkedId()).stream()
.filter(e -> e instanceof HangupRequestEvent)
.anyMatch(e -> isUserChannel(((HangupRequestEvent) e).getChannel()));
框架内部用"监听器计数"保证:所有监听器都处理完,事件才写入链路缓存。多监听器读到的视图完全一致。
你不用管这些细节,继承就完事了。
4.5 主动外呼?注入 Bean 直接用
模块已经把 AsteriskServer 装配好了,配合 OriginateAction 几行代码就能发起外呼。
更骚的是,外呼服务本身就是个监听器——一边外呼一边监听挂机回填结果:
@Service
@AmiListener({HangupRequestEvent.class, HangupEvent.class})
public class CallServiceImpl extends EventAbstractListener implements CallService {
private final AsteriskServer asteriskServer; // 直接注入
public CallVO call(String phone, String lineId) {
OriginateAction action = new OriginateAction();
action.setChannel("PJSIP/" + phone + "@" + lineId);
action.setContext(asteriskProperties.getContext().getOut());
action.setExten(asteriskProperties.getContext().getExten());
action.setPriority(1);
action.setTimeout(60000L);
asteriskServer.originateAsync(action, callback); // 异步发起
// ...
}
@AmiEvent(HangupEvent.class)
public void onHangup(HangupEvent e) { /* 回填呼叫结果 */ }
}
划重点:@AmiListener 不只能打在专门的 Listener 上,任何 Spring Bean 都行,包括 @Service。
灵活得像一块万能抹布。
5. 一张表看看到底简单在哪
| 能力 | 原生 asteriskjava | jpower-core-asterisk |
|---|---|---|
| AGI 脚本注册 | 手写 Server + MappingStrategy + 起线程 | @Agi("x.agi") + @Component |
| AGI 生命周期 | 自己管理 | 随 Spring 自动启停 |
| 挂机检测 | 自己轮询通道状态 | 框架 AMI 事件驱动 + 轮询兜底,幂等 |
| TTS 放音 / ASR 识别 | 自己对接、自己造轮子 | playback() / radio() 一行搞定 |
| AMI 连接 | 自己建、自己登录、自己重连 | 自动装配成 Bean |
| AMI 事件分发 | 一屏 if (event instanceof ...) | @AmiListener + @AmiEvent |
| 事件链路回溯 | 自己缓存、自己管理 | 继承 LinkedEventListener |
| 主动外呼 | 自己拼 Action、自己管回调 | 注入 AsteriskServer 即用 |
结论就一句话:以前要写几百行样板、踩无数坑的活,现在几个注解、几行代码搞定。
你把省下来的时间,拿去干嘛不好?摸鱼都行。
6. 这些简单的注解,上面长出了啥
底座这么简单,上层就能长出花来:
🧠 AI 智能应答——接了通义千问、DeepSeek 做意图识别,radio() 的全双工打断让机器人对话接近真人,还接了 MaxKB 知识库做多轮问答。
🔊 端到端实时对话模型——基于 streamSession() 的全双工裸流,音频直通大模型服务端,模型自己完成"听—想—说",首响延迟更低,服务端 VAD 打断。
📞 可视化 IVR 流程编排——LogicFlow 拖拽式设计,Say / Answer / Intention / Agent / Transfer / Extract / Script 一堆节点,每个节点背后就是 AgiSupport 的一个方法。
📋 通话记录(CDR)——LinkedEventListener 的事件链路缓存,撑着话单归因、转接记录、呼损统计。
📤 智能外呼——OriginateAction + AMI 挂机监听,批量外呼、智能重拨、多机器人并发。
👥 坐席管理 / 转人工——AMI 事件驱动的坐席状态监控、队列分配、通话保持与转接。
你看到的每一个"智能"功能,底层都是这几个注解在稳稳地跑。
7. 凭啥值得你试试
通信底座极简,二次开发门槛低,本文的例子你拷过去就能跑。
AI 能力齐全:大模型意图识别、ASR/TTS 多厂商可插拔、端到端实时对话、知识库智能体,全是开箱即用。
架构够企业级:微服务 + 单体双模、多租户、RBAC、OAuth2、网关限流、SkyWalking 链路追踪。
部署够省心:完整 Docker Compose 编排,Asterisk 镜像一键拉起,支持内网私有化。
开源可商用:Apache 2.0。
8. 去哪看
Gitee 仓库自己搜 SmartCall,官网 qidiangk.com。
我就不贴链接了,免得像软文。
但如果你正在做智能客服、AI 外呼、IVR 语音机器人,或者受够了原生 asteriskjava 那一坨样板代码——真的,去看一眼。
你的头发,会感谢你的。
P.S. 推荐一个大神的教程给想要了解或者学习人工智能知识的读者,这个教程里内容讲解通俗易懂且风趣幽默,对我帮助很大。我想与大家分享这个宝藏教程,请点击下方链接查看,传送门https://blog.csdn.net/qq_74013365
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐



所有评论(0)