Как стать автором
Обновить
63.47
Exolve
Конструктор омниканальных диалогов для бизнеса

Как создавать A/B-тесты SMS-рассылок с нейросетью DeepSeek

Уровень сложностиСредний
Время на прочтение13 мин
Количество просмотров530

Привет, Хабр. В этой статье поможем владельцам бизнесов и маркетологам в два клика с помощью нейросети получить хорошие тексты для A/B-тестирования SMS-рассылок и разослать выбранные варианты контактам из CRM.

Для генерации текстов используем API DeepSeek, для рассылок — SMS API от МТС Exolve, а контакты берём из CRM-системы «Битрикс24».

С этим микрорешением мы получим пять вариантов SMS — из контекста, который зададим. Для начала упростим первый шаг и выделим пять условий:

  1. Исполняемая нейросетью роль

  2. Целевая аудитория

  3. Наше предложение для целевой аудитории

  4. Ограничения

  5. Формулировка задачи

Одобрим один или несколько вариантов текста через чекбокс — после этого контакты из CRM-системы будут разбиты на группы, которые получат разные варианты рассылки.

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

Такой способ автоматизации освободит владельцев бизнеса и сотрудников от рутины и позволит повысить доход от продаж.

Использование API DeepSeek

Для взаимодействия с DeepSeek используем библиотеку deepseek с более простым и читаемым кодом по сравнению с обращением к API напрямую. При получении HTTP-запроса с нашей веб-формы запускается функция:

def dump_JSON(JSON):
   with open(SETTINGS_PATH, 'w') as f:
       json.dump(JSON, f)
@csrf_exempt
def save_and_run_deep_seek(request):
   # Get prompt textfields content
   conditions_JSON = json.loads(request.body.decode('utf-8'))
   dump_JSON(conditions_JSON)
   # Compose prompt and run API
   prompt = prompt_constructor(conditions_JSON)
   print(prompt)
   response = api_client.chat_completion(prompt=prompt).replace('```json', '').replace('```', '')
   print(response)
   answer_JSON = json.loads(response)
   sms_data.set_JSON(answer_JSON)
   return show_page(request, answer_JSON)

Функция save_and_run_deep_seek сохраняет последнюю редакцию условий, собирает запрос воедино и убирает из ответа ненужное. Лишние символы связаны с тем, что нейросеть возвращает ответ в формате Markdown, и JSON оборачивается в раздел, обозначающий, что внутри лежит код.

Использование ролей при построении запроса

Для успешной генерации текстов SMS с помощью нейросети важно чётко задать условия выполнения задачи. Так модель понимает контекст, целевую аудиторию, предложение, ограничения и саму задачу.

Мы протестировали несколько условий, которые помогают получать наиболее полезные сообщения:

  1. Исполняемая роль задаёт контекст и стиль текста, поэтому он более релевантный

  2. Целевая аудитория помогает персонализировать сообщение, чтобы оно лучше воздействовало на конкретную группу

  3. Предложение в сообщении привлекает и мотивирует

  4. Ограничения обеспечивают соответствие техническим и содержательным требованиям

  5. Формулировка задачи объединяет все условия и задаёт чёткую цель, что упрощает процесс генерации

Исходя из содержимого текстовых полей нашей веб-формы и с учётом постоянного заданного системой условия собираем наш промпт:

PROMPT_CONDITIONS_NAMES = ['Твоя роль: ', 'Целевая аудитория: ', 'Предложение для целевой аудитории: ', 'Ограничения: ',
                          'Твоя задача: ']
prompt_initial = [
   {"role": "system",
    "content": "Предложи 5 вариантов ответа. Запиши их в виде массива строк в поле proposition JSON-объекта. Верни просто JSON в виде строки."},
]
def prompt_constructor(conditions_JSON):
   prompt = prompt_initial.copy()
   for ai, cond in enumerate(conditions_JSON['prompts']):
       if len(cond) == 0: continue
       prompt.append(
           {"role": "user",
            "content": PROMPT_CONDITIONS_NAMES[ai] + cond})
   return prompt

В нашем эксперименте зададим такие условия:

Исполняемая роль

Представь, что ты опытный маркетолог, работаешь в компании «Радуга», производящей уходовую косметику. Тебе нужно написать короткий текст для SMS. Его текст должен быть убедительным и увлекательным, побуждающим к покупке или изучению ассортимента

Целевая аудитория

Мама с ребёнком, который пошёл в детский сад. Мама работает менеджером среднего звена, имеет средний и выше доход, разведена. С ребёнком помогают родители мамы. Времени на себя мало

Предложение для целевой аудитории

Скидка 30% на серию кремов для лица. Предложение действует до 15 апреля

Ограничения

В SMS должно быть как можно меньше символов: от 70 до 140 знаков с пробелами

Задача

Предложи SMS для рассылки среди целевой аудитории, чтобы стимулировать её купить наш товар

Построение веб-приложения на Python при помощи Django

Для пользователей сделаем веб-приложение с интерфейсом. Бэкенд будет отвечать не только за формирование контента, но и за работу с базой контактов и статистические тесты. С Django легко и быстро создавать веб-приложения, это отличный выбор для проектов — от простых сайтов до сложных платформ. 

Создаём представления

В views.py хранятся представления — функции для обработки запросов. Помимо save_and_run_deep_seek, в файле есть функции вывода страниц, отображения результатов DeepSeek и другие, вспомогательные.

sms_data = SmsTrial()
db = DB()
def show_page(request, proposition_JSON=None):
   # Read JSON, fill in textareas previously saved conditions
   return render(request, 'deep_seek_api_app/index.html', context=get_context(proposition_JSON))
@csrf_exempt
def process_selected_propositions(request):
   numbers_JSON = json.loads(request.body.decode('utf-8'))
   print(numbers_JSON)
   sms_data.select(numbers_JSON['buttons'])
   start_processing()
   return show_page(request)
def start_processing():
   db.fill_bases(sms_data.groups_number())
   for ID in db.interactions_table.index:
       current_text_index = db.interactions_table.loc[ID, 'group']
       current_text = sms_data.get_text_by_group_number(current_text_index)
       send_SMS(db.get_phone(ID), current_text)
def get_list(texts_iterable=None, number: int = 5):
   if texts_iterable is None:
       texts_iterable = ()
   ans = []
   for ai in range(number):
       if ai < len(texts_iterable):
           ans.append(texts_iterable[ai])
           continue
       ans.append('')
   return ans
def get_context(answer_JSON=None):
   if answer_JSON is None: answer_JSON = {}
   proposition = get_list(answer_JSON.get('proposition'))
   prompt_JSON = get_settings()
   prompts = get_list(prompt_JSON.get('prompts'))
   ans = {'prompts': prompts, 'proposition': proposition}
   print(ans)
   return ans
def get_settings():
   if not os.path.exists(SETTINGS_PATH):
       return {}
   with open(SETTINGS_PATH, 'r') as f:
       return json.load(f)
@dataclass
class SmsTrial:
   answer_JSON = {}
   selected_propositions = []
   def set_JSON(self, JSON):
       self.answer_JSON = JSON
   def select(self, buttons_number):
       self.selected_propositions = buttons_number
   def get_text_by_group_number(self, number):
       answer_id = self.selected_propositions[number]
       return self.answer_JSON['proposition'][answer_id]
   def groups_number(self):
       return len(self.selected_propositions)

Об используемой здесь базе данных — классе DB — речь пойдёт позже.

Настройка маршрутов

В urls.py связываются URL, по которым отправляются запросы из веб-формы с релевантными представлениями:

from django.contrib import admin
from django.urls import path
from . import views
urlpatterns = [
   path('admin/', admin.site.urls),
   path('main_page/', views.show_page),
   path('response/', views.save_and_run_deep_seek),
   path('try/', views.process_selected_propositions),
]

Создание шаблонов

Чтобы представление могло вернуть большую страницу, удобнее использовать шаблоны. Шаблон — страница, где могут быть переменные, значения которых передаются той же функцией render или вычисляются внутри самой страницы специальными инструкциями. HTML-шаблоны размещаются в папке templates/<имя_приложения>/.

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

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

<!DOCTYPE html>
<html lang="ru">
<head>
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>АВ тест SMS</title>
   <style>
       /* Полное описание стилей для наглядности этой статьи мы убрали, полная версия доступна в нашем гитхабе https://github.com/duckdevdotdev/postprod-article-mar2025-ab-test-sms-with-deepseek-ai */
   </style>
</head>
<body>
   <h1>Генерация и АВ тест мотивирующих SMS</h1>
   <h2>Условия для генерации текста SMS</h2>
   <!-- Первый блок -->
   <div class="block">
       <label for="textarea1">Роль, которую исполняет ИИ:</label>
       <textarea id="textarea1" placeholder="Задайте условие">{{prompts.0}}</textarea>
       <label for="textarea2">Целевая аудитория:</label>
       <textarea id="textarea2" placeholder="Задайте условие">{{prompts.1}}</textarea>
       <label for="textarea3">Предложение для целевой аудитории:</label>
       <textarea id="textarea3" placeholder="Задайте условие">{{prompts.2}}</textarea>
       <label for="textarea4">Ограничения:</label>
       <textarea id="textarea4" placeholder="Задайте условие">{{prompts.3}}</textarea>
       <label for="textarea5">Задача:</label>
       <textarea id="textarea5" placeholder="Задайте условие">{{prompts.4}}</textarea>
       <button id="send_req_button">Отправить запрос на генерацию</button>
   </div>
   <h2>Варианты рассылки, предложенные Deep Seek</h2>
   <!-- Второй блок -->
   <div class="block">
       <div class="block-2">
           <label for="textarea6">Вариант 1:</label>
           <textarea id="textarea6" placeholder="Тут будет вариант СМС, предложенный нейросетью">{{proposition.0}}</textarea>
           <input type="checkbox" id="checkbox1">
       </div>
       <div class="block-2">
           <label for="textarea7">Вариант 2:</label>
           <textarea id="textarea7" placeholder="Тут будет вариант СМС, предложенный нейросетью">{{proposition.1}}</textarea>
           <input type="checkbox" id="checkbox2">
       </div>
       <div class="block-2">
           <label for="textarea8">Вариант 2:</label>
           <textarea id="textarea8" placeholder="Тут будет вариант СМС, предложенный нейросетью">{{proposition.2}}</textarea>
           <input type="checkbox" id="checkbox3">
       </div>
       <div class="block-2">
           <label for="textarea9">Вариант 4:</label>
           <textarea id="textarea9" placeholder="Тут будет вариант СМС, предложенный нейросетью">{{proposition.3}}</textarea>
           <input type="checkbox" id="checkbox4">
       </div>
       <div class="block-2">
           <label for="textarea10">Вариант 5:</label>
           <textarea id="textarea10" placeholder="Тут будет вариант СМС, предложенный нейросетью">{{proposition.4}}</textarea>
           <input type="checkbox" id="checkbox5">
       </div>
       <button id="showModalButton">Тестировать выбранное</button>
   </div>
   <!-- Модальное окно -->
   <div id="modal" class="modal">
       <div class="modal-content">
           <p id="selectedCheckboxes"></p>
           <div class="modal-buttons">
               <button id="okButton">ОК</button>
               <button id="cancelButton">Отмена</button>
           </div>
       </div>
   </div>
   <script>
      const backend_site = 'http://127.0.0.1:8000/'
      document.getElementById('send_req_button').addEventListener('click', async function() {
      const prompts = [];
      // Перебираем textarea, в которых редактировались условия промпта.
      for (let i = 1; i <= 5; i++) {
         const textarea = document.getElementById(`textarea${i}`);
         if (textarea) {
            prompts.push(textarea.value);
         }
      }
      // Формируем JSON-объект
      const json = {
         prompts: prompts
      };
      try {
         // Отправляем JSON с поправленными пользователем условиями
         const response = await fetch(backend_site+'response/', {
            method: 'POST',
            headers: {
               'Content-Type': 'application/json'
            },
            body: JSON.stringify(json)
         });
         if (response.ok) {
            console.log('JSON успешно отправлен!');
            // alert('JSON успешно отправлен!');
            document.body.innerHTML = await response.text();
         } else {
            console.error('Ошибка при отправке JSON:', response.statusText);
            alert('Ошибка при отправке JSON.');
         }
      } catch (error) {
         console.error('Ошибка при отправке запроса:', error);
         alert('Ошибка при отправке запроса.');
      }
       });
  
       // Функция для отображения модального окна
       const showModalButton = document.getElementById('showModalButton');
       const modal = document.getElementById('modal');
       const selectedCheckboxesText = document.getElementById('selectedCheckboxes');
       const okButton = document.getElementById('okButton');
       const cancelButton = document.getElementById('cancelButton');
       showModalButton.addEventListener('click', () => {
           const checkboxes = document.querySelectorAll('.block-2 input[type="checkbox"]');
           const selected = [];
           checkboxes.forEach((checkbox, index) => {
               if (checkbox.checked) {
                   selected.push(index + 1);
               }
           });
           if (selected.length > 0) {
               selectedCheckboxesText.textContent = `Выбраны варианты для тестирования: ${selected.join(', ')}. Нажмите ОК для начала рассылки.`;
           } else {
               selectedCheckboxesText.textContent = 'Ни один чекбокс не выбран.';
           }
           modal.style.display = 'flex';
       });
       // Закрытие модального окна
       cancelButton.addEventListener('click', () => {
           modal.style.display = 'none';
       });
       // Отправка данных по нажатию на ОК
       okButton.addEventListener('click', () => {
           const checkboxes = document.querySelectorAll('.block-2 input[type="checkbox"]');
           const selected = [];
           checkboxes.forEach((checkbox, index) => {
               if (checkbox.checked) {
                   selected.push(index + 1);
               }
           });
           // Отправка JSON
           const data = { buttons: selected };
           fetch(backend_site+'try/', {
               method: 'POST',
               headers: {
                   'Content-Type': 'application/json',
               },
               body: JSON.stringify(data),
           })
           .then(response => response.json())
           .then(data => {
               console.log('Успешно отправлено:', data);
           })
           .catch((error) => {
               console.error('Ошибка:', error);
           });
           modal.style.display = 'none';
       });
   </script>
</body>
</html>

На странице видим условия, введённые один раз и сохраняемые на диске в JSON.

Для продолжения работы нажимаем на «Отправить запрос на генерацию». Через функцию process_selected_positions формируется запрос к API DeepSeek. JSON-ответы записываются в переменную proposition. Django при рендере страницы подставляет в нужные места её HTML-кода значения элементов массива proposition. В результате мы видим предложенные нейросетью варианты, из которых можем выбирать с помощью чекбоксов:

Нажимаем на кнопку «Тестировать выбранное» и запускаем процедуру рассылки сообщений. Это довольно ответственный этап, и страница переспросит о выбранных вариантах — мы увидим всплывающее окно.

Только после второго одобрения JSON с выбранными вариантами отправится на бэк. И тогда уже будут подтягиваться контакты с «Битрикс», которые разобьются на группы — им придут разные сообщения (контрольная группа не получит SMS).

Использование CRM «Битрикс24» как базы контактов

Для работы с «Битрикс24» нужны функции для получения базы контактов и сделок, поиска успешно закрытых сделок с каждым контактом. Как это сделать с примером кода, мы уже писали в нашей статье про экочеллендж. Там приведены функции: get_contacts, get_deals, is_contact_in_deal, search_deals_by_contact, get_deals_sum. Возьмите код этих функций и вставьте его в файл с классом базы данных, о котором пойдёт речь ниже, либо возьмите полную версию кода из гитхаба.

Используемая база данных

Для хранения базы контактов и информации о принадлежности к группе и сумме заключённых сделок используем свой дата-класс. Он содержит два дата-фрейма: соответствие номеров телефонов уникальным клиентам и соответствие уникальным клиентам номера группы и суммы сделок.

@dataclass
class DB:
   interactions_table = pd.DataFrame(columns=['ID', 'group', 'sales']).set_index(['ID'])
   phone_table = pd.DataFrame(columns=['phone', 'client_id']).set_index(['phone'])
   def add_client_phones(self, client_full_record):
       contact_phones = [ph.get('VALUE', '').strip('+') for ph in client_full_record.get('PHONE', [{}])]
       if len(contact_phones) == 0:
           return 0
       for c in contact_phones:
           self.phone_table.loc[c] = [client_full_record.get('ID')]
       return 1
   def add_interaction_item(self, client_full_record):
       if not self.add_client_phones(client_full_record):
           return
       self.interactions_table.loc[client_full_record.get('ID')] = 0
   def get_phone(self, client_ID):
       phone_indexes = self.phone_table['client_id'] == client_ID
       return self.phone_table.loc[phone_indexes].index[0].strip('+')
   def set_sales_by_tel(self, tel, num=1):
       client_id = db.phone_table.loc[tel].iloc[0]
       db.task_table.loc[client_id, 'sales'] += num
   def fill_bases(self, groups_number=2):
       contacts = get_contacts()
       for c in contacts:
           self.add_client_phones(c)
           self.add_interaction_item(c)
       split_into_groups(self.interactions_table, groups_number)
   def select_by_group(self, number):
       indexes = self.interactions_table['group'] == number
       return self.interactions_table.loc[indexes]
   def calculate_deals(self):
       dls = get_deals()
       for ID in self.interactions_table.index:
           current_deals = search_deals_by_contact(dls, ID)
           total_ordered = get_deals_sum(current_deals)
           self.interactions_table.loc[ID, 'sales'] += total_ordered
def split_into_groups(df: pd.DataFrame, groups_num=2):
   shuffled = df.sample(frac=1).index.values
   splitted_index = np.array_split(shuffled, groups_num)
   for ai in range(groups_num):
       df.loc[splitted_index[ai], 'group'] = ai

Рассылка SMS по API

Отправка сообщений через API позволяет интегрировать функциональность рассылок в ваше приложение или CRM-систему. Для этого необходимо зарегистрироваться на платформе МТС Exolve, получить API-ключ и использовать HTTP-запросы (например, через библиотеку requests в Python) для отправки сообщений.

Мы используем для этого такую функцию:

def send_SMS(recepient: str, send_str: str):
   payload = {'number': crm_phone, 'destination': recepient, 'text': send_str}
   r = requests.post(r'https://api.exolve.ru/messaging/v1/SendSMS', headers={'Authorization': 'Bearer ' + sms_api_key},
                     data=json.dumps(payload))
   print(r.text)
   return r.text, r.status_code

Анализ результатов

После проведения рассылки контакты разбиваем на группы, подсчитываем число успешных сделок в каждой из них. Для выявления статистически значимых различий используем Z-тест пропорций. Для каждой группы, включая контрольную, подсчитываем число клиентов, у которых сумма закрытых сделок отлична от нуля.

При помощи теста пропорций проверяем, для какой группы меньше вероятность того, что фиксируемые отличия в количестве откликов между ней и контрольной обусловлены случайностью. Тест учитывает число откликов и элементов в каждой группе.

Для анализа используем функцию proportions_ztest из библиотеки statsmodels. Реализация на Python:

z_stat, pval = proportions_ztest(dealed, nobs=nobs)

Если p-value меньше 0,05, различия считаются статистически значимыми.

def get_numbers(group_num):
   selected_contacts = db.select_by_group(group_num)
   nobs = selected_contacts.shape[0] + 1
   dealed = np.count_nonzero(selected_contacts['sales'].values)
   return dealed, nobs
def finish_observation():
   pval = []
   dealed0, nobs0 = get_numbers(0)
   for ai in range(1, sms_data.groups_number()):
       dealed = [dealed0]
       nobs = [nobs0]
       dealed1, nobs1 = get_numbers(ai)
       dealed.append(dealed1)
       nobs.append(nobs1)
       z_stat, pval1 = proportions_ztest(dealed, nobs=nobs)
       pval.append(pval1)
   index = np.argmin(pval)
   pval_min = min(pval)
   return index, pval_min

Например, при одном и том же количестве откликов — 15 в первой группе и 25 во второй, при 40 клиентах — в каждой группе различия являются значимыми (p = 0,025 < 0,05), а при 65 клиентах в группе такие же различия не считаются значимыми (p = 0,057 > 0,05).

Пока рано делать выводы о том, справится ли DeepSeek с полной автоматизацией, но у него есть все шансы. Сейчас в среднем на 13% эффективнее оказались те SMS, где мы в первом предложении подсвечиваем сразу скидку и цифры. И в любом случае с каждой новой рассылкой можно улучшать тексты и отсеивать явно лишние варианты.

Такой способ автоматизации позволит владельцам бизнеса и маркетологам сосредоточиться на стратегических задачах. Работа с платформой МТС Exolve и нейросетью DeepSeek не только упрощает процесс, но и повышает его эффективность, что ведёт к увеличению продаж. Использование статистических тестов и мощного матаппарата помогает сделать выводы наиболее обоснованными и объективными.

Теги:
Хабы:
-1
Комментарии0

Публикации

Информация

Сайт
exolve.ru
Дата основания
Численность
501–1 000 человек