Как приручить LLM

от автора

  • Пробовали сделать цифрового сотрудника на LLM?

  • У вас получилось?

  • Долго старались?

  • Насколько он качественный?

  • Сколько он стоит в обслуживании?

Очень много вопросов возникает к технологии ведения диалогов нейросетями.

- Ты всегда следуешь скрипту и не импровизируешь.

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

ТыжИИ! Сделай хорошо, быстро, дешево!

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

Суть проблемы — слишком широкое окно контекста у LLM. Человек за свою жизнь привык жить со своим маленьким окном контекста, поэтому, приступая к большой задаче, он «ест слона» по частям, выделяя из множества информации именно то, что сейчас ему актуально. LLM же одновременно опирается на всю инструкцию, и на длинных текстах её работа превращается в хаос вместо структурированного диалога. Для решения больших задач делают агентов, которые умеют разбивать задачу на части и решать её поэтапно, как это делает человек. Но количество вызовов нейросети (а соответственно, и стоимость диалога) при этом вырастает в разы, зачастую делая применение LLM нецелесообразным.

При разработке своего бота я столкнулся с узкими рамками борьбы между стоимостью и качеством диалога. Я не мог позволить себе строить сложную агентскую сеть для решения довольно простой задачи — следовать скрипту. И тогда я решил «приземлить» LLM к скрипту более дешевым и эффективным способом, не сжигая лишние токены, а задав структуру сценария жестко в логике бота.

Приземляем LLM

Каких-то вменяемых готовых решений беглый обзор GitHub не выявил. Тогда я решил сделать своего агента «с блэкджеком и дешевыми токенами», который будет следовать четко установленной инструкции. И, поскольку решаемая проблема следования инструкциям типовая для самых разных применений, решать задачу я решил в общем виде. Идею сформулировал так: “Нужна библиотека для построения чат-ботов, которые смогут вести диалог по заранее определенному сценарию. При этом сценарий должен храниться отдельно от кода и быть легко заменяемым.” Движок — отдельно, предметная — область отдельно. Главная идея: Граф ведёт, модель исполняет.

Код можете найти на github: https://github.com/chechestor/tg-statemachine-bot. В репозитории открыт раздел с обсуждениями, добро пожаловать.

В основу были заложены следующие принципы:

  1. Весь диалог разбивается на этапы, нет общения вне этапа диалога.

  2. Этапы объединены в граф с направленными переходами. Из этапа может быть несколько выходов в зависимости от результатов этапа.

  3. У каждого этапа есть определенная цель. Как правило, это получение некоторой информации из ответов собеседника.

  4. Входная и выходная информация каждого этапа организована в именованные переменные. Выходные переменные одних этапов могут быть входными переменными других этапов.

  5. Общение на каждом этапе ведет LLM. Она же принимает решение о необходимости перехода на следующий этап, формируя соответствующий сигнал перехода.

Граф общения бота.

Граф общения бота.

Так в основу архитектуры легли три модуля:

  • fsm — (Finite State Machine) машина состояний пользователя. В ней хранится граф состояний диалога и текущее состояние пользователя.

  • vars_memory — память переменных диалога.

  • conversation_core — собственно LLM-болталка. Оркестратор для fsm и vars_memory.

Conversation_core. Особенности оркестрации.

Деление инструкции по этапам — это основной механизм, который позволяет LLM работать по скрипту любого объема. За счет того, что в конкретный момент времени модели передается только инструкция текущего этапа.

Инструкция для вызова LLM собирается как композиция:

  • общая роль и правила (common.md);

  • инструкция текущего этапа;

  • только те входные переменные, которые для этапа явно прописаны (stage_input);

  • прогресс этапа: что уже заполнено, чего ещё нет;

  • доступные сигналы перехода.

Модель на каждом ходу отвечает не свободным текстом, а фиксированным контрактом из четырех полей:

  • response_message — что сказать пользователю;

  • fsm_signal— какой переход совершить (или null, если ещё рано);

  • stage_result— структурированные данные этапа;

  • stage_summary — саммари при завершении этапа.

Как известно, LLM умеют нарушать инструкции, а иногда и врать. Даже если в инструкции четко описаны все сигналы, допустимые для текущего состояния, модель может выдать несуществующий (я называю это «на основании собственного опыта»). Поэтому в conversation_core вокруг вызова LLM дополнительно введена обвязка, которая при обнаружении нарушений контракта делает перезапросы в модель от роли developer, указывая на проблему и требуя исправить её. К таким ошибкам относятся, например: нарушение JSON-схемы ответа, несуществующие сигналы перехода, отсутствие результатов этапа при попытке перехода. Для связанности диалога я решил не «изобретать велосипед», а воспользоваться готовым решением — ведением цепочки Responses API через previous_response_id. Так даже при переходе между этапами сохраняется адекватность общения без резких смен темы, переспрашиваний и т.п. Одновременно такое решение уменьшает расходы за счет cache-read с более дешевой ставкой. В ходе работы над кодом обнаружилась потребность применять LLM не только для общения, но и для агрегации ранее полученных знаний. Поэтому пришлось выделить отдельный вид этапов — так называемые технические этапы, на которых модель без видимой для собеседника активности «что-то обдумывает внутри себя». Например, когда нужно составить отчет по этапу и сохранить его в переменные.

vars_memory

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

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

fsm. Описание инструкции

Основное назначение модуля — ориентироваться по состояниям общего скрипта и предоставлять данные только актуального этапа. Общая схема диалога описана в виде YAML-файла. Объемные инструкции этапов вынесены в отдельную папку /instructions/. YAML и инструкции этапов — этого достаточно для того, чтобы сменить прикладную область бота — сам движок менять не нужно.

Как с этим работать

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

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

Но вам надо будет как-то контролировать результат же. Но даже читать YAML своими глазами не советую, утомляет. Чтобы ориентироваться в нейрослопе, пришлось разработал небольшой скрипт /tools/generate_fsm_drawio.py. Скрипт генерирует из YAML диаграмму в формате draw.io, вполне удобную для анализа человеком и контроля результата работы нейросети.

Для тестового периода необходим сбор логов диалогов тестировщиков, чтобы оценивать качество ответов и адекватность поведения бота. Для этого логи собираются в папку /data/chat_logs. Просматривать голый текст глазами оказалось вредно для зрения. Поэтому решил немного «облагородить» логи и разработал небольшой вьюер /tools/view_log.html. Он отображает переписку в виде, похожем на привычный чат, а также выводит спойлеры с системным логом. Удобно смотреть переходы между состояниями. При отладке на локальном ПК можно также включить автообновление, чтобы наблюдать лог в реальном времени.

Пример применения

В репозитории выложен код бота-аналитика. Цель бота — помочь разработчику пет-проекта с постановкой задачи на разработку. Скрипт проводит пользователя по этапам разбора требований, задавая вопросы, которыми начинающие соло-фаундеры часто не имеют привычки задаваться. Этот бот-аналитик не претендует на звание «бота №1» среди аналогичных решений, а приведен лишь как пример применения библиотеки conversation_core.

Итог применения conversation_core получился довольно приятным: на каждое сообщение пользователя приходится примерно 1,03 вызова LLM, что довольно недорого. Бот держит линию диалога, следует скрипту и собирает информацию именно так, как я того и хотел. Он не идеален, есть известные косяки, в том числе для демобота я забил промт инжениринг. Проект есть куда развивать, и, я уверен, обязательно найдется читатель, который лучше меня ориентируется в ботостроении. Буду рад вашим комментариям, отзывам и предложениям.

ссылка на оригинал статьи https://habr.com/ru/articles/1066298/