Скажите, если к вам придёт потенциальный клиент, но вместо красивого сайта, приложения или сотрудника его встретит чатик с текстовой нейросетью, которая что-то знает о вашем продукте и теоретически может его продать – вам будет комфортно? Это, может, нетипично для энтузиаста, закопавшегося по уши во всякие GPT и PaLM, но лично мне в такой ситуации будет очень страшно. А вдруг нейросеть продаст что-то несуществующее? Или вообще ничего не будет продавать? Или нагрубит клиенту?

Похоже что эти опасения разделяют многие: каждую неделю появляется ворох новых сервисов, пишущих нейросетью что-то для последующей обработки человеком (начиная с кода и заканчивая рекламными текстами), а вот примеров, в которых нейросеть "пускают" напрямую к клиентам далеко не так много. Но, как мне кажется, я нашёл способ от этих опасений в существенной степени избавиться. (Конечно, может быть, кто-то уже нашёл его раньше и я просто этого не заметил, но что уж поделаешь, сфера новая и очень быстро развивается.)

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

Как видим, нейросеть может навредить человечеству не только изобретением новых ядов, но и пассивной агрессией в адрес пользователей.
Как видим, нейросеть может навредить человечеству не только изобретением новых ядов,
но и пассивной агрессией в адрес пользователей.

Сервис для маршрутизации заявок в техподдержку

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

Изменение в схеме прохождения тикетов
Изменение в схеме прохождения тикетов

По сути, для получения такого результата нам нужно:

  • Определить, есть ли в исходном сообщении пользователя информация о том, в каком именно сценарии использования возникла проблема. Если информация есть – сразу направить тикет к соответствующему специалисту.

  • Если информации нет – спросить у пользователя, при каком действии возникла ошибка.

  • Снова определить наличие информации с учётом ответа пользователя, если есть – направить тикет.

  • Если информации всё ещё нет – направить тикет к "живому" сотруднику первой линии.

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

Ниже приведена переписка пользователя со службой поддержки. \
Можно ли из этой переписки понять, про какой из процессов в приложении говорит пользователь? \
Начини ответ с ключевого слова:
ПОКУПКА если речь идёт о покупке
ПЕРЕВОД если речь идёт о переводе денег другому человеку
ПОЛУЧЕНИЕ если речь идёт о получении денег от другого человека
Если из переписки невозможно определить, о каком процессе идёт речь, \
начни с ключевого слова НЕПОНЯТНО
Про форматирование промптов

В данном случае тексты, которые используются в сервисе, по своей природе ближе к коду, чем к части текста статьи. Поэтому я выделяю их как блоки кода.

Однако, в блоках кода нет переноса строки, а в текстах для нейросетей (в отличие от кода, как правило) могут быть очень длинные строки. Поэтому я разбиваю строки при форматировании, а обратным слэшем отмечаю места, где я разбил строку для удобства, но фактически в промпте переноса строки нет.

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

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

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

Пожалуйста, уточните, при каком действии возникла ошибка?

И затем, после получения ответа от пользователя, направляем в нейросеть все три реплики, снова с той же инструкцией. Если получили ключевые слова по процессам – направляем к соответствующему специалисту, если второй раз получили НЕПОНЯТНО – направляем к "живому" сотруднику первой линии. Ну и, на любом этапе процесса, если нейросеть не справилась и не указала ключевое слово – тоже направляем тикет на "живого" сотрудника.

Общий принцип построения подобных сервисов

Если все рассуждения выше несколько обобщить, то получится примерно такая схема сервиса:

Взаимодействие компонентов сервиса
Взаимодействие компонентов сервиса

Самое интересное и необычное здесь – это сценарий диалога. Он представляет из себя множество возможных состояний диалога с пользователем, правила перехода между ними и точки выхода. В нашем примитивном случае сценарий выглядит так:

Сценарий нашего маршрутизатора заявок
Сценарий нашего маршрутизатора заявок

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

  • Обработка одного и того же текста нейросетью несколько раз с разными целями. Скажем, мы узнали что пользователь пытался оплатить покупку, и теперь хотим уточнить, каким способом он это делал. Он мог сказать об этом и сразу, поэтому прежде чем задавать уточняющий вопрос – снова загоним всю предыдущую переписку в нейросеть, но теперь с инструкцией для извлечения способа оплаты.

  • Использование дополнительных источников информации. Например, мы можем отправить нейросети логи по пользователю и попросить сделать из неё какие-то выводы с учётом информации от самого пользователя.

  • Выход из диалога без последующих действий – собственно, решением проблемы пользователя. Банальный пример – если мы выяснили, что пользователь пытается оплатить покупки, мы далее можем узнать какой картой он пользовался и каким способом оплаты, и если это например карта Visa и Google pay, то мы можем прямо в рамках сценария объяснить ему ситуацию и предложить альтернативные решения.

  • Более сложные реплики со стороны сервиса. Я говорю как о частичном написании их самой нейросетью в некоторых особенно сложных местах, так и о банальном использовании шаблонов с подстановкой данных, в самом простом случае – имени пользователя.

А насколько оно действительно надёжно?

Естественно, вся эта красота не будет иметь никакого смысла, если на каждой реплике половина пользователей будет улетать к "живому" сотруднику, потому что нейросеть решила ответить нам в свободной форме вместо фиксированных ключевых слов. Опыт и интуиция говорят мне, что можно добиться довольно низкого процента, но это довольно слабый аргумент. Поэтому я вручную написал 80 возможных сообщений от пользователя, и сгенерировал ещё 160 с помощью GPT-3.5, и написал скрипт на python для проверки надёжности всей этой системы.

Скрипт для обработки датасета
import requests
import pandas as pd


def get_answer(messages, keywords):
    url = 'https://api.openai.com/v1/chat/completions'
    headers = {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer <API key>'
    }
    obj = {
        'frequency_penalty': 0,
        'max_tokens': 50,
        'messages': messages,
        'model': 'gpt-3.5-turbo', # стандартная 3.5, на мой взгляд 4 в такой задаче будет избыточной и слишком дорогой
        'presence_penalty': 0,
        'stream': False,
        'temperature': 0, # используется 0, т.к. задача по сути детерминистичная
        'top_p': 1
    }

    try:
        response = requests.post(url, headers=headers, json=obj, timeout=20)
    except requests.exceptions.Timeout:
        return 'timeout', ''

    print(response.status_code)
    if response.status_code != 200:
        return 'request_error', ''

    data = response.json()
    raw_answer = data['choices'][0]['message']['content']
    print(raw_answer)
    for k, v in keywords.items():
        if raw_answer.startswith(v):
            return k, raw_answer
    return 'format_error', raw_answer


base_first_message = "Ниже приведена переписка пользователя со службой поддержки. " + \
                "Можно ли из этой переписки понять, про какой из процессов в приложении говорит пользователь? " + \
                "Начини ответ с ключевого слова:\n" + \
                "%purchase% если речь идёт о покупке\n" + \
                "%send% если речь идёт о переводе денег другому человеку\n" + \
                "%receive% если речь идёт о получении денег от другого человека\n" + \
                "Если из переписки невозможно определить, о каком процессе идёт речь, " + \
                "начни с ключевого слова %unknown%"
second_message = "Пожалуйста, уточните, при каком действии возникла ошибка?"


def get_first_message(keywords):
    result = base_first_message
    for k, v in keywords.items():
        result = result.replace('%' + k + '%', v)
    return result


keywords = { # этот dict будем менять для замены ключевых слов
    'purchase': 'ПОКУПКА',
    'send': 'ПЕРЕВОД',
    'receive': 'ПОЛУЧЕНИЕ',
    'unknown': 'НЕПОНЯТНО'
}

first_message = get_first_message(keywords)
process_keywords = ['purchase', 'send', 'receive']


def get_messages_from_row(row): # эту фунцкию будем менять для замены логики сбора цепочки сообщений
    messages = [
        {
            'role': 'user',
            'content': first_message
        },
        {
            'role': 'user',
            'content': row['message1']
        }
    ]
    if row['type'] == 2:
        messages.append({
            'role': 'assistant',
            'content': second_message
        })
        messages.append({
            'role': 'user',
            'content': row['message2']
        })
    return messages


def process_row(row):
    print(row['message1'])
    print(row['message2'])
    messages = get_messages_from_row(row)
    #print(messages)
    answer, raw_answer = get_answer(messages, keywords)
    print(answer)

    correct = answer == row['correct_answer']
    error_type = None

    if answer in ['format_error', 'timeout', 'request_error']:
        error_type = answer

    if answer != row['correct_answer'] and \
            answer in process_keywords and \
            row['correct_answer'] in process_keywords:
        error_type = 'wrong_process'

    if answer == 'unknown' and \
            row['correct_answer'] in process_keywords:
        error_type = 'wrong_unknown'

    if answer != row['correct_answer'] and \
            answer in process_keywords and \
            row['correct_answer'] == 'unknown':
        error_type = 'process_when_unknown'

    print(error_type)
    print()
    return answer, correct, error_type, raw_answer


df = pd.read_csv('dataset.csv')
df['answer'] = ''
df['correct'] = ''
df['error_type'] = ''
df['raw_answer'] = ''

while True:
    # Вообще, примерно в 50% случаев получается прогнать все 240 примеров с первого раза.
    # Но очень обидно, когда три раза подряд вылетает пара таймаутов, поэтому добавил повторные проходы "до победного"
    df_filtered = df[df['raw_answer'] == '']
    print('rows to process: ' + str(len(df_filtered.index)))
    if len(df_filtered.index) == 0:
        break
    for index, row in df_filtered.iterrows():
        answer, correct, error_type, raw_answer = process_row(row)
        df.loc[index, 'answer'] = answer
        df.loc[index, 'correct'] = correct
        df.loc[index, 'error_type'] = error_type
        df.loc[index, 'raw_answer'] = raw_answer

df.to_csv('output.csv')

print(df)

Скрипт для работы с результатами
import pandas as pd


df = pd.read_csv('output.csv')
n = len(df)

print("Overall number of rows is", n)
print()

with pd.option_context('display.max_rows', None,
                       'display.max_columns', None
                       ):
    print(df.groupby(['error_type'], dropna=False).size())
    print()

    print(df.groupby(['type', 'error_type'], dropna=False).size())
    print()

    print(df.groupby(['source', 'error_type'], dropna=False).size())
    print()

    print(df.groupby(['source', 'type', 'error_type'], dropna=False).size())
    print()

    print(df.groupby([df['correct_answer'] != 'unknown', 'error_type'], dropna=False).size())
    print()

    print(df.groupby([df['correct_answer'] != 'unknown', 'source', 'type', 'error_type'], dropna=False).size())
    print()

О датасете

Сам датасет

В датасете присутствует два типа строк – с одним сообщением (имитируем ситуацию, когда пользователь только пришёл с первым сообщением) и с двумя (имитируем ситуацию, когда пользователь пришёл с первым сообщением, мы его переспросили, он ответил). Различить их можно по столбцу type. Сами сообщения лежат в столбцах message1 и message2. В поле correct_answer лежит ответ (идентификатор процесса или unknown), предполагавшийся при написании примера. Поле source показывает, был ли этот пример придуман мной (manual) или сгенерирован нейросетью (generated).

Для каждого сочетания параметров (type и correct_answer) я сгенерировал своим мозгом 10 вариантов, и нейросетью ещё 20. Итого 240 строк.

Для генерации нейросетью ситуаций, в которых в первом же сообщении есть информация о проблемном процессе, использовал такой запрос:

Что может написать в техподдержку пользователь банковского приложения, \
у которого не срабатывает (оплата/перевод денег/получение денег от другого пользователя)?
Учти возможные вариации в эмоциональной окраске, орфографии и стиле.
Предложи 10 вариантов.

Для получения сообщений, в которых нет информации о конкретной проблеме, писал так:

Что может написать в техподдержку пользователь банковского приложения, \
у которого не работает какой-либо функционал? Рассматривай только те случаи, \
в которых пользователь не указывает в первом сообщении суть проблемы.
Учти возможные вариации в эмоциональной окраске, орфографии и стиле.
Предложи 10 вариантов.

И затем, после ответа нейросети, дозапрашивал вторые сообщения для получения «длинных» примеров:

Предположим, далее на все эти вопросы сотрудник техподдержки ответил \
пользователям "Пожалуйста, уточните, при каком действии возникла ошибка?". \
Что пользователи могли ответить ему? Предположим, у них не работает \
(оплата/перевод денег/получение денег от другого пользователя).

Либо для получения второго сообщения без детальной информации:

Предположим, далее на все эти вопросы сотрудник техподдержки ответил \
пользователям "Пожалуйста, уточните, при каком действии возникла ошибка?". \
Что пользователи могли ответить ему? Рассматривай только варианты, \
когда пользователь и во втором сообщении не указывает, \
что именно он пытался сделать и что именно не работает.

Во всех запросах время от времени делал небольшие изменения – например, добавлял упоминание о том, что сообщение не должно быть слишком длинным. Примерно 10% вариантов пришлось скорректировать вручную, т.к. нейросеть описывала не тот процесс (например, перевод вместо оплаты) или вообще не давала информации о процессе. Также местами нейросеть выдавала весьма забавные по стилю и по формулировке варианты, это не корректировал.

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

  • Ошибка формата (format_error) – когда в начале ответа нейросети вообще нет ключевого слова из указанных. То есть нейросеть вообще отказалась следовать инструкции и написала что-то своё. Важно понимать, что в случае русского языка попытки нейросети склонять ключевое слово тоже могут попасть сюда. И ещё замечу, что это единственный тип ошибки, который можно определить формальным образом. Для всех остальных всегда будут граничные случаи, в которых не до конца ясно, кто прав – человек (при простановке предполагаемого правильного ответа) или нейросеть.

  • Неверный процесс (wrong_process) – когда по сообщению пользователя было понятно, о чём он говорит, но нейросеть утверждает что он говорит о чём-то другом.

  • Избыточная осторожность (wrong_unknown) – когда по сообщению пользователя было понятно, о чём он говорит, но нейросеть этого не видит. При этом нейросеть с одной стороны не справилась с исходной задачей, с другой стороны корректно «отрефлексировала» свою неспособность с ней справиться и сообщила о ней в соответствии с заданным форматом.

  • Отсутствующий процесс (process_when_unknown) – когда по сообщению пользователя НЕ понятно, о чём он говорит, но нейросеть утверждает что понятно. В какой-то степени это можно назвать галлюцинацией.

Итак:

Что вывел скрипт
Overall number of rows is 240

error_type
format_error             42
process_when_unknown      2
wrong_process            10
wrong_unknown             7
NaN                     179
dtype: int64

type  error_type          
1     format_error              7
      process_when_unknown      1
      wrong_process             4
      wrong_unknown             7
      NaN                     101
2     format_error             35
      process_when_unknown      1
      wrong_process             6
      NaN                      78
dtype: int64

source     error_type          
generated  format_error             23
           process_when_unknown      1
           wrong_process             4
           wrong_unknown             3
           NaN                     129
manual     format_error             19
           process_when_unknown      1
           wrong_process             6
           wrong_unknown             4
           NaN                      50
dtype: int64

source     type  error_type          
generated  1     format_error             1
                 process_when_unknown     1
                 wrong_process            3
                 wrong_unknown            3
                 NaN                     72
           2     format_error            22
                 wrong_process            1
                 NaN                     57
manual     1     format_error             6
                 wrong_process            1
                 wrong_unknown            4
                 NaN                     29
           2     format_error            13
                 process_when_unknown     1
                 wrong_process            5
                 NaN                     21
dtype: int64

correct_answer  error_type          
False           format_error             29
                process_when_unknown      2
                NaN                      29
True            format_error             13
                wrong_process            10
                wrong_unknown             7
                NaN                     150
dtype: int64

correct_answer  source     type  error_type          
False           generated  1     format_error             1
                                 process_when_unknown     1
                                 NaN                     18
                           2     format_error            15
                                 NaN                      5
                manual     1     format_error             5
                                 NaN                      5
                           2     format_error             8
                                 process_when_unknown     1
                                 NaN                      1
True            generated  1     wrong_process            3
                                 wrong_unknown            3
                                 NaN                     54
                           2     format_error             7
                                 wrong_process            1
                                 NaN                     52
                manual     1     format_error             1
                                 wrong_process            1
                                 wrong_unknown            4
                                 NaN                     24
                           2     format_error             5
                                 wrong_process            5
                                 NaN                     20
dtype: int64


Process finished with exit code 0

Один полный прогон потребовал примерно $0.10 расходов на API.

Пока не самый воодушевляющий результат.

Всего ошибок – 25%, из них:
Ошибка формата – 17.5%
Неверный процесс – 4%
Избыточная осторожность – 3%
Отсутствующий процесс – 0.8%

При этом сильной разницы между «ручными» и сгенерированными примерами не заметно, это хорошо.

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

Как улучшить результат

Сначала будем смотреть только на ошибки формата, так как их вдвое больше всех остальных вместе взятых. Если посмотреть в данных, что именно к ним приводит, можно увидеть три ключевые проблемы:

  1. Нейросеть игнорирует инструкцию и пытается продолжить диалог с позиции «сферического сотрудника техподдержки в вакууме». Например:

Здравствуйте! Ничего не работает, что происходит?
Пожалуйста, уточните, при каком действии возникла ошибка?
У меня проблемы с получением перевода от другого пользователя, что нужно сделать, \
чтобы решить это?
Получение перевода от другого пользователя может зависеть от нескольких факторов. \
Пожалуйста, уточните, какая ошибка возникает при…
  1. Нейросеть воспринимает инструкцию, но вместо того, чтобы начинать ответ с ключевого слова, использует его как часть предложения:

Можете помочь разобраться с проблемой?
Пожалуйста, уточните, при каком действии возникла ошибка?
Отправка денег не срабатывает
Понятно, речь идёт о процессе ПЕРЕВОДА денег другому человеку. Я постараюсь помочь вам
  1. Нейросеть использует ключевое слово корректно, но меняет его формат:

Здравствуйте! Можете помочь?
Пожалуйста, уточните, при каком действии возникла ошибка?
Мне перевели деньги, но я их не вижу в приложении
Получение

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

Пользователь: Ниже приведена переписка пользователя со службой поддержки…
Пользователь: Здравствуйте! Можете помочь?
Бот: Пожалуйста, уточните, при каком действии возникла ошибка?
Пользователь: Мне перевели деньги, но я их не вижу в приложении

Она увидит такую:

Пользователь: Ниже приведена переписка пользователя со службой поддержки…

Здравствуйте! Можете помочь?
Пожалуйста, уточните, при каком действии возникла ошибка?
Мне перевели деньги, но я их не вижу в приложении

То есть, вообще говоря, она увидит не переписку, а одно сообщение пользователя с просьбой обработать определённый текст.

Попробуем сделать это. В приведённом выше коде изменится только функция get_messages_from_row.

Новый вид get_messages_from_row
def get_messages_from_row(row):
        {
            'role': 'user',
            'content': first_message + "\n\n" + row['message1']
        }
    ]
    if row['type'] == 2:
        messages[0]['content'] = messages[0]['content'] +\
                                 "\n" + second_message + "\n" + row['message2']
    return messages

Результат на тех же данных следующий:

Всего ошибок – 6.7%, из них:
Ошибка формата – 0.8%
Неверный процесс – 0.4%
Избыточная осторожность – 5.4%
Отсутствующий процесс – 0%

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

При этом «избыточная осторожность» в основном встречается примерно в таких ситуациях:

Привет! Я заметил, что мои платежи не проходят через ваше приложение. Что не так?
НЕПОНЯТНО

В общем-то, ситуация неоднозначная, и описание можно интерпретировать и как проблему с оплатой, и как проблему с переводом денег. Я бы не назвал критичной проблемой то, что такие вопросы перенаправляются на «живого» сотрудника. Кроме того, всегда можно добавить в сценарий более детальные уточняющие вопросы – например, если нейросеть выдаёт нам НЕПОНЯТНО, но в вопросе присутствует подстрока «платёж», можно спрашивать «Уточните, вы говорите об оплате покупки или о переводе средств другому человеку?».

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

Добрый день
Добрый день! Как я могу вам помочь?

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

А можно ли сделать ещё лучше?

Как мне кажется, результат достигнут достойный, но меня в нём очень смущает ещё одна вещь. Формат ключевых слов (слово на русском капслоком) выбран совершенно наобум, и категорически непонятно, является ли он оптимальным. В моём представлении, выбор ключевых слов представляет из себя балансирование трёх факторов:

  • Ключевое слово должно быть каким-либо образом выделено, чтобы нейросеть не пыталась встроить его в остальной текст как обычное слово.

  • Ключевое слово должно быть достаточно далёким от обычного слова, обозначающего то же самое, чтобы нейросеть не начала генерировать новые ключевые слова исходя просто из здравого смысла.

  • Ключевое слово должно быть достаточно близким к обычному слову, обозначающему то же самое, чтобы нейросеть не начала путаться в значениях.

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

  • Просто слова капслоком (исходный вариант)

  • Слова капслоком с выделением восклицательным знаком

  • Слова капслоком с выделением знаками равенства

  • Просто слова

  • Слова на английском, с той же вариативностью что на русском

  • Последовательные числа – 1, 2, 3, 4

  • Случайные числа, далёкие друг от друга – 609, 124, 984, 440

Кроме этого, я взял лучшие и худшие варианты, и посмотрел, как на них повлияет возвращение к схеме с отдельными сообщениями.

С точки зрения кода, при замене формата ключевых слов меняется только словарь keywords, например:

keywords = {
    'purchase': '!ПОКУПКА',
    'send': '!ОТПРАВКА',
    'receive': '!ПОЛУЧЕНИЕ',
    'unknown': '!НЕПОНЯТНО'
}

Итак, получилось следующее:

Видно, что выбранный изначально формат был неплох, но не был лучшим.
Видно, что выбранный изначально формат был неплох, но не был лучшим.

На мой взгляд, можно сделать такие выводы:

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

  • С объединением сообщений процент ошибок формата резко снижается, процент излишней осторожности остаётся на примерно тех же уровнях, оба всё так же варьируются в зависимости от формата ключевых слов. Здесь уже удалось загнать оба показателя почти в ноль форматами !ПОКУПКА и !PURCHASE.

  • Похоже, что зависимость процента излишней осторожности от формата ключевых слов примерно одинакова в вариантах с объединением сообщений и без него. Т.е. как с объединением ошибок формат 609 сильно хуже формата ПОКУПКА (51 ошибка против 7), так и без объединения он сильно хуже (52 ошибки против 13). Насчёт зависимости процента ошибок формата от формата ключевых слов то же самое или противоположное сказать сложно – слишком маленькие абсолютные числа при использовании объединения сообщений.

  • Про ошибки с неверным определением процесса и с «придумыванием» процесса сказать ничего нельзя, слишком маленький датасет.

Итого, лучшим форматом ключевых слов можно считать !PURCHASE с 96.3% корректно обработанных ситуаций. Второй по качеству - !ПОКУПКА, причём отрыв может выглядеть как заметным, так и незначительным, в зависимости от того, экстраполировать ли результаты по ошибкам формата без объединения сообщений на вариант с объединением сообщений.

Немного о предпосылках именно таких форматов

Естественно, к моменту написания этой статьи я уже пробовал много разных форматов и прикидывал их эффективность, хотя и не измерял её так системно. Изначально я работал только с английским и пришёл к выводу, что на нём оптимален формат =PURCHASE=. Затем при переходе на русский я попытался это экстраполировать, увидел, что =ПОКУПКА= на русском работает гораздо лучше чем =PURCHASE=, и решил что нейросети в принципе мешает смесь языков. После этого пришёл к формату !ПОКУПКА, который работал ещё лучше, и на этом остановился на какое-то время в поиске новых форматов. Только после этого я придумал приём с объединением сообщений, и увидел что он ещё сильнее повышает качество работы.

Поэтому то, что объединение сообщений сильно снижает процент ошибок, как и то, что формат !ПОКУПКА работает хорошо, для меня было предсказуемо. А вот то, что !PURCHASE при русскоязычном основном тексте работает ещё лучше – стало полной неожиданностью.

Что в итоге получилось?

На мой взгляд – из всего написанного выше можно сделать вывод, что мой способ работы с нейросетями (через жёсткий сценарий, ключевые слова и минимальное непосредственное влияние нейросети на то, что видит пользователь) как минимум имеет право на жизнь и стоит того, чтобы дальше его развивать, чем я и планирую заниматься. Всё-таки огромная часть интеллектуальной работы в экономике требует не абсолютного творчества, а использования креативности для создания результата, вписывающегося в какие-то жёсткие рамки, потому что иначе этот результат просто не получится использовать. И как мне кажется, описанный подход – это довольно простой способ заставить нейросеть работать именно так.

Отмечу отдельно, что не очень претендую на уникальность этого подхода и своё авторство. Мир большой, а технология новая, так что хотя я и не нашёл у кого-либо ещё всего того же самого в тех же пропорциях – вполне допускаю, что просто плохо искал.

Если вас вся эта история заинтересовала, и у вас сразу появилось желание прикрутить ещё что-нибудь сбоку, или конструктивная критика, или же мысль «О, я точно знаю куда эту штуку можно воткнуть» – пишите, буду рад любому интересу к этой теме!

P.S. Если интересно пощупать описанный сервис руками и попытаться заставить его ошибаться – он реализован в описанном виде (с объединением сообщений и форматом !PURCHASE) здесь. Корректное определение процесса отображается как <Переводим ваш вопрос на специалиста по процессу purchase/send/receive>, неспособность определить процесс и ошибка формата отображаются как <Нейросеть не справилась с определением процесса, переводим на сотрудника 1 линии>. При обновлении страницы создаётся новый диалог, но можно снова зайти в старый диалог (или поделиться диалогом), добавив параметр адресной строки conversation_id=<указанный сверху id диалога>, например так.

Комментарии (0)