做过呼叫中心 / 智能外呼 / IVR 语音机器人的同学都知道,Java 对接 Asterisk 是个体力活:AGI 要自己注册脚本、管理线程和生命周期、自己轮询检测挂机;AMI 要自己建连接、写一个巨大的 onManagerEvent,然后在里面堆一屏 if (event instanceof XxxEvent)。光是把这些样板代码搭起来,一两天就没了,还处处是坑。

今天介绍的开源项目 SmartCall(AI 客服呼叫中心),把这一切收敛成了几个注解。它的通信基座 jpower-core-asterisk 让「实现一个 AGI 脚本」「监听一个 AMI 事件」都变成了几行代码的事。这篇文章就用最直白的对比,带你看清它到底有多简单。

一、SmartCall 是什么?

SmartCall 是一套基于 AI 大模型 + Asterisk 通信引擎 的新一代智能客服呼叫中心系统,深度融合了 AI 语音机器人、可视化 IVR 流程编排、实时 ASR/TTS、大模型意图识别、端到端实时对话模型等能力,覆盖从「呼入智能应答」到「智能外呼」的全链路。

  • 技术栈:Java 17 + Spring Boot 3.5 + Spring Cloud 2025 + MyBatis-Flex + Asterisk(PJSIP) + Spring AI,前端 Vue 3。
  • 架构:微服务 / 单体双模,可独立部署也能一键启动;原生多租户、RBAC、OAuth2。
  • 协议:Apache 2.0,允许商用。

而它「打电话」这件事的所有底座能力,都来自 smart-aster 模块 + jpower-core-asterisk 这个 jar。下面我们就聚焦这个 jar,看看 AGI 和 AMI 是怎么被它「注解化」的。


二、5 分钟接入:一个依赖 + 一段配置

引入依赖:

<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)媒体流通道。

你要做的,只剩写业务。


三、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 识别、录音、收号?全部自己造轮子

✅ 用 jpower-core-asterisk 的写法:

@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 地狱——@Agi + @Component + 继承 AgiAbstractScript,一个能接听、放音、识别、挂断的 IVR 脚本就成了。

3.2 dialplan 只需一行

Asterisk 的 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.3 AgiSupport:一行代码一个能力

service(AgiSupport support) 里的 support 是通话级对象(每来一通电话框架自动实例化),把 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 识别是全双工并行的,用户一开口就打断机器人,纯 Java 侧控制、无磁盘中转。做「能被打断的语音机器人」最难的一块,这里一个方法搞定。

3.4 需要定制?继承一下就行

@Agi(value = "call.agi", support = AgiSupportImpl.class) 可以指定你自己的支撑类,用来注入 ASR/TTS 引擎、加业务回调。SmartCall 就是这么干的:

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) {
        // 每一句话(含音频)都回调这里:实时推前端、落库做通话转写
    }
}

四、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,还得自己管理连接、重连、线程
});

✅ jpower-core-asterisk 写法:

@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());
    }
}

就这么简单。框架会自动:

  1. 收集所有带 @AmiListener 的 Spring Bean;
  2. 按注解里的事件类型过滤(只有你订阅的事件才投递给你);
  3. 反射派发到对应的 @AmiEvent 方法。

你只写「处理逻辑」,订阅、过滤、分发、连接管理全是框架的事。

4.2 两个注解的分工

  • @AmiListener({...})(类级):订阅白名单,声明这个监听器关心哪些事件,留空则收全部。
  • @AmiEvent(Xxx.class)(方法级):分发路由,一个事件进来自动调到标注了它的方法。

一个类可以同时处理十几种事件,各自独立成方法,清爽得不得了。SmartCall 的话单监听器 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.3 需要「跨事件推理」?换个基类即可

如果业务需要回看「同一通电话之前发生过什么」(比如判断是用户先挂还是坐席先挂),把基类从 EventAbstractListener 换成 LinkedEventListener,就能拿到按 linkedId 聚合的整条事件链:

// CACHE 里是这通电话按时序排列的所有事件,随便回溯
boolean userHungFirst = CACHE.get(event.getLinkedId()).stream()
        .filter(e -> e instanceof HangupRequestEvent)
        .anyMatch(e -> isUserChannel(((HangupRequestEvent) e).getChannel()));

框架内部用「监听器计数」保证所有监听器都处理完,事件才写入链路缓存,多监听器读到的视图完全一致。你不用管这些细节,继承就完事了。

4.4 主动外呼?注入 Bean 直接用

模块已经装配好 AsteriskServer,配合 OriginateAction 几行就能发起外呼。SmartCall 的 CallServiceImpl 甚至本身就是一个监听器(@Service + @AmiListener),一边外呼一边监听挂机回填结果:

@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)都可以,非常灵活。


五、「简单」到底体现在哪?一张表说清

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

结论很直白:原本要写几百行样板 + 踩无数坑的活儿,现在几个注解、几行代码就搞定。 这正是 SmartCall 能把精力集中在「AI 能力」和「IVR 编排」上的原因——通信底座足够简单可靠。


六、这些能力,在 SmartCall 里长成了什么样

上面这些「简单的注解」,最终支撑起了 SmartCall 一整套生产级功能:

  • 🧠 AI 智能应答:集成通义千问、DeepSeek 等大模型做意图识别;radio() 的全双工打断能力让 AI 机器人对话接近真人;对接 MaxKB 知识库做多轮智能问答。
  • 🔊 端到端实时对话模型:基于 streamSession() 的全双工裸流,音频直通大模型服务端,由模型端到端完成「听-想-说」,首响延迟更低,支持服务端 VAD 打断。
  • 📞 可视化 IVR 流程编排:基于 LogicFlow 拖拽式设计,Say / Answer / Intention / Agent / Transfer / Extract / Script 等丰富节点,每个节点背后就是 AgiSupport 的一个方法。
  • 📋 通话记录(CDR):LinkedEventListener 的事件链路缓存,支撑起复杂的话单归因、转接记录、呼损统计。
  • 📤 智能外呼:OriginateAction + AMI 挂机监听,实现批量外呼、智能重拨、多机器人并发。
  • 👥 坐席管理 / 转人工:AMI 事件驱动的坐席状态监控、队列分配、通话保持与转接。

一句话:你看到的每一个「智能」功能,底层都是这几个注解在稳稳地跑。


七、为什么值得试试 SmartCall?

  • ✅ 通信底座极简:AGI/AMI 注解化,二次开发门槛低,本文的例子拿来即用。
  • ✅ AI 能力齐全:大模型意图识别、ASR/TTS 多厂商可插拔、端到端实时对话、知识库智能体,全都开箱即用。
  • ✅ 架构够企业级:微服务 + 单体双模、多租户、RBAC、OAuth2、网关限流、SkyWalking 链路追踪。
  • ✅ 部署够省心:提供完整 Docker Compose 编排,Asterisk 镜像一键拉起,完整支持内网私有化部署。
  • ✅ 开源可商用:Apache 2.0 协议。

八、立即体验

⭐ Gitee 开源仓库:https://gitee.com/gdzWork/SmartCall
🌐 官方网站:https://qidiangk.com


写在最后

Java 对接 Asterisk 曾经是件「脏活累活」,但有了 jpower-core-asterisk 这样的注解化封装,AGI 一个注解、AMI 两个注解,就能把复杂的电话通信能力稳稳地跑起来。而 SmartCall 正是把这套「简单」发挥到极致的开源实践——它让你不必再纠结底层通信,而是把精力真正放在「让 AI 讲好每一通电话」上。

如果你正在做智能客服、AI 外呼、IVR 语音机器人,或者受够了原生 asteriskjava 的繁琐,强烈建议去演示站点点看,或者给项目点个 Star ⭐,你的支持是它持续迭代的最大动力。

Logo

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

更多推荐