资讯动态

FreeSWITCH ACD模块开发:从mod_callcenter配置到自研实践

发布时间:2026/10/5 16:05:46 来源:尧图企业网站定制
FreeSWITCH的ACD模块开发是我这几年在呼叫中心项目里被问到最多的需求之一。很多人一开始觉得ACD是个黑盒子其实拆开看就是一套把来电按照策略分给坐席的逻辑。真正难的不是路由判断本身而是排队体验、坐席状态同步、超时处理以及和业务系统的衔接。这篇文章就把我从配置mod_callcenter到自研ACD模块的完整思路讲清楚包含Windows下安装FreeSWITCH的注意事项、队列和坐席怎么配、C模块骨架怎么写、Park/Hold在排队里怎么用以及几个我踩过的坑。1. 核心概念与整体设计思路1.1 先搞懂ACD到底要解决什么问题ACD全称是Automatic Call Distribution自动呼叫分配。在呼叫中心里它承担的角色很简单当客户来电时系统不能无脑把电话接到某个分机上而是要判断当前哪些坐席空闲、哪些坐席技能匹配、队列里已经排了多少人再决定这通电话是马上接通、进入排队还是播放提示音。国内很多项目把ACD和排队机混为一谈这个理解不准确。排队只是ACD的一种表现ACD还需要处理分配策略、坐席状态、超时溢出、黑名单、VIP客户优先、技能组匹配等逻辑。从运维角度看还要关注三个关键指标接通率、平均等待时长、服务水平。开发ACD模块之前先要把这几个业务指标拆解成技术需求否则写出来的东西只是“能响铃、能接电话”离一个可用的呼叫中心还有距离。1.2 FreeSWITCH为什么适合做ACDFreeSWITCH本质上是一个软交换但它和传统交换机最大的区别在于模块化和媒体控制能力。你可以用Dialplan把呼叫路由到任意应用可以接mod_callcenter做排队可以用mod_event_socket把状态和事件抛给上层业务系统也可以自己写一个C模块挂进核心。这种灵活性对于ACD这种业务逻辑强、媒体控制又复杂的场景特别合适。另外FreeSWITCH对音频处理的支持也很完整。排队的时候需要给客户放保持音、播报当前排队位置、回铃音处理、录音这些都可以通过内置的playback、park、hold、record等应用组合出来。相比用asterisk硬凑队列逻辑FreeSWITCH的mod_callcenter已经内置了很多成熟的排队参数直接配置就能覆盖大部分场景。1.3 两条开发路线改配置与写模块做ACD不能一上来就写代码先要确定路线。我在项目里一般分三种情况业务比较标准坐席数量不多直接用FreeSWITCH自带的mod_callcenter通过callcenter.conf.xml配置队列和坐席Dialplan里调用callcenter应用。这是最快最稳的方案。业务规则复杂比如需要根据客户等级动态调整排队策略、坐席需要实时从业务系统拉取或者队列状态要同步到Web端我会选择自研一个ACD模块或者用ESL外部服务来控制。如果是分布式呼叫中心多台FreeSWITCH需要共享队列状态那就要考虑Redis或者数据库作为队列存储再通过ESL对接每台FS。方案优点缺点适合场景mod_callcenter配置内置成熟稳定社区案例多定制能力有限策略不够灵活标准排队、坐席不多、快速上线自研C模块性能最好深度控制媒体和路由开发周期长调试门槛高对路由策略、性能有极端要求ESL外部服务灵活业务接入快多一跳网络重连处理麻烦动态路由、坐席系统和Web联动我个人的建议是能用mod_callcenter解决的不要自己造轮子。自研模块一定要有明确的业务理由比如需要预测式外呼、复杂技能匹配、多租户隔离这些才值得投入人力。2. 基于mod_callcenter的快速落地配置含Windows环境2.1 Windows安装FreeSWITCH与模块确认很多初学者会在Windows上折腾FreeSWITCH。官方提供了Windows安装包装起来不算难但有几个点需要注意。安装完成后默认路径一般是C:\FreeSWITCHbin目录下有fs_cli.exe和FreeSwitchConsole.exe。如果你想让FreeSWITCH在后台跑可以注册成Windows服务但没有特殊需求时用控制台挂着跑更方便看日志。进fs_cli之后先确认mod_callcenter是否加载。输入module_exists mod_callcenter如果返回false打开C:\FreeSWITCH\conf\autoload_configs\modules.conf.xml把mod_callcenter这一行取消注释然后执行reload mod_callcenter另外要注意Windows下路径分隔符和权限问题。保持音乐文件、录音文件尽量不要放在带空格的目录否则配置解析容易出问题。我遇到过不少Windows环境下的奇葩问题最后发现都是目录权限或者文件路径写错导致的宁可把路径写简单点也不要用中文目录。2.2 队列、坐席、技能组三层模型mod_callcenter的模型很清晰分为Queue队列、Agent坐席、Tier层级。Queue就是排队队列一个队列代表一类业务比如售前、售后、投诉。Agent是坐席一个坐席可以属于多个队列。Tier用来描述坐席和队列的隶属关系以及在这个队列中的级别和优先级。Level越高坐席优先级越高Position表示在同一个Level内的顺序数字越小越靠前。这个三层模型一定要理解透。常见的误配置是把Agent直接塞进Queue忽略了Tier。实际上mod_callcenter的路由是先把队列对应的Tier加载出来按优先级排序再根据策略选择坐席。座席的状态也比较重要。mod_callcenter里坐席有Available、Available On Demand、On Break几个状态。状态不对队列永远不给他派话。默认情况下坐席需要显式登录并置为Available系统才会认为它可以接电话。2.3 callcenter.conf.xml配置示例Windows下默认配置路径是C:\FreeSWITCH\conf\autoload_configs\callcenter.conf.xml。一个最小可用的配置长这样configuration namecallcenter.conf descriptionCall Center queues queue namesupportdefault param namestrategy valuelongest-idle-agent/ param nametime-base-score valuesystem/ param namemax-wait-time value60/ param namemax-wait-time-with-no-agent value10/ param nametier-rules-apply valuefalse/ param nametier-rule-wait-second value30/ param nametier-rule-wait-multiply-level valuetrue/ param nametier-rule-no-agent-no-wait valuefalse/ param namediscard-abandoned-after value60/ param nameabandoned-resume-allowed valuefalse/ param namemax-agent-wait-time value20/ param namering-attempt-delay value5/ param nameno-agent-delay value5/ param nameagent-timeout value15/ param namemoh-sound value$${hold_music}/ param namerecord-template value$${base_dir}/recordings/${queue_name}/${strftime(%Y-%m-%d-%H-%M-%S)}.wav/ /queue /queues agents agent name1000 typecallback contactuser/1000 statusAvailable max-no-answer3/ agent name1001 typecallback contactsofia/internal/1001192.168.1.20 statusAvailable max-no-answer3/ /agents tiers tier agent1000 queuesupportdefault level1 position1/ tier agent1001 queuesupportdefault level1 position2/ /tiers /configuration这里的strategy是路由策略我用的是longest-idle-agent也就是优先分配给最长时间空闲的坐席。策略直接决定了用户体验和坐席负载均衡效果具体差异后面专门讲。moh-sound设置排队保持音乐$${hold_music}是FreeSWITCH全局变量默认指向一个内置提示音实际项目中换成你自己的音频文件。2.4 Dialplan接入ACD配置好callcenter之后还要在Dialplan里把来电接到队列。假设客户拨5000进入售后队列extension nameacd_support condition fielddestination_number expression^5000$ action applicationanswer/ action applicationcallcenter datasupportdefault/ action applicationhangup/ /condition /extension注意answer这步很关键。如果不先answer来电在排队期间听到的等待音是回铃音而不是保持音很多客户会误以为没人接。接入层如果用的是SIP中继还要确认外线参数里的ringback是否配置正确。callcenter应用执行后呼叫会进入排队流程。如果坐席空闲系统会立即尝试发起呼叫坐席如果坐席忙或者没有可用坐席来电就进入队列并播放保持音。2.5 命令行管理与状态切换配置写好后用fs_cli可以动态管理队列和坐席这点在实际运维中太常用了。# 重载callcenter配置文件 reload mod_callcenter # 查看队列状态 callcenter_config queue list callcenter_config queue list_members supportdefault # 手动加载坐席 callcenter_config agent load 1000 callcenter_config agent set status 1000 Available callcenter_config agent set contact 1000 user/1000 # 查看队列和坐席绑定关系 callcenter_config tier list supportdefault很多项目里坐席系统是第三方提供的坐席登入登出不会自动通知FreeSWITCH。这时候需要在坐席侧做状态同步最常见的做法是通过ESL调用callcenter_config命令把坐席状态写入FreeSWITCH。我建议把这些命令封装成可重复调用的接口避免人工在fs_cli里面手敲。3. 自己开发ACD模块的核心实现3.1 模块的边界与接口如果mod_callcenter不能满足业务那就需要自研。首先想清楚模块的边界ACD模块负责的是呼叫排队和坐席分配至于坐席界面、工单、客户资料这些属于业务系统不应该混在模块里。我在自研ACD模块时一般让模块暴露两个核心接口一个是给Dialplan调用的应用入口比如acd_queue负责把来电session挂起来等待分配另一个是给坐席系统调用的控制接口负责坐席登入、登出、就绪、忙碌。这两个边界一划后面开发就清晰了。模块内部需要维护一份队列状态至少包含每个队列的等待来电session、每个坐席的空闲状态、坐席平均通话时长、上次空闲时间等。简单场景用内存结构就够分布式场景要外接Redis。3.2 一个最小可用的C模块骨架FreeSWITCH的模块本质是一个动态库通过模块接口注册应用、API、事件钩子。下面是一个简化版的模块骨架演示如何注册一个名为acd_queue的应用。#include switch.h static switch_status_t acd_queue_exec(switch_core_session_t *session, const char *data) { switch_channel_t *channel switch_core_session_get_channel(session); switch_event_t *params NULL; char *queue_name NULL; if (zstr(data)) { switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, acd_queue require queue name\n); return SWITCH_STATUS_FALSE; } queue_name strdup(data); switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, call enter acd_queue: %s\n, queue_name); switch_core_session_answer(session); /* 这里需要实现真正的ACD逻辑 */ /* 1. 把当前session放入队列等待区 */ /* 2. 查找符合条件的空闲坐席 */ /* 3. switch_ivr_originate外呼坐席 */ /* 4. 坐席应答后bridge客户和坐席 */ /* 5. 超时无人接听重新排队或溢出 */ free(queue_name); return SWITCH_STATUS_SUCCESS; } SWITCH_MODULE_LOAD_FUNCTION(mod_acd_load) { switch_application_interface_t *app_interface; *module_interface switch_loadable_module_create_module_interface(pool, modname); SWITCH_ADD_APP(app_interface, acd_queue, ACD Queue Application, ACD routing, acd_queue_exec, acd_queue queue_name, SAF_MEDIA_ONLY); return SWITCH_STATUS_SUCCESS; } SWITCH_MODULE_SHUTDOWN_FUNCTION(mod_acd_shutdown) { return SWITCH_STATUS_SUCCESS; } SWITCH_MODULE_DEFINITION(mod_acd, mod_acd_load, mod_acd_shutdown, NULL);这段代码不能直接编译运行但结构是完整的。真正开发时你需要在load函数里注册坐席状态管理的API在exec函数里写队列调度逻辑。核心思路是客户来电session不能被阻塞要让它进入队列等待同时用另一个线程或定时器去扫描坐席找到坐席后再把客户和坐席bridge起来。3.3 队列管理和路由策略的落地自研模块的路由策略是重头戏。mod_callcenter内置了几种策略我先把它们列出来自研的时候可以参考策略行为适用场景ring_all同时呼叫队列内所有可用坐席坐席少、需要快速接通longest-idle-agent优先呼叫最久空闲的坐席负载均衡常规呼叫中心fewest-calls优先呼叫累计接听次数最少的坐席坐席接听量差异大random随机选一个坐席简单场景rr-ordered按Tier顺序轮流分配固定坐席组自己实现时最核心的数据结构不是普通队列而是按“空闲时长”或“接听次数”排序的优先队列。比如longest-idle-agent策略需要记录每个坐席上次通话结束时间每次路由时找出空闲且时间最早的坐席。我建议用Redis实现共享状态因为多台FreeSWITCH也可以共用。坐席状态存成hash等待的来电存成list路由时用lua脚本原子性地取出坐席。这样可以避免多实例下“同一个坐席被同时路由两通电话”的经典问题。3.4 与Park/Hold、录音等媒体能力联动自研ACD时最容易忽略的是媒体控制。客户进入队列后不能让这个session一直空转必须进入park或者hold状态同时播放保持音。FreeSWITCH里的park和hold经常一起用park是把呼叫临时挂起hold是给通道一个保持状态。mod_callcenter内部已经处理了这层逻辑。但你自研模块时就需要自己把客户session放到保持状态。这里有两条路一种是在Dialplan里先用hold_music播放保持音再调用ACD应用另一种是在模块内部用switch_ivr_sleep循环播放或者用switch_core_media的broadcast来处理。extension nameacd_with_hold condition fielddestination_number expression^5000$ action applicationanswer/ action applicationset datahold_musiclocal_stream://moh/ action applicationacd_queue datasupport/ /condition /extension录音也要提前想好。客户从进线到坐席接通全程录音还是只录坐席通话部分这里的实现位置完全不同。如果全程录音尽量在进入队列后就启动录音如果只录通话就等到bridge成功后再录音。我之前接过一个项目客服要求保留排队录音用于质检但一开始只在bridge后录音结果客户排队时说的投诉内容全丢了返工很痛苦。3.5 用ESL做分布式ACD的混合方案自研C模块门槛很高。如果团队主要写Java/Python/Node更务实的路线是“FreeSWITCH ESL 外部业务服务”的混合方案。mod_event_socket允许外部程序通过TCP控制呼叫ACD状态和路由都放到外部服务里。基本流程是客户来电进入一个Dialplan扩展通过socket应用把呼叫交给外部服务或者用ESL的originate命令主动发起呼叫。外部服务维护一个全局的坐席状态表接到新呼叫事件后按策略选好坐席再通过ESL发起桥接。api originate user/1000 bridge(sofia/internal/1234192.168.1.10)这种方案的好处是业务开发快对接CRM、工单、坐席软电话都很方便。坏处是每个呼叫多了一次事件交互如果ESL连接不稳定要自己处理重连高并发下要特别注意消息处理的吞吐量。我一般推荐日呼叫量在几万以下的场景用这套方案几十万以上的再考虑C模块。4. 高频问题与排查经验4.1 队列有人进来但坐席不接话这个是我被问得最多的。现象是电话打进来能听到保持音队列里也有成员但坐席就是不响铃。排查路径一般是先看坐席是否在这个队列的Tier里再看坐席状态是不是Available最后看坐席的contact是否能被FreeSWITCH呼通。callcenter_config agent list callcenter_config tier list supportdefault如果坐席contact写的是user/1000但分机没有注册到FreeSWITCH那坐席肯定是呼不通的。生产环境里我更喜欢把contact写成具体的SIP地址这样即使分机注册状态有异常也能快速定位。4.2 坐席状态不同步有的项目用数据库保存坐席状态FreeSWITCH这边和数据库经常不一致。最典型的是坐席软电话异常退出FreeSWITCH侧还认为它在线结果客户等了几十秒才听到超时提示。我的经验是一切以FreeSWITCH侧状态为准业务系统通过API同步不要反过来。坐席登入、登出、置忙都必须主动调用callcenter_config命令。为了兜底可以在FS侧做一个定时巡检检查坐席分机的注册状态如果分机已经掉线自动把坐席状态置为On Break。4.3 排队时不放音乐只有回铃音这个问题基本是Dialplan没有在进入队列前执行answer或者answer后没有设置hold_music。记住顺序answer、设置保持音、然后进callcenter。另外有时候外线网关的回铃音策略也会干扰。如果进线走的是SIP中继且中继配置了early media有可能在FS answer之前就把回铃音给了客户。这时候需要在网关侧关掉early media或者调整FS的ringback参数。4.4 Park/Hold相关坑用mod_callcenter时排队本质上是把客户session挂起这个过程和Park机制有直接关系。坑点在于如果你在排队过程中需要做“转接”比如客户按0转人工而当前坐席一直没接这时候不要把客户从队列里强行转移否则session状态会乱。我踩过一次在队列等待时让客户按0转VIP队列我写了个IVR让session在队列里执行transfer结果坐席接通后发现只有坐席听得到客户客户听不到坐席。原因是在transfer过程中把原有通道的hold状态带过去了坐席方向没建立媒体。后来处理方式是让客户退出当前队列再作为一个新呼叫进入VIP队列。4.5 高频问题速查表症状可能原因处理办法进队列后一直没坐席接入坐席未登录或状态不是Availablecallcenter_config agent set status 1000 Available坐席不振铃Tier没配置或contact写错检查tier list和分机注册状态保持音乐不生效没answer或hold_music路径错误确认Dialplan顺序和音频文件坐席接听后客户听不到声音排队中transfer导致媒体错乱避免在队列中transfer改用重排队队列里有成员但状态显示未激活callcenter模块配置没reloadreload mod_callcenter高并发时坐席被重复分配多实例共享状态没有加锁引入Redis或消息队列做状态管理还有一个容易被忽略的点max-wait-time不要设成很大。有的项目希望客户能一直排队就把等待时间设为9999结果队列堆积了几十通电话坐席刚挂断一通系统自动桥接下一通坐席连喝水的时间都没有反而让接通率更差。合理的做法是设置一个最大等待时间超时就播放提示音并挂断或者溢出到语音信箱。排队体验不是越长越好这个道理在技术参数上同样成立。我在实际项目里最深的体会是ACD模块开发的核心不是“把电话转给某个坐席”而是“在正确的时间把正确的呼叫交给正确的坐席”。无论是用mod_callcenter还是自研先把队列模型、坐席模型、超时策略、媒体控制这些细节理清楚代码只是水到渠成的事情。最后再分享一个小习惯每次改完callcenter配置先用fs_cli跑一遍callcenter_config queue list再模拟一通测试电话确认队列有成员、坐席能振铃、双方能通话三步都过了再上线。这个习惯帮我挡掉了不少线上事故。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑