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. 一张表看看到底简单在哪

能力原生 asteriskjavajpower-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

Logo

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

更多推荐