📱

Get Our Mobile App

Take your business learning on the go!

Download on the App StoreGet it on Google Play

Я Отвечал На Эти Вопросы 1000 Раз... Или Как Сделать FAQ ИИ Агента

ИИшенка | AI Automation20:22

Transcription

Друзья, всем привет. Ну что, как встречаем Новый год? Всё хорошо? Мне кажется, новогодние праздники - это прекрасное время, чтобы освоить какие-то новые навыки. И сегодня мы с вами построим мегаинтересную вещь. Эта вещь называется FAQ - frequently asked questions, то есть вопросы, которые часто задаются и ответы на которые будет искать наш ассистент. Ну то есть вы наверняка сталкивались с ситуацией, которая звучит примерно таким образом. Какой-то человек говорит: "Я уже тысячу раз отвечал на эти вопросы. Я хочу сделать какого-то ассистента, чтобы он знал все мои ответы на подобные вопросы и отвечал вместо меня". То есть, по сути, это некоторый мозг человека, который встроен в нашего ассистента и который отвечает за нас. В более продвинутом случае это некоторая команда, например, саппорт-команда, которая общается с нашими клиентами, отвечает на их вопросы. И мы хотим на основе вопросов и ответов сделать ассистента, задать ему какую-то долговременную память, чтобы мы экономили на нашей поддержки, а наши клиенты получали квалифицированные ответы, как и раньше. И сегодня мы построим простейшую версию такой автоматизации, такую, чтобы можно было довольно быстро разобраться и уже к концу праздников у вас был бы уже полностью работающий агент. Но как всегда, у меня для вас три базовых условия. Обязательно ставим лайк, подписываемся там внизу и оставляем какой-то коммент, чтобы это видео увидело больше людей. Погнали строить автоматизацию.

Нам с вами понадобится и сама система вопросов-ответов. То есть наверняка, если вам интересна данная тема, то у вас уже есть какая-то коллекция вопросов и ответов, которые вы где-то ведёте. Храните где-то в базе либо в другой системе автоматизации, в Интеркоме, в Телеграме, в Ватсаппе или в любой другой подобной системе. И наша с вами задача, естественно, научиться с этим работать. И прежде чем мы начнём строить автоматизацию, давайте посмотрим на сам формат данных. Я вот только что выгрузил все свои вопросы и ответы в небольшой JSON. Давайте его откроем. Обратите внимание, как он выглядит. Здесь есть такие сущности, как вопросы, ответы, уже сгруппированные вместе. Есть ID такого набора вопросов-ответов. Ну и категория. Категория, кстати, нам сейчас не очень важна, но пригодится на будущее. В принципе, ваша задача добиться такого же формата данных. То есть я вам рекомендую собрать вопросы схожие, ответы схожие в такие вот группы и каждой группе присвоить свой ID.

И здесь мы подходим сразу к двум базовым понятиям создания FAQ-агентов. Здесь мы должны понять, что методов построения FAQ-агентов огромное количество. Мы будем строить простейший, который мы сможем освоить буквально за несколько часов. Поэтому мы применяем два правила. Правило номер один: мы векторизуем только вопросы и не векторизуем ответы. Это нам помогает сконцентрировать знания для векторного поиска в одном месте и повысить точность нахождения ответов. Правило номер два называется нормализация. Всё разнообразие вопросов по заданной теме или категории нам нужно нормализовать. Это что значит? Наши пользователи могут спрашивать в совершенно разных форматах про одни и те же вещи. Например, эта категория про билинг, про взимание платы с наших клиентов. И, как мы понимаем, клиенты могут очень по-разному спрашивать, например, про возврат денег после отменённой подписки. Начиная от нейтральных вопросов, заканчивая совершенно разнообразными и заострёнными вопросами. Например, "Какого лешего, где мои деньги?". Мы должны осознавать, что леший нас никоим образом не интересует, а вот возврат денег и ответ на этот вопрос нас интересует. Поэтому наша задача в подготовке данных нормализовать вопросы, избавив их от лишних эпитетов, причастных, депричастных оборотов, оставить только суть. И вот такая конструкция нас уже интересует. Мы её подготовили. Забирайте из своей базы. Создайте примерно такой JSON файл.

Ну что, давайте автоматизировать. Так как у меня файл лежит на моём локальном диске, я хочу его забрать с этого диска. Добавлю ноду `readr files from dis` и здесь сразу пропишу место, где у меня лежит этот файл. Чтобы это заработало, ваш NVC мог обращаться к локальным файлам, посмотрите вот это видео. Мы недавно обсуждали тематику, как открывать ваш диск для NVAN. Я же сразу вписываю путь до моего файла. Сразу нажимаю `Execute`. И вижу, что вот этот самый файл, давайте его откроем, посмотрим. Вот этот самый файл, который мы только что смотрели с вами в AntiGravity. Теперь нам с вами понадобится нода `extract`, она так и называется. И здесь выбираем `extract from json`. В принципе, ничего не меняем, выполняем эту ноду. Видим, вот наш JSONчик весь мы получили у себя в Nym. Отлично. Так как у нас там большое количество ответов и вопросов, то есть блоков, давайте разобьём с помощью ноды `Splitout` - это на отдельные айтемы. Добавляем ноду `Splitout` и делим по полю `FQ`. То есть берём, прямо перетаскиваем сюда, получилась `data.FQ`. Выполняем шаг и видим, что теперь у нас есть 50 айтемов. Отлично. Это то, что нам нужно. Я себе для тестовых нужд лимитирую эти 50 штук до десяти. Просто чтобы напрасно не гонять нейронку для тестов. Выполню эту ноду `FQ`. Видим, что он нам отсёк первые 10 элементов. Отлично.

Дальше нам с вами понадобится база, векторная база, где мы будем хранить векторизованные наши вопросы. Помните, мы с вами обсуждали, мы сейчас будем векторизовать только вопросы, не ответы. И мы исторически с вами пользуемся Superbase. Опять же, в прошлом видео, посмотрите, мы обсуждали много раз Superbase. Я предполагаю, что у вас уже это всё развёрнуто, поэтому нажимаю плюсик, добавляю ноду `Superbase`. Нас интересует именно `Superbase Vector Store`. И здесь мы выбираем `Add documents to vector Store`. И здесь наша задача - выбрать таблицу. Я себе только что создал таблицу с именем `documents_4`. Выбираю её и здесь добавляю `query_name: match_documents_4`. Опять же, наша задача - выбрать себе векторную таблицу и функцию поиска к этой векторной таблице. Если вы уже разворачивали себе Superbase, то вы понимаете, о чём речь. Если нет, то вот в этом видео вы можете найти все объяснения. Закрываем эту ноду, добавляем сюда нейронку. Пусть это будет нейронка от OpenAI. И нас интересует модель `text-embeddings-3-small`. Оставляем её. Это выбор по умолчанию. Теперь наша задача - добавить `data loader`. Добавляем такую ноду. И здесь нам нужно немножечко изменить эту конфигурацию. Нам не нужно загружать все данные. Нам нужно загрузить только определённые данные. Я хочу, чтобы загружались только вопросы. Я сюда добавляю `questions` и видим, как сюда прокидывается именно массив наших вопросов. Я думаю, что наша векторная база будет испытывать некоторые проблемы с загрузкой массива, поэтому я предлагаю превратить это сначала в строку, так как все эти вопросы нормализованы, и они примерно про одно и то же, нам это сильно не повредит. Поэтому здесь я прописываю следующее: `JSON.stringify(Questions)`. Беру скобочки `JSON.Questions`, закрываю скобочку. И теперь видим, что уже вместо массива здесь чисто строка. Отлично, нам это и нужно было. И теперь я добавлю два поля в метаданные. Нажимаю `Options`, `Metadata`. Это мегаважная штука, потому что по этим метаданным мы потом будем искать релевантные ответы к вопросам, которые найдёт наша нейронка и векторный поиск. Добавляю `property`. Давайте его назовём `FAQID`. А значение у нас уже есть. Вот наше `FAQID`. Мы его берём сюда, помещаем и будем его сохранять. Прекрасно. Ещё одно `property`, назовём это `category` и прокидываю `category`. Прекрасно. Давайте запускать эту ноду. Выполняем её. Смотрим, как выполняется. Видим, что 10 раз у нас прошло всё через `DataLoader` и на выходе было 10 элементов. Возвращаемся в нашу Superbase и видим, что вот они наши 10 записей. Давайте их проверим. Отлично. Это как раз те самые вопросы, которые мы хотели векторизовать. Видим, что к ним добавились метаданные, `FAQID`, `category`. То есть как будто бы пока идёт всё хорошо.

Друзья, если вы переживаете за эту автоматизацию, то после каждого YouTube видео, всё, что мы строим в рамках видео, я выгружаю прямо отсюда и загружаю в свою бесплатную Telegram-группу. Ссылка на неё в описании. Вы можете туда прийти, пролистать, найти нужное вам видео и найти релевантный к нему JSON. Скачать этот JSON и потом через те же самые три точки нажать `Import from file`, загрузить и продолжать экспериментировать с этой автоматизацией. Так что всё находится в этой группе, забирайте. Ну а в случае, если мы с вами строим усложнённые автоматизации, то они всегда в Pro-группе. Ссылка на неё тоже в описании. Заходите.

Ну а вообще, мы с вами уже на полпути к успеху. У нас уже есть векторизация данных. Давайте же построим самого агента, который будет на основе этих данных давать ответы нашим клиентам. Нажимаю плюс, добавляю `ChatTrigger`. То есть мы через `ChatTrigger` будем закидывать вопросы. Давайте поставим его красиво, ровно. И сейчас мы с вами подходим к ключевому принципу работы с FAQ-агентами. Это принцип предварительной фильтрации. Мы с вами не будем сюда добавлять никакого агента, который сам будет искать данные и потом сам их интерпретировать и отвечать на их основе. Наша задача построить классную систему. Поэтому мы сами будем искать релевантные группы вопросов, доставать их, смотреть на `FAQID` и по этому ID доставать уже релевантные ответы, которые мы предоставляли ранее. Это мегакруто. Смотрите, добавляем с вами ноду опять `Superbase Vector Store`. И теперь выбираем функцию `Get documents`. Выбираем ту же самую таблицу. Это `documents_4`. Добавляем ту же самую функцию поиска. `Match Documents 4`. Обязательно ставим вкладочку `Include metadata`, добавляем нейронку. Давайте опять добавим нейронку от OpenAI. Давайте теперь зададим какой-то вопрос для того, чтобы у нас был пример. Пока просто скажем "Привет". У нас с ошибками наша следующая нода, потому что мы пока не прокинули никакой промт. Открываем ноду `Superbase` и сюда вписываем такую конструкцию `JSON.parse(input)`. Это именно то поле, где содержится инпут от нашего чата, что логично. Вот мы сразу видим наш "Привет". Выполняем эту ноду. И, естественно, мы уже видим результаты векторного поиска, который нам вернулся из нашей базы данных. И теперь мы можем понять, почему мы не используем ноду агента. Потому что векторный поиск всегда вернёт вам какие-то данные, вне зависимости от того, насколько релевантны эти данные. Если бы сейчас это вернулось агенту, он бы уже нам начал давать ответы. Но нас это не устраивает, потому что если мы посмотрим вот сюда на `score` - это рейтинг релевантности, мы увидим, что эти чанки, которые вернулись нам, всего лишь на 27% релевантны вопросу, который спросил наш пользователь. И так как мы сами строим автоматизацию, здесь мы можем принять решение, использовать эти чанки или не использовать эти чанки для ответа нашему клиенту. За некоторый стандарт индустрии принято 68%. То есть все чанки, которые превышают в точности 68%, могут быть использованы для формирования ответа. Всё, что меньше 68% не должно быть использовано, должно быть отсечено или доуточнено у пользователя с помощью фоллоапов или так называемых `human in the loop`, то есть некоторого подтверждения, что да, ты меня понимаешь верно либо неверно. Мы, кстати, на это тоже сегодня посмотрим.

Так как мы с вами уже выяснили, что 68-70% - наша искомая величина, давайте добавим просто ноду `If`. И так, как нам наша Superbase прекрасная возвращает `relevant_score`, давайте попробуем отфильтровать. Прокинем сюда `score`. Нажмём здесь выбор `number`. И здесь `is greater than`, то есть более чем. Здесь так и пропишем `.7`. Выполним эту ноду. И мы видим, что по `true` ничего не ушло, а вот по `false` ушли все-все-все чанки, которые оценены ниже 70%. Для нас это означает, что сейчас мы среди нашей информации не можем найти что-то релевантное в вопросы пользователя. Давайте так и скажем: "Я тебя не понял. Уточни или перефразируй свой вопрос". Единственное, перед этим давайте добавим такую ноду, которая называется `Limit`. Она лимитирует выход предыдущей ноды до одного. Это просто сделано для того, чтобы мы четыре раза не ответили ему: "Уточни свой вопрос". И автоматизации с `ChatTrigger` работают таким образом, что всё, что выходит `output`, то есть в конец автоматизации, и есть ответ в наш `ChatTrigger`. Поэтому мы можем совершенно не заморачиваясь добавить ноду `Set`. Здесь добавить поле, которое называется `answer`, то есть ответ. И здесь так и написать: "Я тебя не понял. Перефразируй, пожалуйста". Отлично. И теперь, если мы исполним эту ноду, мы видим как раз наш ответ: "Я тебя не понял. Перефразируй, пожалуйста". Давайте опять откроем наш чат и ещё раз спросим: "Привет!". Восклицательный знак. Ответ: "Я тебя не понял. Перефразируй, пожалуйста". Ну прекрасно. Негативный сценарий выполнен. Мы не засоряем чат, не засоряем мозг нашего пользователя нерелевантной информацией, потому что то, о чём он спросил, мы об этом ничего не знаем. Перефразируй.

А что будет, когда релевантная информация найдена? Например, я говорю: "Я забыл пароль". Отправляю, смотрю, что происходит. Так, и теперь у нас уже интересней. Теперь я вижу, что один чанк выполнил наши условия, то есть пробил наш трешхолд. И он годится для того, чтобы сформировать ответ нашему пользователю. Вот как он выглядит. "Я забыл пароль. Как установить пароль? Я не помню пароль". То есть очень точно определён чанк, который нам пригодится для того, чтобы дать ответ нашему пользователю. Давайте ещё раз добавим лимитирующую ноду для того, чтобы если у нас было несколько чанков, я бы взял только первый. Вы можете брать сколько вам угодно. Я для этого примера возьму только первый. И теперь интересный вопрос. Хорошо, вопросы-то мы нашли релевантные, но нам же нужно взять ответы и на основе этих ответов дать точную информацию нашим пользователям. Каким образом это делается? Помните, мы в самом начале обсуждали, что ответы векторизованные нас не интересуют. Ответы нам нужны гиперточные, соответствующие нашему вопросу. Давайте достанем ответы, которые точно на 100% соответствуют заданному вопросу. Где наши ответы лежат? Ну, в нашей автоматизации они до сих пор лежат в файле. Но мы идём наверх и заберём прямо вот эти все три ноды. Потому что эти три ноды нам уже помогали с тем, чтобы достать наш JSON файл, в котором лежат все вопросы-ответы. Давайте мы опять его достанем для того, чтобы прочитать все релевантные ответы заданному вопросу. Давайте выполним ноду лимитирования. Да, вот наш чанк. Теперь мы читаем тот же самый файл. Опять извлекаем из него все данные, сплитуем, и нам возвращаются все наши 50 групп вопросов и ответов. И здесь настаёт время нам найти точные ответы тому вопросу. Каким образом мы их ищем? Да, это делается мегапросто. Ведь нейронка нам отдала релевантные чанки, а мы позаботились об этих чанках и добавили к ним метаданные. Помните, мы добавили `category`, а ещё мы добавили `FAQID`. А ведь `FAQID` - это тот самый ID, который соответствует и группе вопросов, и релевантным ответам. Это значит, что по этой айдишке из нашего файла мы можем достать неизменённые оригинальные ответы, которыми мы будем пользоваться, чтобы отвечать нашему пользователю.

Возвращаемся в нашу автоматизацию и добавляем такую ноду, которая называется `Filter`. `Filter` работает мегакруто и мегапросто. Здесь нам нужно из пятидесяти блоков, которые нам вернула нода `Splitout`, выбрать именно тот блок, который соответствует нашей айдишке, которую нам вернула нейронка. Где мы в последний раз видели эту айдишку? Правильно, в ноде `Limit`. Поэтому здесь схлопываем, ищем ноду `Limit` и видим вот тот самый чанк. И здесь среди прочего есть айдишка. Закидываем сюда. И таким образом выстраиваем историю. Из всего файла, JSON файла, отфильтруй мне ту самую группу, у которой `FAQID` равен `4`. Мегапросто. Выполняем ноду и видим, что нам возвращается именно та самая нода, где есть оригинальные ответы, которые нам сейчас мегапригодятся для того, чтобы сформировать ответ. Закрываем тут. И, наконец, настало время добавить агентскую ноду. Так и пишем: `Agent`. Добавляем её. Какой должен быть промт у агента? Ну, понятно, что тот самый промт, который нам написали в чате, поэтому здесь меняем на `Define below`. Ищем наш чат. Видим, что `Chat Input` прямо здесь находится. Прокидываем сюда. Так, вот человек забыл пароль. И теперь ещё добавляем `System Message` и пишем сюда такое: "Ты агент поддержки пользователей. Строй свои ответы строго на основе этого контекста". А контекст - это те самые ответы, которые мы только что получили из фильтра. Берём ответы, закидываем сюда и видим тот самый правильный текст, которым мы будем пользоваться для ответов. Так, нам к агенту `Model`. Опять же, это будет `OpenAI`. `Memory`. Обязательно добавим `Memory`, потому что человек может несколько раз обращаться к нашему агенту. Мы должны дать агенту контекст о том, что он уже отвечал нашему пользователю. Поэтому мы здесь добавляем `Postgres`. Если вы разворачивали Superbase, то Superbase - это, в принципе, Postgres база данных. Поэтому по кредам вы тоже можете подключиться к ней, не только через Nym. Поэтому здесь прокидываем мы их креды и здесь прописываем ключ. А ключ сессии он также у нас хранится в нашей ноде `ChatTrigger`. Вот он `Session ID`. Прямо сюда его закидываем. Отлично. И в принципе нашему агенту больше ничего не нужно. Давайте протестируем весь workflow вместе. Открываю, очищаю. Если я говорю что-то нерелевантное, говорю: "Привет". Я тебя не понял. Перефразируй, пожалуйста. Теперь, если я пишу "Привет!". Кажется, я потерял пароль от своего аккаунта. Нажимаем Enter. Наш агент думает, проходит дальше по автоматизации. И мы получаем: "Я тебя не понял", потому что мы с вами допустили ошибку в автоматизации. Если мы посмотрим вот на это место, мы видим, что какие-то чанки ушли наверх, что, скорее всего, релевантно, и наши правильные чанки ушли наверх. Но при этом все нижние чанки, то есть нерелевантные чанки, ушли вниз, что и сформировало ответ: "Я тебя не понял". Это, естественно, нас не должно устраивать, поэтому мы не сдаёмся. И вот сюда, между нашей нодой `Superbase` и нашей нодой `If`, мы нажимаем плюс и добавляем ещё одну фильтрацию. Добавляем её. И сюда я добавлю то же самое условие. Я скажу, что нас интересуют только чанки, у которых `relevant_score` больше шестидеся. Поэтому прописываю здесь условие `is greater than`. Тогда у нас, в принципе, не пройдут никакие чанки дальше в автоматизацию, а только нужные чанки, с которыми мы можем пообщаться. Если же попадут какие-то чанки меньше этого скора, то из этой ноды вернётся пустое значение. Поэтому мы должны здесь выбрать в её настройках всегда возвращать какое-то значение для того, чтобы наш `If` среагировал по `false`. Так как у пустого значения JSON Score будет ниже заданного значения, вернее, его вообще не будет, то у нас всё уйдёт по нижней ветке и должно всё работать хорошо. Поэтому давайте проверять ещё раз один нерелевантный запрос. "Привет". Да, видим, всё прошло хорошо, и он нам ответил: "Я тебя не понял, перефразируй". Теперь я хочу ещё раз послать вот эту автоматизацию, вернее этот вопрос. И прекрасно, у нас всё вернулось. "Здравствуйте. Пожалуйста, используйте функцию сброса пароля на странице входа. Мы отправим вам письмо для восстановления доступа. Если письмо не приходит, проверьте папку спам". То есть это именно те самые правильные ответы, которые так нужны нашему пользователю.

Здесь, конечно же, может всё быть круче. Например, это всё может работать с Телеграмом, да ещё и с подтверждением пользователя. Давайте я вам сейчас покажу. Более продвинутые системы в случае, если они сомневаются в том, насколько они правильно нашли сейчас ответ, они могут уточнять у пользователя, что вы вот про это сейчас говорите. Например, вы это поведение можете видеть в классических чатах поддержки. Например, если это полностью нерелевантная история и пользователь говорит: "Я почистил зубы", тогда система думает, думает и говорит: "Перефразируй". Если пользователь подходит как-нибудь неформально, например, "Я профукал пароль", тут можно как бы двояко понять всё. То система спросит подтверждение у пользователя с помощью так называемого `Human in the loop`. Только в случае, если пользователь говорит "да", то система думает и отдаёт ему ответ, который точно релевантен этому вопросу. Ну а если система уверена больше, чем на 70%, то без всяких дополнительных вопросов она готовит ответ на основе данных, которые мы ей дали, и формирует его и отвечает точно по заданному вопросу с ответами, которые мы ей дали. Естественно, такая автоматизация требует намного более ветвистой структуры. Здесь волшебных пилюль нет. Поэтому, если вам интересна более усложнённая версия, которая в случае неуверенности может дозадавать вопросы и ждать ответов от пользователя, то эту автоматизацию вы ищите в Pro-группе, ссылку на которую вы найдёте тоже в описании. Ну а вот эту бесплатную версию, которая уже может выполнять большинство ваших задач, ищите в бесплатной Telegram-группе. Ну а на этом всё. До скорых встреч.