YTalk呼叫平台以及手拨在催收场景中的应用

一、语音通信的衍生

1. 传统电话的实现

1876年贝尔发明世界上第一部电话,当时最基本的通话原理即声音传播带动受话器的震动形成电流信号,电流信号在导线传播最终在另一方的听筒形成声音,此时一方即可听到另一方发出的声音。但是有线电话必须有一条电话线最终通向运营商,为了顺应小型化以及移动化的潮流,移动电话才是方向。

2. 移动电话的诞生

1973年4月3日,全球第一台真正意义上的手机摩托罗拉 Dyda TAC 8000X诞生。自此,通话设备虽然还是依然笨重,但是终于摆脱了电话线实现了真正的移动通信。以后移动通信的原理从仅仅支持语音的1G模拟信号到2G数字信号的出现一直发展到如今的5G通信技术,通话的质量以及能够承载的信息量、速度都有了质的飞跃。

3. 互联网电话的发展

与此同时,互联网也在高速发展,出于对业务系统和呼叫功能的高效结合的追求,VOIP(Voice over Internet Protocol)应运而生,VOIP只要有互联网连接,无论他所处的地方,即使没有手机信号塔,他都能够拨打语音电话。由于一切构建于已经发展完善的Internet之上,使得VOIP的开发成本相对低很多也更容易推广,例如我们经常使用的微信语音也属于VOIP的范畴。2011年WebRTC开源促进了桌面PC基于浏览器的语音通信的发展,到今天,我们的业务人员给一台电脑上接上耳机麦克风,在浏览器里就可以一边和用户通话一边为用户办理业务,大大降低了业务人员的操作复杂度,提升了服务效率。

二、洋钱罐呼叫平台

1. 业务背景:

鉴于洋钱罐本身业务有外呼以及呼入的需求,此前我们的外呼业务大多依赖于第三方公司,由于业务黑盒、定制化需求无法做到,我们于2019年开始了呼叫平台YTalk的开发。本着易于扩展和稳定成熟的目的,经过调研之后我们确定基于Freeswitch的二次开发作为主要方向。我们先后在平台开发了以下功能,为公司的多个业务提供了通话能力支持。

  • 手动呼出:手动呼出是一个呼叫平台最基础的功能,在界面输入号码或者通过点击系统中的号码即可呼出拨打到用户的手机上和用户通话,通话的同时可以为用户在同一界面办理业务,是公司内部应用最广泛的功能。

  • IVR:拨打用户的号码,待用户接起之后播放一段音频;一般用于还款提醒、召回等简单通知类场景,不需要和用户有交互。

  • 预测式外呼:对于用户的接通率以及坐席的空闲率进行实时计算,预测下一轮拨打的接通以及处理情况、调节自动拨打的速率。在用户接起之后我们才将通话转接给坐席,在减少坐席的等待时间和高效的拨打用户号码之间取得动态平衡。

  • 智能外呼:拨打用户号码,用户接起后根据用户说的话通过ASR(Automatic Speech Recognition,语音识别,音频转文本)以及意图理解等流程判断用户表达的意思,然后返回相应的话术、通过TTS(Text-to-Speech,文本转语音)播放出来,从用户的角度来看就是有来有回的说话,类似于真人沟通,当然鉴于各个流程的耗时以及TTS的仿真程度依然无法比拟人工。

  • 一键多呼:一键拨出一批号码,最先接起的用户和坐席通话,其他人可以根据业务需求挂断或者转入IVR、智能外呼。

  • 呼入:用户通过手机拨打400或者其他号码即可呼入我们的系统,通过语音提示和按键等操作可以实现自助查询、转接人工坐席等等操作。目前主要用于客服业务。

目前,Ytalk在我们公司的应用范围包括:

  • 催收业务:手动呼出、IVR、智能外呼、一键多呼、预测式外呼、呼入。
  • 营销业务:手动呼出、IVR。
  • 保险业务:手动呼出、IVR、呼入。
  • 客服业务:手动呼出、呼入。
  • 报警平台:IVR。

2. Freeswitch介绍

在第一部分中我们说到,通话A、B双方需要有线路连接来传递语音电流,那么如果A想打给C怎么办呢?需要A和C之间也有线路连接吗?如果所有用户之间两两相连,那么硬件成本是无比巨大的。因此,交换机出现了,交换机是一个可以根据规则将通话双方进行拆线以及接线的装置。早期手摇固话A想打给B,一般拨出之后会由接线员接起,A口头告诉接线员想打给谁,接线员人工将AB双方的线路接线在一起,这时B才开始响铃。后来用户变多,接线员无法人工操作大量的线路,于是被叫号码和自动交换机出现了,自动交换机可以根据被叫号码的规则自动进行接线和拆线。

在物理线路的交换机中,作为电话运营商肯定能够知道哪条线路对应的是哪个号码,那么在移动通信的情况下,如何做到通过被叫号码找到用户呢?其实手机在开机阶段就会有一个注册过程,表示将这个号码注册到附近的几个基站上。当然,用户在移动的情况下还会出现切换基站的情况。这样,运营商知道了被叫所在位置,即可通过被叫附近的基站来通知被叫来电话了。

类似于移动通信,基于VOIP的网络电话也是一样的。首先用户也会有一个注册过程,将自己的IP以及端口等信息保存在软交换机,软交换机在收到A的通话请求之后会根据被叫信息查询出B的地址信息,以此来通知B,在我们的系统中充当注册中心的角色就是Freeswitch。

不像常见的硬件级交换机,Freeswitch是纯软件的,围绕Freeswitch进行二次开发可以实现很多定制化功能,开源的环境也让它在很多平台都有应用,具有一个较好的开发以及维护环境。Freeswitch包含一些基本的功能,比如:坐席注册,网关对接,通话呼叫发起、音频流的传输,通话录音以及音频文件保存,IVR菜单以及按键交互等等。

3.SIP - 会话初始协议

上面提到了流程中包含了两个主要步骤:注册以及拨打。这样的操作是通过什么样的一种协议来进行控制的呢?那就是SIP:SIP全称是Session Initiation Protocol,会话初始协议。它是一个基于文本的应用层IP 语音会话控制协议,用于创建、修改和释放一个或多个参与者的会话。首先,SIP协议可以承载于UDP或者TCP,属于应用层协议,但是在webrtc中也可以承载于websocket 进行传输。其次SIP传输的协议内容是文本格式的,可以直接通过抓包或者浏览器开发控制台看到其传输内容。对于Freeswitch来说,一通电话的发起、回铃音的协商、用户接通、用户挂机乃至按键DTMF信号的传递,都离不开SIP协议的支持。下面我们来看看这两个主要步骤的过程,限于篇幅我对内容会做一些精简:

注册流程:

其中UA代表坐席(User Agent)

  • 1.坐席号为8801的坐席发起REGISTER注册,其中第一行包含了本次消息的类型,域名以及协议版本;from 和 to分别代表消息的发起方和接收方,注册消息中都是8801;call-id代表一次会话的标识,本次注册流程所有消息的call-id都是一致的;
1
2
3
4
5
REGISTER sip:opensips-test.yangqianguan.com SIP/2.0
To: <sip:8801@opensips-test.yangqianguan.com>
From: <sip:8801@opensips-test.yangqianguan.com>;tag=jkmn1j53l4
Call-ID: nenr9bkuf95rqur5a9jjoj
Contact: <sip:8801@opensips-test.yangqianguan.com>;expires=600
  • 2.Freeswitch收到之后鉴权发现注册没有附带相关授权信息,于是回复401表示需要鉴权,其中WWW-Authenticate字段包含了鉴权的随机字符串以及加密算法等信息。
1
2
3
4
5
SIP/2.0 401 Unauthorized
From: <sip:8801@opensips-test.yangqianguan.com>;tag=2c13hqji91
To: <sip:8801@opensips-test.yangqianguan.com>;tag=rKB1g622gN8Ze
Call-ID: nenr9bkuf95rqur5a9jjoj
WWW-Authenticate:Digest realm="opensips-test.yangqianguan.com", nonce="07577657-7c5d-451d-8c1b-6c1e1d1efd06", algorithm=MD5
  • 3.坐席拿到上一步返回的鉴权信息对本地密码进行计算,附带上计算出来的信息再次发起注册REGISTER,其中Authorization中包含了计算出来的结果,Freeswitch对自己存储的坐席密码进行同样的加密逻辑之后如果结果和坐席发来的信息匹配代表坐席的密码正确,注册成功。整个注册过程真实密码不会进行传输。
1
2
3
4
5
6
REGISTER sip:opensips-test.yangqianguan.com SIP/2.0
To: <sip:8801@opensips-test.yangqianguan.com>
From: <sip:8801@opensips-test.yangqianguan.com>;tag=2c13hqji91
Call-ID: nenr9bkuf95rqur5a9jjoj
Authorization: Digest algorithm=MD5, username="8801",nonce="07577657-7c5d-451d-8c1b-6c1e1d1efd06",response="0570ba0a5efebbfb354634d549431123", cnonce="m83dogd7nimg"
Contact: <sip:8801@opensips-test.yangqianguan.com;transport=wss>expires=600
    1. Freeswitch返回200 OK消息表示注册成功,此时Freeswitch已经记录下8801坐席的地址信息,可以通过该信息给8801发消息了。
1
2
3
4
5
SIP/2.0 200 OK
From: <sip:8801@opensips-test.yangqianguan.com>;tag=2c13hqji91
To: <sip:8801@opensips-test.yangqianguan.com>;tag=Sv4Sj1K6Dyyja
Call-ID: nenr9bkuf95rqur5a9jjoj
Contact: <sip:8801@172.20.64.31:5060;type=wss>;expires=220

呼叫流程

下图就是一个典型的通过Freeswitch 坐席A向手机号码B发起通话的主要SIP流程,其中包括几个主要步骤:通话发起、回铃音、接通、挂机。这里摘取主要的几个消息进行展示。

  • 通话发起:此阶段包括了消息编号1、2、3、4、5;

    • 1.A向Freeswitch发送INVITE请求发起通话,第一行同样包含消息类型以及目的uri,协议版本。INVITE中的from和to即标识了通话的发起方(8801)和目的方(18812341234),Call-ID同样用于标识整个会话。Content-Type为SDP标识此消息包含语音协商信息,即下面消息体中的IP地址(IP4 10.0.13.xxx)和端口号(audio 58361)

      1
      2
      3
      4
      5
      6
      7
      8
      INVITE sip:18812341234@opensips-test.yangqianguan.com SIP/2.0
      To: <sip:18812341234@opensips-test.yangqianguan.com>
      From: <sip:8801@opensips-test.yangqianguan.com>;tag=knutvg0g37
      Call-ID: 370kqsd7pfmcdb8g2md5
      Content-Type: application/sdp

      o=- 476668281734765389 2 IN IP4 10.0.13.xxx
      m=audio 58361 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126
  • 2.Freeswitch发现INVITE消息中没有鉴权信息,类似于注册,回复407要求鉴权。

  • 3、4.A收到407之后会回复ACK表示已经收到要求,然后通过本地密码进行计算,同样生成鉴权信息附带在INVITE中再次发起通话,此处不再重复展示,只是相对于1号INVITE多了部分鉴权信息,其他完全一样。

  • 5.Freeswitch收到合法的INVITE之后会向线路方B发送通话请求,这里解释下什么是线路方,简单的理解线路方就是VOIP以及传统语音通信商的桥梁,允许我们通过VOIP直接拨打手机号码。其实Freeswitch发送SIP是发给线路方而不是用户的手机,线路方再通过运营商才能联系上用户手机,这里为了简化模型可以理解为B是线路方同时也是被叫手机号码。由于此时是Server端向Client端发起,B不会要求鉴权。我们可以看到,跟1号INVITE很像,223.123.123.123是线路方的IP,INVITE中的消息体内容即音频流协商信息:IP地址(47.123.123.123,即我们Freeswitch的公网IP),端口(30036)。

    1
    2
    3
    4
    5
    6
    7
    8
    INVITE sip:18812341234@223.123.123.123:6977 SIP/2.0
    From: "8801" <sip:051212341234@223.123.123.123:6977>;tag=pepv6UFXeHHaF
    To: <sip:18812341234@223.123.123.123:6977>
    Call-ID: 8f9732c0-5346-123a-e59e-00163e142701
    Content-Type: application/sdp

    o=FreeSWITCH 1624918288 1624918289 IN IP4 47.123.123.123
    m=audio 30036 RTP/AVP 102 9 0 8 103 104 101
  • 回铃应协商:此阶段包括消息编号6、7

    • 6.被叫B收到Freeswitch的INVITE请求之后,首先开始在手机上振铃提醒用户,然后会根据5号INVITE中包含的IP以及端口,将自己的回铃音音频流发送给Freeswitch,同时回复一条183 Session Progress,其中消息体中包含了被叫B的IP(223.123.123.123)以及端口(10498)用于接受音频流。此时,Freeswitch和被叫B都已经获得对方的音频流IP和端口,可以进行音频流传输,但此时B还没接起,一般是回铃音阶段。

      1
      2
      3
      4
      5
      6
      7
      8
      SIP/2.0 183 Session Progress
      From: "8801" <sip:051212341234@223.123.123.123:6977>
      To: <sip:18812341234@223.123.123.123:6977>
      Call-ID: 8f9732c0-5346-123a-e59e-00163e142701
      Content-Type: application/sdp

      o=- 1624948324 1624948324 IN IP4 223.123.123.123
      m=audio 10498 RTP/AVP 8 96
    • 7.类似于6的流程,此时A与Freeswitch的回铃音协商是通过4号INVITE以及7号183 Session Progress,此时A坐席已经可以听到B设置的回铃音了。

  • 通话接起:此阶段包括消息编号8、9

    • 8.坐席B按下接听键之后,会发送200 OK消息到Freeswitch,类似于回铃音,200 OK消息中包含了通话语音流的IP以及端口信息,结合5号INVITE中的IP和端口信息即可实现坐席B能接收和发出语音流了。

      1
      2
      3
      4
      SIP/2.0 200 OK
      From: "8801" <sip:051282670444@223.123.123.123:6977>
      To: <sip:18812341234@223.123.123.123:6977>
      Call-ID: 8f9732c0-5346-123a-e59e-00163e142701
    • 9.同理8,9的流程协商了坐席A和Freeswitch的语音IP和端口信息,至此,A和B已经能通话了,A说出的话转化成音频流由Freeswitch转发发送给B,B收到音频流播放出来就能听到了,反之亦然。

  • 通话挂断:此阶段包括消息编号10、11、12、13

    • 10、11. 通话完成之后,坐席A先按下挂断按钮,此时向Freeswitch发送了BYE消息要结束会话,11是Freeswitch对挂断请求的回复表示请求收到。

      1
      2
      3
      4
      BYE sip:18812341234@172.20.64.32:5062;transport=udp SIP/2.0
      To: <sip:18812341234@opensips-test.yangqianguan.com>;tag=6ggF0887eZmtS
      From: <sip:8801@opensips-test.yangqianguan.com>;tag=q09i4pbimg
      Call-ID: 370kqsd7pfmcdb8g2md5
    • 12、13. Freeswitch收到BYE请求后同样向坐席B发起BYE请求,B坐席收到后回复ACK表示收到请求,同时坐席B的界面也结束了通话,至此整通电话流程结束。

这些消息在坐席的浏览器侧都是可以在开发者模式下查看websocket内容看到的:

4. 实现

Freeswitch是一套以C语言开发的软件,而我们的常用技术栈是Java,为了协调两套语言,整个系统是以事件作为核心流转。其实对于我们的Java服务来说只需要做到两件事情:控制(操作Freeswitch进行一系列功能实现)和看到(知道Freeswitch发生了什么)。

  • 控制:Freeswitch 启动后会启动一个内置的TCP server,默认监听8021端口。双方通过ESL(Event Socket Library)进行交互。ESL也是一套纯文本协议,采用socket tcp长连接的方式,减少了不必要的资源创建及释放时间,用于通过命令对Freeswitch进行控制。以下是部分常用函数:

    • chat:可以给坐席发送消息,传输文本信息
    • originate:拨打一通电话
    • bridge/uuid_bridge:通过唯一标识桥接通话到另一通电话,使两通电话可以互相交流
    • park:通话被挂起,等待后续操作
    • uuid_broadcast:通过唯一标识广播语音或者命令到通话中
    • uuid_kill:通过唯一标识终止通话
    • uuid_transfer:通过唯一标识将通话转入一些特定的流程、执行函数
    • wait_for_answer:通话接起后执行后续操作
    • playback:播放一段语音
    • record_session:给整通电话录音
    • event:订阅事件
    • export:设置一些自定义变量
  • 看到:Java服务在启动后会创建一个client连接上8021端口,然后通过命令: event plain all 订阅一系列通话相关的事件。Freeswitch对于每通电话的每个状态的流转都会触发一个事件,然后将事件内容发送给订阅了该事件的所有client,Java服务收到后就可以根据事件内容作出一系列控制操作以及数据库状态更改。下面是一些常用的事件:

    • CHANNEL_CREATE:通话创建
    • CHANNEL_PROGRESS:通话收到回铃音
    • CHANNEL_ANSWER:通话接通
    • CHANNEL_PARK:通话被挂起
    • CHANNEL_HANGUP_COMPLETE:通话挂断
    • sofia::register:坐席登录
    • sofia::unregister:坐席登出

    比如可以在收到通话创建事件时写库、收到通话接起事件时修改状态为通话中、收到通话挂断事件时修改通话状态为已挂断等等。我们来看一个手机接起时CHANNEL_ANSWER的事件简化版,可以看到包含了事件名称、主被叫、唯一标识等一系列信息,便于我们存储和判断。其他事件内容格式大同小异,不再赘述。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    Event-Name: CHANNEL_ANSWER //事件名称
    FreeSWITCH-IPv4: 172.20.64.XX //所在Freeswitch机器ip
    Event-Date-Timestamp: 1626070207465863 //事件时间戳
    Event-Sequence: 156792 //事件编号
    Channel-Name: sofia/external/18812341234 //通道名
    Unique-ID: e7544e92-201a-4747-821c-e3f066e6cfc9 //通道唯一标识
    Answer-State: answered //通话状态
    Caller-Caller-ID-Number: 1000 //主叫坐席号
    Caller-Destination-Number: 18812341234 //被叫号码
    variable_sip_gateway_name: gateway1 //使用的网关

这样,我们通过代码能够知道正在发生的事情,也能够通过命令控制Freeswitch做我们想要的操作,我们就可以灵活的实现我们想要的功能啦。

三、手拨场景与应用

前面介绍了大量的呼叫方面的概念以及知识,接下来我们从实际场景中看看都是怎么用到这些东西的。
坐席手动拨打号码是一个最为常见的呼叫场景,比如在催收场景,用户在平台的借款如果逾期会进入几个阶段,我们在相应的阶段会通过电话的形式通知借款人按时还款,或者如果对客户有什么不懂的地方还能予以操作步骤的帮助。例如,坐席登录系统后的界面类似于下图:

坐席可以打开一个委派给自己的案件,打开案件系统就会显示用户的相关资料,点击用户的手机号码我们即可自动实现呼出。


当然,为了客户隐私考虑我们的所有号码都是进行了脱敏以及加密处理的。后续坐席可以根据通话的结果更新客户资料以及进行之后的持续跟踪。拨打完成也可以在界面上记录催记信息。

我们来看看手拨的具体流程,以及系统是如何控制以及实现手拨的。手拨时序图如下:

主要流程:

  1. 坐席通过SIP消息发起INVITE通话请求之后,首先Freeswitch收到请求后会触发坐席的CHANNEL_CREATE事件,表示一通电话开始,我们收到事件后会进行通话记录的创建、坐席状态的修改等一系列操作。
  2. 在Freeswitch的配置中坐席一旦呼出会自动执行park命令将坐席挂起,并同时触发CHANNEL_PARK事件通知Ytalk,等待后续指令。
  3. Ytalk收到PARK事件之后知道有坐席要呼出,首先要进行线路选择,选线逻辑主要包括:盲区(过滤掉不能拨打该号码归属地的线路)、限频(过滤掉已经打过多次的线路)、并发(过滤掉当前并发已满的线路)、评分规则(根据剩余可用线路的接通率选择)等。
  4. 选线完成之后会通过 ESL命令 bridge(sofia/gateway/gateway1/18812341234)控制Freeswitch使用指定的线路呼叫被叫用户并将坐席桥接给新通话,这里命令中的gateway1就是最终选出来的线路的标识。此时也会收到用户的CHANNEL_CREATE事件。从坐席的角度看:呼出->PARK时间->选线->拨打桥接 的耗时很短,所以几乎没有感知到这一系列操作。
  5. 桥接完成,坐席在耳机中就能听到手机的拨打回铃音了。用户手机一旦开始振铃,线路方会通过SIP消息 SIP/2.0 183 Session Progress 通知Freeswitch,Freeswitch就会触发CHANNEL_PROGRESS事件,Ytalk收到该事件将通话状态修改为振铃中。
  6. 用户接起之后线路方会通过SIP消息200 OK 通知Freeswitch,Freeswitch会触发CHANNEL_ANSWER事件,此时我们可以根据事件将通话状态改为通话中。
  7. 通话双方有一方挂机都会导致通话结束,挂断方会通过SIP消息BYE 通知Freeswitch,此时双方都会触发一个CHANNEL_HANGUP_COMPLETE事件,Ytalk收到后会将通话状态更改为已挂断。

可以看到,在上面的流程中SIP消息、Event事件、ESL控制都结合在了一起才实现了一个简单的手拨外呼功能。

四、现阶段使用情况

目前生产环境的外呼数量在高峰时期能够达到60万通电话每天,平时基本也能够保持45万通电话每天,处理的事件量最高能够达到230万个每天。

当然手拨作为最基础的外呼功能,呼叫量只占一小部分,大部分的外呼量都是自动的任务所占据例如IVR以及预测式外呼。

后期呼叫平台还会接受更大的业务量的考验,一方面推送的任务量会越来越多,每天100万的通话量也不会很遥远,如何在保证接通质量的情况下提升速度、如何优化现有架构保证事件的及时消费都将是我们要关注的问题。另外,关于呼叫系统本身的优化点,例如稳定性、高可用、监控报警的完善、灵活的配置也是我们努力的方向。