Як створити спеціалізований чат-бот. Детальний гайд на основі кейса клієнта
Мене звати Ігор Козлов, я Big Data & ML Engineer в Levi9. Останнім часом значно зросло використання різних Великих Мовних Моделей (LLM, Large Language Model) для повсякденного розв’язання задач у різних сферах. В ІТ-сфері їх часто застосовують як асистентів-помічників під час написання коду (наприклад, GPT модель від OpenAI).
Більшість таких моделей натреновані на великому обсязі даних з різних галузей, але що робити, коли ми хочемо додати специфічні, вузькоспрямовані знання? Коли потрібно розробити чатбот, який буде відповідати на питання про продукцію компанії чи надавати документацію проєкту? У цьому гайді я відповім на ці питання, розповім як створити спеціалізований чатбот та поділюсь тим, на що варто звернути увагу для покращення результату.
Почати варто з самого визначення Large Language Model (LLM), мовної моделі, що спеціалізується на розумінні та генерації тексту й використовує архітектуру трансформерів (transformers). Наразі існує багато різних моделей LLM, найпопулярнішими серед яких є GPT-3.5, GPT-4, LLama, Cohere та BERT. Для нашого проєкту ми будемо користуватись GPT-моделями від OpenAI.
Обираємо хмарного провайдера
Щоб користуватись GPT-моделями, ми маємо обрати один з двох варіантів:
- Використовувати безпосередньо від OpenAI.
- Використовувати Azure Open AI.
Вони відрізняються переважно безпекою та доступністю. У таблиці нижче для зручності наводжу основні (не всі) відмінності, на які потрібно звернути увагу:
Характеристика |
Azure OpenAI |
OpenAI |
---|---|---|
Безпека |
Має стандартну безпеку екосистеми Azure, є можливість використовувати віртуальні мережі (VNETS) та приватні ендпойнти (private endpoints) |
Не зазначено |
Дані для тонкого налаштування (fine tuning) |
Використовується тільки для поточного користувача сервісу |
Дані можуть бути використані безпосередньо для покращення GPT моделей. Є опція відмовитись від цього надіславши запит в службу підтримки |
Prompts та completions |
Зберігаються 30 днів в зашифрованому вигляді, доступні тільки для авторизованих користувачів |
Зберігаються для тренування, дотренування та покращення моделей |
Регіональна доступність |
Доступно в незначній кількості регіонів (повний перелік тут) |
Доступно в багатьох країнах та регіонах (повний перелік тут) |
Отже, якщо для вашого проєкту важлива безпека даних, і ви не хочете, щоб вони використовувалися кимось, крім вас, а також у вашому регіоні доступний Azure OpenAI — краще використовувати його. Це також стосується і випадку, якщо ви вже використовуєте Azure як хмарного провайдера. Також Azure OpenAI інтегрований з Azure Cognitive Services. Детальніше про безпеку, яку надає сервіс, можна прочитати тут.
Наш приклад не має чутливих даних та специфічних безпекових вимог, тому ми будемо використовувати OpenAI. Усе, що потрібно — залогінитися та отримати ключ API.
Як передати LLM специфічні знання
LLM мають суттєве обмеження — на вхід вони можуть отримувати лімітовану кількість інформації. Цей обʼєм даних вимірюється у токенах, які формують контекстне вікно. Розмір контекстного вікна, своєю чергою, не дозволяє надати моделі великий текст, на основі якого вона має генерувати відповіді. Тому аби впровадити нову інформацію для моделі, існують такі способи, як тонке налаштування (fine tuning) та Retrieval Augmented Generation (RAG, пошук та генерація).
Тонке налаштування більше підходить для навчання моделі специфічних завдань (наприклад, асистент з побудови раціону здорового харчування) або стилів (написання технічних статей тощо). Коли необхідно відтворити факти або дати відповідь на запитання, модель може використати знання, які отримала під час початкового навчання. Це пов’язано з тим, що під час тонкого налаштуванні використовуються ваги моделі, які більше нагадують довгострокову пам’ять (long-term memory).
З іншого боку, те, що ми передаємо на вхід моделі через контекстне вікно, більше схоже на короткострокову пам’ять. Ці дані можна порівняти з нотатками, які модель може використати, щоб надати відповідь.
Саме на контекстному вікні й побудований RAG. Ідея полягає в тому, що ви створили для себе базу знань і шукаєте в ній інформацію, найближчу до запитання, яке було задано. Найчастіше для цього використовуються векторні бази даних. Ваші дані (текст) зберігається у вигляді вектора в БД.
Запитання, яке було поставлене, також трансформується у вектор, після чого в БД відбувається пошук вектора, який буде найближчим або подібним до вектора-запитання. Наступним кроком знайдена інформація та саме питання передається мовній моделі, разом з інструкцією відповіді на нього, що базується на наданій інформації. Візуально це можна відобразити так:
Саме RAG ми використовуємо для реалізації нашого чатбота, побудованого на вузькоспеціалізованих знаннях.
Як приєднати різні джерела даних до LLM
Першим кроком є безпосередньо побудова бази даних (індексу) з інформації, на основі якої ми хочемо отримувати відповіді. Найголовніше в цьому — наявність надійних, високоякісних даних. Ви не зможете побудувати надійного чатбота, якщо дані, які ви надали на цьому етапі, суперечливі або не містять інформації, необхідної для відповіді на поставлене питання. Тому варто ще раз наголосити, що якісні дані — основа успіху будь-якого проєкту, пов’язаного з машинним навчанням.
Звісно, ми хочемо бути гнучкими в джерелах отримання наших даних. Для чатбота також важливо міксувати інформацію з різних джерел в одній базі даних, щоб розширити список питань, на які він може дати відповідь. Саме тому для побудови нашої RAG-системи я обрав LLamaIndex — фреймворк, який дозволяє приєднати різні джерела даних до великих мовних моделей.
Основні (але не всі) джерела даних, які можуть бути використані разом з LLamaIndex, наведені на зображенні:
Розробники створили спеціальний портал, де зібрані основні джерела даних, конектори й навіть готові застосунки з використанням RAG. Використати їх або додати свій можна за посиланням.
Джерелом даних був вебсайт клієнта. Для прикладу та збереження конфіденційності в статті я буду використовувати вебсайт з документацією LlamaIndex, який має чимало цікавої інформації.
Загалом LLamaIndex виконує всі основні функції, які нам потрібні:
- завантаження даних;
- створення бази даних;
- пошук по базі даних;
- передача питання, контексту та інструкції до мовної моделі;
- повернення відповіді.
Чому сподобався саме LLamaIndex? По-перше, це швидкість розвитку проєкту та спільнота. Багато хто покращує його самостійно, і це входить в наступні релізи. По-друге, підтримка в Discord-спільноті. Ваше питання точно не залишиться без відповіді. І, по-третє, екосистема навколо.
Я вже згадував про LLamaHub. Є ще LLamaІndex RAGs, який дозволяє побудувати чатбот з вашими даними, використовуючи вже наявний чатбот. Це саме той асистент на базі великої мовної моделі, про який я згадував на початку статті.
Для візуалізації чатбота ми використаємо streamlit.io. Цей сервіс дозволяє швидко і з мінімальними зусиллями візуалізувати роботу моделі. У цьому випадку він надає простий UI чатботу. На мою думку, для успіху реалізації проєкту в машинному навчанні важливо якнайшвидше отримати зворотний зв’язок від замовника.
Streamlit дозволяє нам швидко побудувати інтерфейс, провзаємодіявши з яким, замовник може надати оцінку реалізації проєкту до безпосередньої інтеграції моделі в систему. Ну що ж, почнімо!
Створюємо перший прототип
Отже, клонуємо проєкт.
Переходимо в корінь проєкту, створюємо файл .streamlit/secrets.toml
, додаємо до нього ваш API ключ та встановлюємо всі необхідні бібліотеки (рекомендую зробити це в окремому віртуальному середовищі): pip install -r requirements.txt
. Запускаємо проєкт streamlit run 1_🏠_Home.py
та переходимо за вказаною адресою.
Далі бот запитує необхідну інформацію (звідки брати дані, яка функція, чи хочемо ми змінити якісь параметри):
Вказуємо необхідний вебсайт (ви також можете обрати PDF-документ, csv-файл та інше) та надаємо інструкцію. У першому прототипі залишаємо параметри за замовчуванням. Зліва на панелі в нас з’являється лінк, за допомогою якого ми можемо перейти до чатбота. Обираємо Generated RAG agent та потрібного агента.
Ставимо декілька питань:
Як бачимо, ми використовуємо vector_tool
. Це функція, яка власне робить пошук нашою базою даних.
Також ви могли звернути увагу на вкладку Sources:
Вона показує, який саме текст з бази даних був використаний для відповіді на питання. Це дуже важливо для оцінки якості відповіді та валідації коректності даних. На цьому етапі можна сміливо тиснути Deploy в правому верхньому кутку та надсилати лінк замовнику, щоб він оцінив відповіді й сам поспілкувався з ботом. Ранній зворотний зв’язок дуже важливий і допомагає покращити продукт.
Тепер оцінимо якість наших відповідей.
Програмні методи оцінки
Найголовніший відгук, звісно, від замовника. Але нам потрібно й самостійно оцінювати застосунок, навіть якщо ми не розбираємось в доменній області.
Екосистема LLamaIndex має для цього Rag Evaluator.
Перше, що треба зробити, це підготувати дані для тесту. Реалізувати це можна власноруч, надавши такі складові, як «Контекст», «Питання» та «Відповідь». Одним з прикладів може бути «Це питання просто для тесту», «Питання тестове?», «Так». Але, погодьтесь, такий датасет дещо складно створювати. Тому ще раз звернемося до штучного інтелекту, щоб він підготував нам все необхідне.
Наступні кроки:
- Обираємо n сторінок з сайту. Для реального проєкту краще взяти найбільш релевантні та складні сторінки або документи.
- Завантажуємо індекс, який ми побудували раніше (він доступний в корні проєкту
rags/cache/agents/{Agent_ID}/storage
). - За допомогою GPT генеруємо питання та контрольні відповіді.
- Порівнюємо ці відповіді з тими, які надає наш індекс.
Основні критерії оцінки:
- Достовірність (Correctness) — наскільки згенерована відповідь відповідає контрольній відповіді стосовно питання.
- Семантична схожість (Semantic Similarity) — чи семантично схожа згенерована відповідь з контрольною відповіддю.
- Точність (Faithfulness) — чи відповідає відповідь наданому контексту (чи є галюцинації).
- Релевантність контексту (Context Relevance) — чи релевантні отриманий контекст і відповідь на питання.
- Дотримання вказівок (Guideline Adherence) — чи відповідає передбачена відповідь конкретним вказівкам.
Код для оцінки якості:
from llama_index.llama_dataset.generator import RagDatasetGenerator from llama_index.llms import OpenAI from llama_index import ServiceContext, StorageContext, load_index_from_storage from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter from llama_index.readers import TrafilaturaWebReader from llama_index.llama_pack import download_llama_pack parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter) parser.add_argument("-k", "--apikey", help="OpenAI API key") args = vars(parser.parse_args()) os.environ["OPENAI_API_KEY"] = args["apikey"] # set context for llm provider gpt_4_context = ServiceContext.from_defaults( llm=OpenAI(model="gpt-3.5-turbo", temperature=0.3, chunk_size=512) ) urls = [ 'https://docs.llamaindex.ai/en/stable/use_cases/chatbots.html', 'https://docs.llamaindex.ai/en/stable/use_cases/agents.html', ] reader = TrafilaturaWebReader() documents = reader.load_data(urls) # instantiate a DatasetGenerator dataset_generator = RagDatasetGenerator.from_documents( documents, service_context=gpt_4_context, num_questions_per_chunk=2, # set the number of questions per nodes show_progress=True, ) # generate dataset rag_dataset = dataset_generator.generate_dataset_from_nodes() RagEvaluatorPack = download_llama_pack( "RagEvaluatorPack", "./rag_evaluator_pack" ) rag_evaluator_pack = RagEvaluatorPack( query_engine=index.as_query_engine(), rag_dataset=rag_dataset ) # PERFORM EVALUATION benchmark_df = rag_evaluator_pack.run() print(benchmark_df)
Приклад результату роботи скрипта:
Загалом надані відповіді були непогані, але можливо ми зможемо їх покращити. Саме тут грають роль ті параметри, які ми залишили за замовчуванням на початку конфігурації. Розглянемо їх більш детально.
Розширене налаштування
Embedding model
Одним з кроків, які ми робимо для побудови чатбота, є створення бази даних. Для швидкого пошуку використовуються векторні представлення (vector embeddings), які зберігаються в БД. Аби вони були якісні, нам потрібно обрати кращу модель для їх побудови та визначити вимоги до неї (однією з вимог може бути підтримка багатомовності, якщо ми хочемо спілкуватися з нашим ботом кількома мовами).
У такому разі слова різними мовами розташовуються в одному векторному просторі, що дозволяє нам швидко знайти схожість, навіть якщо питання поставити не тією мовою, у якій зберігаються дані.
Під час вибору моделі нам допоможе Massive Text Embedding Benchmark (MTEB). Ідея полягає в тому, що різні моделі порівнюються на одній і тій самі задачі та даних. Ми можемо перейти за посиланням та подивитися ці результати.
Нас цікавить саме задача отримання (Retrieval). За замовчуванням використовується text-similarity-ada-001
та text-embedding-ada-002
. Як бачимо, вони розташовуються достатньо низько у списку. Тому варто спробувати інші моделі також. Я рекомендую звернути увагу на Cohere/Cohere-embed-multilingual-v3.0 та intfloat/multilingual-e5-large, які ми тестували для замовника і вони показали кращі результати за умов використання декількох мов для спілкування з ботом.
Chunk size
Вибрати оптимальний розмір чанку важливо з декількох причин:
- Релевантність та точність. Малий розмір чанку надає нам більшу точність, але ми можемо втратити частину важливої інформації, яка перебуває в сусідніх чанках (пізніше я розповім, як можна обійти це). Водночас великий розмір чанку може містити багато зайвої інформації, яка також буде використана для надання відповіді.
- Швидкість відповіді. Що більше розмір чанку, то більше інформації ми передаємо LLM, що може підвищити час очікування відповіді.
Щоб обрати оптимальний розмір, варто використати метрики релевантності контексту (Context Relevancy) та точності (Faithfulness), про які ми згадували раніше. А також порівняти результати для різних розмірів саме на ваших даних, адже не існує універсального. Загалом для отримання метрик ми можемо використати код, який наведено вище, підставляючи необхідний розмір чанку. Приклад результату:
Top K
Це характеристика, яка визначає, скільки документів, близьких до нашого питання, ми будемо брати до уваги й передавати в LLM для генерування відповіді. Вибір цього значення також пов’язаний з розміром чанку, адже у разі невеликого розміру потрібна інформація може бути в пʼяти найближчих чанках, тоді як ми за замовчуванням обираємо, до прикладу, два.
Додаткова обробка результатів
Додаткова обробка потрібна для того, щоб уникнути недоліків, які можуть виникнути під час пошуку потрібного документу. Загалом ми беремо результат пошуку нашою базою даних (саме документи, які можуть бути використані для відповіді на поставлене питання) та додатково їх обробляємо.
Зараз це неможливо зробити через LLamaIndex RAGs, який ми використовували для генерації індексу та побудови інтерфейсу. Та ми можемо завантажити вже згенерований нами індекс та додати до нього необхідні кроки.
Переранжування (re-ranking)
Деякі нерелевантні документи можуть отримати високі оцінки, тоді як деякі релевантні, навпаки, можуть отримати нижчі бали. Так, не всі документи top-k є релевантними, і не всі відповідні документи є в top-k. Переранжування уточнює ці результати та відображає найбільш відповідні запиту.
Зазвичай для переранжування використовуються мовні моделі. Їхній рейтинг ми можемо знайти в MTEB на вкладці Reranking.
Особисто моя рекомендація — використати Cohere rarank:
from llama_index.postprocessor.cohere_rerank import CohereRerank cohere_rerank = CohereRerank(api_key=api_key, top_n=2) query_engine = index.as_query_engine( similarity_top_k=10, node_postprocessors=[cohere_rerank], ) response = query_engine.query( "What is node postprocessor?", )
Отримання додаткового контексту
Як згадував вище, за маленького розміру чанку потрібна інформація може міститися «навколо» знайденого документа. За допомогою PrevNextNodePostprocessor
ми також можемо отримати документи, які перебувають «до і після» знайденого документа.
Приклад використання:
from llama_index.postprocessor import PrevNextNodePostprocessor postprocessor = PrevNextNodePostprocessor( docstore=index.docstore, num_nodes=1, # number of nodes to fetch when looking forawrds or backwards mode="next", # can be either 'next', 'previous', or 'both' ) postprocessor.postprocess_nodes(nodes)
Сортування за датою
Хочу зазначити, що якщо вам необхідно отримувати найновіші документи, то це можливо зробити за допомогою FixedRecencyPostprocessor
. Для цього потрібно додати до метаданих документа поле «дата» під час побудови індексу й потім використати додаткову обробку. Приклад:
from llama_index.extractors import ( BaseExtractor, ) from datetime import date from llama_index.postprocessor import FixedRecencyPostprocessor from llama_index.ingestion import IngestionPipeline from llama_index.readers import TrafilaturaWebReader class CustomExtractor(BaseExtractor): def extract(self, nodes): metadata_list = [ { "date": date.today() } for node in nodes ] return metadata_list extractors = [CustomExtractor()] urls = [ 'https://docs.llamaindex.ai/en/stable/use_cases/chatbots.html', 'https://docs.llamaindex.ai/en/stable/use_cases/agents.html', ] reader = TrafilaturaWebReader() documents = reader.load_data(urls) pipeline = IngestionPipeline(transformations=extractors) nodes = pipeline.run(documents=documents) postprocessor = FixedRecencyPostprocessor( tok_k=10, date_key="date" # the key in the metadata to find the date ) postprocessor.postprocess_nodes(nodes)
Оскільки сторінки документації не мають дати, ми просто ставимо дату додавання сторінки в індекс. Ви можете реалізувати свою логіку, підставляючи необхідну дату з потрібного джерела.
OpenAI GPTs
Нещодавно OpenAI опублікувало GPTs та Assistants API. Загалом вони дозволяють побудувати аналог ChatGPT на ваших даних.
Хлопці з LlamaIndex зробили порівняння їхні рішення та Assistants API від OpenAI. Основні висновки:
- для індексу з багатьох документів краще працює LLamaIndex;
- Assistants API набагато швидше за LLamaIndex, коли вся інформація об’єднана в один документ;
- обробка пʼяти документів в LLamaIndex зайняла пʼять хвилин, коли в Assistants API така сама обробка зайняла одну годину.
Знаючи ці основні моменти, ви також можете орієнтуватись, який метод побудови чатбота обрати.
Підсумки
Цей гайд має на меті допомогти вам побудувати базовий чатбот навколо ваших даних з мінімальною кількістю коду або взагалі без його використання (за допомогою іншого чатботу, як не дивно). Цього цілком достатньо для побудови прототипу та верифікації вашої ідеї клієнтом.
Після отримання відповіді від замовника з основними проблемами, ви можете покращити роботу боту за допомогою таких методів, як-от вибір моделі, розміру чанку або додавання ще одної обробки після отримання результатів. Користуйтесь!
10 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів