Автоматизуємо службу техпідтримки за допомогою RAG: порівнюємо OpenAI + Pinecone й OpenAI Assistants API

💡 Усі статті, обговорення, новини про AI — в одному місці. Приєднуйтесь до AI спільноти!

LLM-революція у розпалі, а на DOU досі жодної великої статті про RAG (retrieval-augmented generation)? 🤔 Непорядок. Будемо це виправляти.

У цій статті я поділюсь двома бекенд-застосунками, що послуговуються технологією RAG і можуть бути використані для автоматизації служби техпідтримки або вирішення інших завдань типу chat with your documents. У першому випадку це буде RAG, зібраний самостійно з OpenAI API (Embeddings, Chat Completion) і векторної бази даних Pinecone (назвемо її «кастомною»).

Другий застосунок побудований на основі нещодавно анонсованої OpenAI Assistants API, яка реалізує RAG «з коробки» (функція Retrieval). Інші технології, використані у цих застосунках: node.js / Typescript, Postgresql, Python.

З продуктової точки зору, ми розглянемо аналог Chatbase, де клієнт отримує чатбота, здатного зв’язно й адекватно відповідати на основі клієнт-специфічних даних. У кінці статті в якості бонусу я поділюся невеликим дослідженням ринку та ідеєю одного з продуктів, який може бути реалізованим на основі цього коду.

Аудиторією статті бачу технічних і нетехнічних спеціалістів, які знайомляться з LLM і рішеннями на їхній основі. Постарався написати просто і з визначенням базових термінів, фокусуючись на більш загальних поняттях і логіці, щоб стаття була зрозумілою зокрема для тих, хто тільки знайомиться із цією темою.

Місце RAG серед варіантів архітектури AI застосунків

Перед тим, як ми перейдемо до суті, вважаю за корисне коротко поглянути на популярні варіанти побудови AI-застосунків і місце RAG серед них. За однією з класифікацій, наведеною у цьому відео, станом на середину 2023 серед популярних варіантів архітектури можна було виділити такі:

  • «Базова» / «проста» — є LLM і наш застосунок, який надсилає LLM запити (prompts, далі — промпти), які можуть бути як простими запитаннями, так і містити різні прийоми промпт-енжинірінгу, як-то задання ролі (Act as a senior JS developer), бажаного типу контенту (стаття, лист, скрипт тощо), його формату (Show as markdown / list / PDF тощо) та ін. Цей підхід є основою для всіх інших зазначених нижче.
  • RAG (retrieval augmented generation, генерація, доповнена пошуком) — коли запит до LLM «збагачується» інформацією, на основі якої модель має відповісти. Тобто промпт виглядає як «Дай відповідь на запитання X виходячи з інформації Y». Для пошуку релевантної інформації використовують бази даних та технології пошуку (детальніше — див. нижче).
  • «Багатокрокова» архітектура — група більш складних і різноманітних підходів, спільною характеристикою яких є те, що для отримання відповіді виконується кілька запитів до LLM з різними промптами. Існує багато варіацій підходу, наприклад, розділення задачі на дрібніші завдання з їхнім подальшим автономним виконанням у циклі (AutoGPT, BabyAGI), виконання завдання групою спеціалізованих агентів з можливим залученням людини як модератора (Autogen) та інше. Підхід може містити RAG та використання різних інших інструментів (функцій, плагінів, інтеграцій).

За результатами опитування власників АІ-проєктів, приведеними у цьому відео, приблизно 2/3 опитаних використовували у своїх застосунках RAG (або ж розробники RAG більш активно беруть участь у опитуваннях 😉), але у будь-якому разі схоже, що ця архітектура є однією з найбільш популярних).

Що таке RAG і нащо він

Отож уявімо, що ми хотіли б полегшити роботу служби техпідтримки нашого онлайн-магазину, створивши чатбота, що відповідає на популярні звернення користувачів. Сучасні LLM здатні генерувати вельми зв’язні відповіді. Проблема в тому, що для задач типу озвученої, відповіді мають бути «контекстними», тобто про конкретний бізнес / продукт тощо. Природньо, що LLM OpenAI, скоріш за все, не «знає» годин роботи вашого магазину, доступних варіантів оплати чи нюансів повернення покупки.

Проблему можна вирішити принаймні двома способами:

  • Дотренувати модель на власних даних. Потенційно робочий підхід, однак має нюанси — потрібні дані (краще — більше), час і вартість [періодичного] тренування тощо. За нашим досвідом в Мaster of Code Global, для проєкту на кшталт автоматизації техпідтримки і за умови, що ми використовуємо комерційні LLM типу OpenAI, цей підхід не завжди може бути оптимальним. Якщо спробувати створити спеціалізовану модель на основі open-source-типу LLAMA-2, імовірно, озвучені параметри (ефективність, вартість) можуть бути іншими, але з’являються інші — час і вартість реалізації, підтримка власної LLM тощо.
  • Посилати моделі запитання, додаючи у запит також «контекст» (текст з можливою відповіддю). Наприклад:

Загальна ідея Retrieval Augmented Generation:

Підготовка контекстів для RAG

Для того, щоб наш RAG-застосунок міг відповідати на запитання користувачів на основі певного контексту, нам потрібно цей контекст підготувати.

Якби ми виконували завдання від реального клієнта, то логічно було б запросити в нього транскрипти спілкування служби техпідтримки з користувачами (містять як запитання, так і відповіді), або хоча б матеріали, що слугуватимуть контекстами для відповідей (FAQ з вебсайту, інші бази знань).

Оскільки ми створюємо застосунок для абстрактної служби техпідтримки, все це доведеться створити самим. Припустимо, що типовий діалог між відвідувачем і агентом техпідтримки виглядяє так:

або те ж саме, але спочатку small-talk, а потім уже власне питання:

Закономірно, частина звернень можуть містити незрозумілі запити або бути взагалі «не в тему». Також відвідувач, якщо він спілкується з ботом, може захотіти переключитись на агента.

Я для генерації тестових даних скопіював FAQ одного з онлайн-магазинів і зберіг його у Google Spreadsheet, після, за допомогою Python-скрипта, перефразував тексти і прибрав із них будь-які ідентифікатори (назви, телефони, адреси тощо; приклад у OpenAI Playground).

Після цього за допомогою іншого Python-скрипта до кожного тексту було згенеровано по три запитання (приклад у OpenAI Playground).

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

Тепер нам потрібно реалізувати власне RAG (retrieval augmented generation). Спершу ми розглянемо більш «кастомний» підхід, де наповнимо векторну БД вручну та поєднаємо БД із запитами до LLM власною логікою. Після цього познайомимось з реалізацією на основі OpenAI Assistants API, де ці процеси абстраговані від користувача API і працюють «із коробки». Але спершу ще трохи теорії.

Векторні бази даних — що це таке і як працює

Окрім загальновідомих моделей для генерації тексту (типу GPT-4), у тієї ж OpenAI є Embeddings-моделі. Це моделі, які для вхідного тексту (слова / фрази / сторінки) генерують так званий вектор — великий масив чисел з плаваючою комою, що слугує свого роду «хешем» переданого тексту й показує, де у багатовимірному просторі моделі перебуває цей текст. Тексти зі схожим змістом, навіть написані різними словами, мають ближчі за значенням вектори.

Отож нам потрібно «векторизувати» наші контексти: згенерувати для них вектори та зберегти їх разом з оригінальними текстами у базу даних для подальшого пошуку. Після цього вже в runtime для кожного запитання від користувачів ми також будемо генерувати вектор, за цим вектором шукати схожі вектори і повертати (якщо знайдемо) їхні тексти.

Існує багато різних векторних баз даних. Дещо раніше я провів невелике порівняння Pinecone, FAISS і pgvector. У кастомному варіанті RAG, представленому в цій статті, я використав Pinecone як варіант, найбільш швидкий і зручний для прототипування.

Отож за допомогою іншого Python-скрипта для кожної з пар запитання-відповідь, які були отримані нами раніше, був згенерований вектор, який разом з текстом був збережений у Pinecone. Крім вихідного тексту, кожен запис мав поле data_type, яке могло мати значення knowledge_base (власне контексти для відповідей) або raise_to_support (запити на переключення на живого агента).

Так у загальному виглядала retrieval-augmentation-частина проєкту. У разі реального продукту замість скриптів для ручного запуску відповідна логіка може бути реалізована як окремий сервіс. Можливий self-service-варіант, де клієнт завантажує бази знань у вигляді файлу чи вказує лінку на сайт для скраппінгу, і сервіс автоматично парсить дані, ділить їх на частини, генерує вектори та зберігає їх у векторну БД. Категорії записів (knowledge_base, raise_to_support тощо) також можуть відрізнятись чи бути відсутніми залежно від конкретних вимог.

Автоматизація техпідтримки — «кастомний» варіант RAG

Основна частина логіки була реалізована у вигляді node.js / Typescript-застосунка, що використовує express.js для API і базу даних Postgresql для збереження листувань з користувачами (а в перспективі й інших даних, наприклад, сustomer-specific-конфігурацій / промптів, аналітики тощо). Застосунок також писався з думкою про підтримку кількох клієнтів (кожного зі своїми OpenAI ключами).

Допитливому й більш технічно підкованому читачеві пропоную склонувати репозиторій і вивчити / запустити його локально. Тут же дозволю собі описати основну логіку, яка була реалізована для того, щоб впоратися з очікуваними діалогами:

  • Для кожного вхідного запиту генеруємо вектор і шукаємо збіги у Pinecone
  • Якщо знайдено схожі контексти категорії raise_to_support, бот ініціює переключення на агента (у разі цього РОС — відповідає фразою Looks like this is a task for a human. Let me please pass you to a live agent)
  • Якщо схожих контекстів знайти не вдалось, використовуємо system prompt наступного виду (приклад в OpenAPI Playground), завдяки якому LLM має відповісти на small-talk / попросити відвідувача сформулювати своє запитання, або також переключити на агента:


You are an experienced and professional customer service agent of ExampleShop, tasked with kindly responding to customer inquiries.

Give the answer in markdown format. Always answer in English. Keep your answers concise.

Determine if user’s message belongs to the small talk categories specified below and delimited by +++++.

If user message does NOT belong to these defined small talk categories, respond «I haven’t found relevant info in my knowledge base. Let me please pass you to a live agent». Otherwise politely respond and ask the user about the subject of their question.

+++++Small Talk categories:

Greeting: User message simply contains greetings, for example hi, hello, good morning

Farewell: User is saying goodbye, e.g. bye, see you

Thanks: User simply expresses gratitude, e.g. thanks, tnx

Profanity: User message contains swear words, obscene language, e.g. what the fuck

Affirmative: User simply responds with a confirmation, such as yes, sure

Negative: User responds wit a denial, e.g. no+++++

Ось кілька прикладів подібних діалогів, змодельованих у OpenAPI Playground:


  • Якщо ж контексти типу knowledge_base вдалось знайти, використовуємо інший системний промпт для відповіді на запитання відвідувача, виходячи зі знайденого контексту:


You are an experienced and professional customer service agent of ExampleShop, tasked with kindly responding to customer inquiries.

Give the answer in markdown format. Always answer in English. Keep your answers concise.

You only know the information provided in the VECTOR_CONTEXT. Do not make up any info which is not present in VECTOR_CONTEXT.

If VECTOR_CONTEXT doesn’t provide enough details to answer user’s question, ask the user to provide more details or rephrase the question.

Generate each response based on the following VECTOR_CONTEXT: <context>

If VECTOR_CONTEXT references any resources (addresses, links, phone numbers, lists), include them in the ANSWER.

Приклад у OpenAI Playground:

Складнощі та спостереження

Обробка запитань від відвідувачів часто ставить дещо суперечливі вимоги:

  • Бот повинен відповідати тільки на запитання, для яких він має відповідь (контекст), у решті випадків не галюцинувати і переключати на живого агента. Гірше за чатбота, що не може відповісти, мабуть тільки чатбот, який повертає недостовірні дані.
  • Водночас модель повинна підтримувати small-talk (open-domain conversation).
  • Бот повинен ескалювати до людини, якщо відвідувач просить про це буквально.

Щоб «подружити» суперечливі вимоги (відповідати на основі контексту та водночас відповідати на small-talks [без контексту]), можливі принаймні два підходи:

  1. One prompt to rule them all — оптимізувати основний промпт, додавши до нього правила для всіх можливих кейсів, і також додавати в цей єдиний промпт контексти всіх категорій (knowledge_base, raise_to_support), знайдені у векторній БД. Плюс: всі рішення ухвалюються LLM, менше коду. Мінус: промпт стає вельми складним, шанси виконання моделлю всіх численних правил падають. Або потрібно використовувати більш «розумну» і дорогу модель (4 замість 3.5-turbo);
  2. «Гібридний підхід» — використати кілька більш спеціалізованих промптів (наприклад, один для small-talks, коли контекстів не знайдено, інший — для happy flow, коли контексти є) і перемикатися між ними за результатами пошуку у векторній БД. Тобто частина рішень ухвалюються LLM, частина «зашиті» в коді.

Як уже зазначалось вище, у РОС я пішов другим шляхом, який за результатами моїх тестів видався мені більш контрольованим. Хоча припускаю, що перший також може бути успішним (особливо в майбутньому, коли моделі ставатимуть все більш «розумними»).

Як можна побачити з цього вельми загального огляду, кастомний RAG-застосунок може потребувати значного «тюнінгу» у плані промптів і того, як вони поєднуються з результатами пошуку у векторній БД.

Фінальний сетап залежить від багатьох факторів — вимог (наприклад, потрібна ескалація до людини чи ні, чи є blacklisted теми тощо), типових діалогів з відвідувачами (наприклад, чи відвідувачі схильні одразу ставити запитання, чи треба збирати запитання із кількох циклів бесіди тощо), ефективності LLM / бюджету (наприклад, 3.5-turbo vs 4) тощо.

Результату, скоріше за все, можна досягти різними способами — як єдиним промптом, який має запрограмувати LLM вибирати між різними флоу / відповідями, так і гібридним підходом, де частина виборів робиться LLM, а частина «зашита» у коді (наприклад, опирається на результати пошуку у векторній БД, категоріях знайдених записів і т. д.).

OpenAI Assistants API

Переходимо до другого варіанту рішення даного завдання. Нещодавно OpenAI запустила серію важливих новинок своєї платформи, зокрема можливість створення GPTs — «кастомізованих» варіантів ChatGPT, оформлених у вигляді «асистентів».

Асистентів можна створювати через UI або через нову OpenAI Assistants API (наразі у статусі «beta»). Одна з фіч таких асистентів — RAG «із коробки» (функція Retrieval).


Автоматизація техпідтримки — рішення на основі OpenAI Assistants API

Цей застосунок простіший за попередній. Основне, що він робить — для кожного нового користувача створює тред (thread), куди додає запит користувача і «проганяє» нашого асистента тредом для генерації відповіді. Власне асистента можна всього за кілька кліків створити через відповідний дешборд або запустивши простий скрипт, який завантажить файл з контекстами і смикне потрібну API для створення асистента.

Тестування і порівняння 2 сетапів

Для оцінки ефективності двох вищеописаних застосунків була створена проста система тестування такого виду:

  • Google Spreadsheet з колонкою запитань й очікуваних відповідей. Запитання належали до таких категорій:
    • Запитання, для яких є контекст, і які а) точно збігаються із збереженими у векторній БД, або б) сформульовані інакше — тут очікувалось, що бот має відповісти на основі контексту.
    • Запитання, для яких немає контексту — тут очікувалось, що бот має утриматись від галюцинацій і ескалювати до агента.
    • Small-talks (привітання, прощання тощо) — тут бот повинен підтримати розмову, пропонувати користувачу поставити своє запитання.
  • Python-скрипт, який зчитував запитання з вищезгаданого Google Spreadsheet, посилав їх локально запущеному застосунку та зберігав відповіді в новій колонці того ж документу.
  • Оцінка якості відповідей проводилась вручну. Всього тестовий набір містив близько 60 запитань.

Результати порівняння

За результатами порівняння, застосунок на основі «кастомного» RAG мав дещо вищий відсоток правильних відповідей, порівняно з RAG на основі OpenAI Assistants API. Так, процент правильних відповідей склав 75% і ~52% відповідно. Слід зазначити, що тест вельми невеликий, тож оцінки треба трактувати, як вельми приблизні.

Рішення на основі OpenAI Assistants API було більш схильне до галюцинацій або «не знаходило» відповідей у підключеному документі. Відмічу, що наразі Assistants API дозволяє лише вибрати LLM-модель і не дає керувати параметрами запитів до LLM, такими, наприклад, як temperature. Ризикну припустити, що дефолтне значення temperature для асистентів може бути вищим за нуль (як це було встановлено мною для «кастомного» рішення). Уся інша логіка навколо similarity search також абстрагована від користувача.

Іншим фактором не на користь OpenAI Assistants API є потенційно більш висока ціна. Як зазначено у документації, Retrieval currently optimizes for quality by adding all relevant content to the context of model calls, тож якщо ваші документи відносно невеликі, вони можуть додаватися повністю у кожен запит. Якщо ж використовувати найбільш «розумні», а відтак дорогі моделі типу GTP-4 Turbo із 128-k контекстним вікном, ціни починають відверто кусатись (один запит може досягати $0,1 і більше).

Після анонсу GPTs могло здатись, що OpenAI, відкривши можливість користувачам створювати асистентів із RAG з коробки, одномоментно виштовхує з ринку всі продукти на основі кастомних RAG, виробників векторних баз даних і т. д. Але більш детальний аналіз показує, що це не обов’язково так. Зворотною стороною простоти налаштування RAG у випадку OpenAI Assistants є потенційно вища вартість і важкість чи неможливість додавання кастомної логіки між пошуком у векторній БД і генерацією відповіді. Водночас OpenAI Assistants API лише у «беті» і буде розвиватись. Тож час покаже.

Бонусний розділ: як це можна продати

Я провів невелике дослідження для оцінки ринку та можливостей створення комерційного продукту на основі застосунку, з яким можна ознайомитись у документі.

Сфера застосування рішень типу chat with your data широка, і в багатьох нішах уже є вельми успішні продукти. Якщо розглядати конкретно ідею автоматизації техпідтримки, то прикладами є Chatbase/Botsonic/Fini тощо. Тим не менш, ринок великий, росте, охоплення AI-автоматизацією все ще часткове, що залишає місце для нових гравців. Одним з варіантів є створення кастомних чатботів для конкретних клієнтів, тюнінгованих під їхні потреби.

Інше спостереження — хоча чатботів на основі RAG багато, я не знайшов рішень, де така система пропонується у форматі Operator Assistant, тобто коли з відвідувачем спілкується людина, яка, у свою чергу, користується підказками від RAG-бота. Конкретний приклад, де такий RAG Operator Assistant може бути реалізований — платформа / маркетплейс віджетів LiveChat (принаймні станом на кінець літа 2023 я не бачив там конкретно таких продуктів).

Згідно з деякою статистикою, LiveChat займає біля 5,5% глобального ринку платформ Customer Service Management, з приблизно 80 тисяч компаній-клієнтів.

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

Сподіваюсь, що матеріал був для вас корисним. Які рішення на основі RAG будували ви? Як ваш досвід співвідноситься з тезами цієї статті?

P.s. Якщо ви віддаєте перевагу відеоформату, матеріал на тему можна також переглянути на Youtube.

👍ПодобаєтьсяСподобалось11
До обраногоВ обраному5
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Інше спостереження — хоча чатботів на основі RAG багато, я не знайшов рішень, де така система пропонується у форматі Operator Assistant, тобто коли з відвідувачем спілкується людина, яка, у свою чергу, користується підказками від RAG-бота.

Щось схоже ми робимо в Chatty, але все дуже залежить від запитів конкретного клієнта.

Крутий проект 😎 👍

Так, а ще, по суті, формат «Operator Assistant» можна назвати більш популярним зараз словом «Copilot» ;) А якщо вірити Microsoft, копайлоти скоро будуть скрізь

Так, все вірно, копілоти будуть кругом :)

Дуже цікаіво, але я геть не розумію як це працює... Де можна навчитись роботі з AI ? Чи є в Україні курси на цю тему?

AI дуже широка тема, і шляхів туди безліч. Якщо мається на увазі популярна зараз Generative AI і без зайвих «прелюдій», одразу ближче до практики, то лише як кілька варіантів, міг би порадити наступне:
— Проглянути ресурс типу learnprompting.org/docs/intro , щоб зрозуміти як взаємодіяти з LLM, або
— Пройти один із курсів від Google/Microsoft etc (чисто як варіант, https://www.cloudskillsboost.google/course_templates/536), на Udemy, Coursera etc, крупних західних університетів, або
— Подивитись Youtube-канали AI-ентузіастів (кілька із моїх підписок — www.youtube.com/@AIJasonZ , www.youtube.com/@engineerprompt , www.youtube.com/@jamesbriggs ).
— Нарешті, можна попросити ChatGTP скласти вам curriculum згідно ваших інтересів, наявного часу, рівня знань і т.д.

Дякую за статтю!
Якраз готую свою про LlamaIndex)

Дуже цікавий матеріал
Як на Вашу думку, чи реально створити бота за допомогою GPT для е-коммерса, який би був підкючений до БД з асортиментом і усіма даними по ньому? Щоб, наприклад, на питання про будь-який товар, зміг би зрозуміти що мова йде саме про конктрентний товар та володів би усією інформацією про нього. Або, наприклад, зміг би прийняти заказ та оформити доставку через усілякі API
Та взагалі чи доцільно таке робити, з точки зору вартості та складності?

Так, вищеописана архітектура видається потенційно підходящою для такої задачі. Слід було б згенерувати і зберегти вектори по назві-характеристиках-опису кожного товару, і потім намагатись зматчити запит користувача із даними векторами через similarity search. І якщо отримали достатньо високе співпадіння, то надсилати completion-моделі спец. промпт із інструкціями відповісти на основі інформації по товару. Можна було б піти далі, і зробити так, щоб бот міг проконсультувати на основі інструкції по використанню товару, або резюмувати відгуки тощо.

Якщо припустити, що магазин використовує БД типу Postgresql, то є розширення pgvector для реалізації semantic search, і можна було б обійтись без Pinecone, використавши існуючу БД.

Та взагалі чи доцільно таке робити, з точки зору вартості та складності?

На перший погляд, це не виглядає надто складно чи дорого, хоча діявол ховається у деталях, треба б потестити на якихось реальних даних. В принципі, цю ідею можна протестувати на 2-му із вищеописаних застосунків (котрий на основі Assistants API, щоб простіше) — зберегти описи десятка товарів у окремі текстові документи, і потім виконати (нескладний) сетап згідно інструкції.

До речі, ще ідея проекту на основі RAG і інтеграції із БД з асортиментом якогось магазину: можна було б зробити бота для групових чатів, який би «слухав» чат, і періодично намагався зматчити тему переписки із описами товарів, і у випадку співпадінь постив рекламу релевантних товарів. Такий собі conversational adsense.

Можливо. Використовуючи LlamaIndex, TextToSql. Базові доки для дослідження:
docs.llamaindex.ai/...​indices/SQLIndexDemo.html
docs.llamaindex.ai/...​ther/structured_data.html

Також в блозі своєму вони писали на цю тему.

цікаво. чому ви не згадали той же Langchain? не подобається рішення чи інші мотиви

Коротко: для більшого контролю за підготовкою векторів і також через додаткову кастомну логіку за результатами пошуку у векторній БД було логічніше використати самописне рішення, ніж Langchain.

Дещо детальніше: у контексті «кастомного» RAG найбільшою складністю було мінімізувати галюцинації і при цьому щоб бот покривав усі необхідні флоу (відповідав на релевантні запитання, хендлив смолтоки, не намагався відповідати на сторонні запитання).

Тому контексти парсились вручну (щоб бути впевненим у тому, що вони правильно поділені на документи і збережені у векторну БД). Також довелось додати кастомну логіку між пошуком у векторні БД і запитами до LLM — якби використовувася Langchain, довелось би розбиратись як це зробити. Тож написати власне рішення (в принципі, досить просте) було швидше, ніж розбиратись як кастомізувати стороннє.

Підписатись на коментарі