четверг, 23 мая 2013 г.

Автоматическая система обзвона клиентов

Как сделать автоматический обзвон написано уже много, в том числе и на этом сайте. Гибкость asterisk'a не имеет границ. Написано огромное количество статей по реализации простейших действий встроенными средствами, либо с использованием сторонних продуктов и решений. Поэтому на мой взгляд наиболее интересно будет решение не стандартной задачи, для которой пришлось полностью с нуля разработать и внедрить систему, учитывая совместимость с текущей схемой колл-центра.

Предыстория


На данный момент с момента внедрения прошло уже больше года. Сейчас переписано около 90-95% всего программного кода системы и я четко представляю как развивается и как должна развиваться система. Но на тот момент когда перед мной поставили задачу, у меня было расплывчатое представление как должен выглядеть код судя по ТЗ, а опыта в общении с asterisk'ом не было в принципе. Сразу скажу что основная идея не моя, моя задача была реализовать или даже скорее изобразить, то что было расписано в условиях задачи. Но при этом что самое важное я был практически ничем не ограничен в выборе технологий и способов решения — что на мой взгляд позволило мне довести всю схему к виду, который я хотел.

На тот момент в компании уже был рабочий колл-центр. Около 10 очередей, от 4 до 20 операторов в каждой очереди и около 12-15 тысяч звонков круглосуточно. 5 провайдеров для местных, междугородних и международных вызовов. За долгое время колл-центр оброс большим количеством различного функционала и собственных разработок. Основной софт платформой являются сервера на астерисках, база со статистикой звонков и бизнес логикой на MySQL, а так же обвязка из скриптов на AGI.

Задача


Периодически возникает необходимость обзвонить клиентов по различному роду вопросов. Это может понадобится как периодически, например раз в месяц (задолженность, уведомления по акции или рекламе ), как и эпизодически (нерешенные проблемы, технические вопросы). На текущий момент из очереди выбираются несколько человек, которые обзванивают таких клиентов и формируют отчёт по своей работе, который затем передается друг другу. Если возникает большая очередь звонков — операторов возвращают на входящие звонки и они помогают устранить нагрузку, затем снова возвращаются обратно. В итоге возникает много проблем как человеческого фактора, человек может ошибиться в номере, забыть кого нибудь из клиентов, так и в административном управлении — требуется следить за очередью и перекидывать операторов из обзвона на входящие звонки и обратно. 
Поэтому требуется некий автомат, который будет сам обзванивать нужных клиентов, при этом учитывая нагрузку в очереди, так чтобы по простым вопросам, можно было обзванивать только при минимальной нагрузке, а по более сложным и важным — в приоритете даже над входящими звонками. Автомат должен напрямую соединять оператора и клиента, так чтобы клиент сразу же начинал разговор уже с живым человеком, без прохождения IVR'a или ожидания в очереди. Оператор которого выбрали для обзвона — должен оставаться в очереди и после завершения разговора без каких либо действий мог принять обычный входящий звонок или очередной обзвон. После соединения с оператором нужен таймаут для того, чтобы оператор успел вникнуть в суть звонка — понять для чего он звонит клиенту, а после завершения звонка в независимости от результата таймаут на то, чтобы откомментировать задачу. Планируемая нагрузка 2000 звонков в месяц а затем в перспективе до 10 000 — 15 000.

План реализации


Судя по описанию условий задачи формируем техническую модель: 
  • Требуется планировщик задач. Нужно формировать сами задания, выдавать их как в любое время так и в определённое (никому не понадобится информация по балансу в 3 часа ночи). Необходимо следить за нагрузкой очереди, чтобы задания на обзвон не забили все слоты и не мешали обычным входящим звонкам.
  • Нужен механизм для прямого соединения оператора и клиента. В первую очередь выбрав свободного оператора, поднимается плечо оператора и только потом плечо клиента. На время вызова должна сохранится вся логика обработки очереди. Оператор должен быть занят и вызов должен быть засчитан в статистике.
  • По возможности нужно минимальное участие оператора. Здесь в данном случае я подразумеваю максимальную автоматизацию со стороны работы оператора. Для него должен прийти входящий вызов, оператор отвечает на него и система сама делает вызов клиента, выбирая как направление, так и сама меняя статус задаче, в независимости от того, что дозвонились мы или нет до клиента. Подробнее объясню ниже.
  • Требуется балансировка и резервирование как исходящих направлений, так и самих серверов asterisk’a. Так же при выборе исходящего провайдера нужно учитывать что кроме принадлежности к зоне — ещё есть и различная стоимость звонка.
  • Обязательно нужно резервирование логики сопровождения звонка. Если оператор не ответит на вызов — зависнет компьютер или раздумает, звонок не должен просто отпасть и потеряться. Если при обычном входящем вызове, мы получив событие“AgentRingNoAnswer”, можем просто выбрать другого оператора, то в данном случае мы получаем узкое место в промежутке между ответом оператора и дозвоном до клиента.

Воплощение в жизнь


Планирование

Сейчас по прошествии времени уже трудно вспомнить хронологию исполнения, но первое что я сразу решил — все планирование должно быть в биллинге. Задачи будут формироваться на основе существующего клиента и его признаков: привязанные к нему номера телефонов, баланс, состояние привязанного к нему оборудования, бизнес статистика и прочее. Изначально было понятно какие широкие перспективы дает такая автоматизация для связи с клиентом. Мы можем запросить всех — кто подключив какую либо услугу — не использует её. При выходе новой прошивки, обзвонить клиентов, чтобы предложить обновится, если нет auto provisioning'a. Предупредить клиентов о проведении плановых работ, или назойливо продвигать дополнительные услуги компании. Но такая задача с технической стороны телефонии просто выглядит как номер телефона и причина, по которой мы звоним. 
Мы можем позвонить по банальной проблеме, но нам может понадобится и более срочная связь с клиентом — например во время разговора звонок прервался. Нам нужно как можно быстрее связаться с клиентом и решить его вопрос. Поэтому нам нужен такой параметр как приоритет. По нему можно позвонить в любое время и в первую очередь.
Каждой задаче — своё решение. Если у нас вопрос административного плана, мы решаем его через абонентский отдел. Технический вопрос решается через техническую поддержку. Соответственно при формировании задачи нужно выбирать какая из служб получит задание.

В итоге через внутреннюю логику биллинга была сделана вьюшка, из которой можно получить задачу в виде: id задачи, номер телефона, приоритет и очередь для которой сформированное задание. С моей стороны был написан демон, который постоянно мониторит загрузку очереди — берет текущее количество звонков, ожидающих ответа к максимально допустимому в процентах, а так же количество свободных операторов. Например, для задач с низким приоритетом требуется 0% загрузка очереди и хотя бы 1 свободный оператор. Если приоритет более высокий то загрузка не более 70%. Для более высшего приоритета загрузка не равная 100%. При благоприятных условиях демон получив задачу, меняет ей статус на «выполняется» и закидывает звонок в очередь. Номер задачи записывается в локальный кэш демона, по которому происходит мониторинг задачи в таблице-словаре в нашей базе вида: id задачи -> dialstatus. Если появился статус — то меняем статус задачи в биллинге и удаляем задачу из словаря и локального кэша. Соответственно при старте демона, из биллинга собираются все задачи со статусом“выполняются” для мониторинга их состояния.

Работа с оператором

Разрабатывая схему работы с оператором — я выделил основные характеристики:
  • Процесс выбора и соединения с оператором должен проходить максимально быстро. Любой занятый оператор должен быть проверен на незанятость и пригодность для звонка не более чем за 3-5 секунд.
  • Если произошел fail и оператор не ответил на звонок или завис компьютер. Необходимо сопроводить звонок обратно в очередь, а проблемного оператора вывести из очереди.
  • Необходимо отслеживать статус звонка чтобы по завершению вызова проставить статус задаче либо выполнена либо нет.

Когда я стал искать решение — то сразу понял что текущая статистика не правильно увидит и не зачтет звонок оператору как успешный, кроме того не понятно было как выдёргивать оператора для приёма таких особых звонков, поэтому были предприняты попытки сделать костыли типа сделать дозвон оператору и сбриджевать его с плечом клиента, а так же различные махинации со слотами очереди. Собранные схемы были рабочие, но крайне нестабильные и трудоёмкие при сборе статистики. В итоге я отказался от попыток подстроиться под текущую схему и решил написать независимую часть логики по обработки входящих звонков нового типа. Механизмом для работы с оператором был выбран Originate. Многие в противовес ставят call file безусловно технология прикольная, и для своего рода задач имеет право на существование, но для моей задачи не подошла бы: для соединения с оператором требуется мгновенный ответ может или нет оператор принять вызов, в случае с файлом нужно мониторить сам файл, что с моей точки зрения не удобно и не рационально. Я просто выполняю ami запрос, жду около 5 секунд, если оператор снял трубку значит звонок идёт в следующую стадию, если нет, то возвращаем звонок в очередь и ищем следующего оператора. Так же для файлов характерна локальная работа на астериске, для которого он сформирован. То есть нам нужен будет некий локальный скрипт на каждом сервере, который будет локально формировать файл, попутно выясняя кто сейчас из коллег серверов наименее нагружен. Полная децентрализация управления. В моем случае я просто запрашиваю нагрузку по всем серверам и выбрав наименее нагруженный отправляю ему ami запрос. Для работы была использована библиотека Asterisk::AMI.
После того как оператор ответил на вызов, в сформированном звонке через локальные переменные выбирается «exten» для звонка, где проставлен таймаут через «Wait» перед «Dial’ом». Основная проблема была в том, что если оператор раздумает в момент таймаута или вызова и положит трубку — то звонок пропадает. Поэтому при поступлении dialstatus’a “Cancel” звонок считается не обработанным и возвращается в очередь. Оператора деактивируем из очереди. Дальше по dialplan’y происходит Dial с дополнительным параметром “g”, чтобы продолжить выполнение после завершения разговора. По завершению таймаута после разговора через «hangup» экстен добавляется запись в словарик — идентификатор задания и dialstatus, insert запросом. Таким образом имея рабочую машину и sip клиента на ней, включив функцию «auto answer» — оператор только комментирует задачи в интерфейсе, вся обработка вызова происходит за него. Возможно кто то скажет что это плохая идея, но так сделано по нескольким причинам. Если мы хотим, чтобы клиентам гарантировано пытались дозвонится 30 секунд — то мы просто задаем таймаут для команды dial, а не ждем каких либо действий со стороны оператора. Опять таки мы получаем схему при которой сокращается монотонная работа для людей. Так получилось, что в самом начале я работал в этой же компании оператором технической поддержки и я не по наслышке знаю, как утомляют однообразные действия, по мимо самих клиентов.

Балансировка и резервирование

Если поискать в интернете информацию на тему как резервировать исходящие вызовы при недоступности одного из провайдеров — чаще всего вы найдете рекомендацию типа сделать в диаплане несколько Dial’ов друг за другом. Мне же со своей стороны хотелось сделать, что то более гибкое и динамичное. Основная проблема была в том что у нас используется много разных железок и перед формированием звонка нет времени обходить их в поисках наименее нагруженного и доступного транка. Основная идея была в том чтобы работать уже с агрегированной информацией. Для этого был написан отдельный демон, задачи которого сводились к следующему: собирать с разного типа оборудования нагрузку, с TDM разность занятых слотов к свободным, с voip провайдеров доступность через sip peers и на основе текущих каналов с астерисков выбирать кто больше из ни используется. Но помимо выбора направления надо учитывать что любой из транков может упасть, поэтому надо выбрать альтернативу. В моём случае это были voip провайдеры, но у них так же существуют свои расценки, поэтому балансировка должна выбирать с наиболее дешёвых к дорогим (по качеству они абсолютно равнозначны). Для такой выборки я сделал для каждого провайдера свой вес или свою стоимость. Таким образом получился список из ключей и значений такого вида:

  • Местная зона => 10
  • Voip провайдер А => 20
  • Voip провайдер B => 30
  • Voip провайдер C => 40

Так если один из провайдеров не доступен — мы ищем ему замену с низшего к более высокому, или дорогому.

Но по мимо этого существует ещё и распределение в зависимости от принадлежности номера. То есть на номера Москвы например следует звонить через своего местного провайдера, в Сибирь или другие страны через своего, если такой имеется, либо через междугороднее и международное направление. Здесь возникает потребность определить к какому региону относится номер. Для этого, я написал парсер реестра росвязи в базу. Выглядит как:

mysql> select local.get_region(903599****);
+-------------------------------------+
| voicecon_new.get_region(903599****) |
+-------------------------------------+
| Moskva i Moskovskaya oblast         |
+-------------------------------------+


Общая схема работы и различные нюансы


  • Для каждой задачи на текущий момент можно задать количество попыток, которое декрементируется каждым статусом «не выполнено». Через опять таки задаваемый таймаут задание снова вернется на выдачу.
  • Так же недавно появилась возможность приоритетно сделать дозвон через заданного провайдера, если он не перегружен и доступен.
  • Демон для агрегации нагрузки по voip провайдерам ориентируется по префиксу в названии sip каналов. Можно было бы использовать например группы, но пока всё прекрасно работает.
  • Он же периодически пингает как Voip провайдеры так и все железки на пути до транков, если какая из них не доступна — провайдер выводится из балансировки.
  • После выбора свободного оператора — он помечается как занятый и происходит следующая логика:

  1. Мы запрашиваем все asterisk сервера, находящиеся в балансировке.
  2. Для текущей очереди запрашиваются параметры обзвонов. Это время ожидания ответа от оператора и клиента, «source» номер который увидит клиент при входящем звонке — для каждых служб он разный.
  3. По номеру клиента выбираем направление через таблицу с реестром номеров.
  4. Выбираем аплинк для вызова. Если выставить 0 — аплинк не будет участвовать в выборе, соответственно можно балансировать сдвигая приоритет провайдерам. Выбранный транк проверятся на доступность а затем на загрузку. Если условия не соблюдены, аплинк пропускается и мы переходим к следующему по весу.
  5. Далее мы выбираем asterisk сервер с наименьшим количеством каналов. По скольку в Redis'e у нас хранятся все каналы со всех серверов, мы просто получаем общее количество через встроенную функцию hlen. Выбрав сервер, мы пытаемся к нему подключится — если сервер не ответил, мы берём следующий, опять с наименьшей нагрузкой.
  6. На последок для параметра «variable» формируется массив из служебных переменных, на основе которых происходит как учёт звонка, так и выбор направления. Так же там используется привязка текущего звонка оператора к задаче по которой происходит обзвон.


Заключение


Не смотря на заявленную нагрузку сейчас цифра достигла около 30785 звонков за последний месяц. Изначально схема планируемая только для узкого круга задач, разрослась до сервиса способного решить обширный круг задач и справится с большой нагрузкой. На текущий момент работа над ней не остановилась — я постоянно что то дописываю и расширяю функционал, а так же планирую провести очередной рефакторинг.

0 коммент.:

Отправить комментарий