Как разработать умного чат-бота с контекстной памятью на LangChain.js

freeCodeCamp.org 132 тыс. 1 ч 39 мин 8 мин 20.11.2023
Главное

С бурным развитием больших языковых моделей перед веб-разработчиками встал серьезный вызов: как эффективно интегрировать ИИ в привычные интерфейсы и заставить его работать с собственными коммерческими или корпоративными данными. Образовательная платформа freeCodeCamp.org выпустила детальное руководство по фреймворку LangChain.js, призванное упростить этот процесс для JavaScript-сообщества. Под руководством опытного инженера Тома Чанта и при участии сооснователя проекта Джейкоба Ли, курс предлагает практическое руководство по созданию «умного» чат-бота с контекстной памятью, способного отвечать на сложные вопросы по целевым документам.

🌐 Экосистема LangChain.js и новая эра веб-разработки 4:03

До недавнего времени основная часть разработок в сфере искусственного интеллекта была сосредоточена в экосистеме Python. Однако, как отмечает ведущий инженер-программист LangChain Джейкоб Ли, фреймворк LangChain.js был создан специально для того, чтобы сделать технологии работы с ИИ доступными для широкой аудитории веб-разработчиков. Главная задача этого инструмента — помочь в создании приложений, которые способны учитывать контекст и выполнять логические рассуждения (context-aware reasoning applications). Основной акцент в веб-разработке сейчас смещается в сторону систем контекстного поиска по документам (conversational retrieval), расширяющих рамки базовых знаний ИИ.

По словам Тома Чанта, фреймворк LangChain долгое время пользовался репутацией платформы с довольно высоким порогом входа. Ситуация кардинально изменилась с внедрением выразительного языка LangChain Expression Language (LCEL), который значительно упростил проектирование цепочек вызовов. Архитектура фреймворка построена на принципах абсолютной модульности: базовые компоненты, такие как модели, промпты и парсеры, легко заменяются. Разработчик может свободно переключаться между различными базами данных, векторными хранилищами и языковыми моделями, подбирая оптимальную конфигурацию под конкретные нужды своего проекта.

📊 Архитектура приложения и магия эмбеддингов 5:40

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

Чтобы понять, почему ИИ способен находить нужную информацию, необходимо разобраться в природе эмбеддингов. Коллега Тома, Гил, объясняет, что продвинутые ИИ-системы не понимают реальный мир, текст или видео так, как это делают люди. Эмбеддинг — это математическая концепция перевода объекта из пространства контента в векторное пространство в виде набора чисел, сохраняющая его семантическое значение и связи с другими словами.

Принцип работы векторного пространства можно наглядно описать на примере двумерного графика:

Эмбеддинги позволяют производить над словами математические операции. Известный пример векторной арифметики:

$$King - Man + Woman = Queen$$

Если из вектора слова «King» вычесть контекст мужчины и добавить контекст женщины, результирующий вектор окажется максимально близок к координатам слова «Queen». В реальных задачах используются многомерные пространства. Например, модель OpenAI оперирует векторами, имеющими 1536 измерений, где каждое число отвечает за отдельный контекстуальный или семантический аспект слова. Именно это позволяет алгоритмам Spotify, YouTube или Netflix успешно формировать персональные рекомендации.

🛠️ Настройка базы данных Supabase и подготовка данных 15:17

Для реализации векторного хранилища в проекте используется платформа Supabase. После создания стандартного проекта в панели управления необходимо активировать расширение PGVector. Сделать это вручную не требуется: LangChain предоставляет готовый SQL-скрипт, который автоматически создает таблицу documents со следующими полями: id, content (текстовый чанк), metadata (информация о расположении фрагмента) и embedding. Скрипт также инициализирует функцию match_documents, которая берет вектор вопроса пользователя и находит ближайшие к нему векторы текстовых чанков.

Перед отправкой текста в базу данных его нужно правильно разделить. Том Чант использует инструмент RecursiveCharacterTextSplitter, который работает по сложным алгоритмам. Он анализирует текст по приоритетной цепочке разделителей: двойной перенос строки (\n\n), одинарный перенос строки (\n), пробел и отсутствие пробела. Это позволяет сохранять целостность абзацев и предложений, не разрывая логические фразы на стыках чанков.

Параметры сплиттера настраиваются вручную для оптимизации расходов и производительности:

После разделения текста на чанки вызывается метод SupabaseVectorStore.fromDocuments(), который генерирует векторы через API OpenAI и загружает их в облачную базу данных.

🧠 Проблема контекста: создание «изолированных» вопросов 32:00

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

Для решения этой проблемы в LangChain.js строится промежуточная цепочка, генерирующая «изолированный вопрос» (standalone question) — лаконичный запрос, очищенный от словесного мусора. Процесс реализуется с помощью класса ChatOpenAI и объекта PromptTemplate. Промпт-шаблон использует синтаксис F-строк, заимствованный из Python, где переменные передаются внутри фигурных скобок. Модели ИИ дается жесткая инструкция: трансформировать запутанную фразу пользователя в четкий, емкий вопрос.

🔗 Конвейеры данных: от метода Pipe до RunnableSequence 39:05

Связывание компонентов в LangChain.js традиционно начинается с применения метода .pipe(). Он организует последовательную передачу данных: берет выходные данные промпта и отправляет их на вход языковой модели. Однако при попытке подключить к этой цепочке поисковый механизм (retriever) разработчики сталкиваются с ограничениями типов данных. Языковая модель возвращает сложный объект ответа, в то время как ретривер требует на вход чистую строку.

Для преодоления этой проблемы Том Чант вводит в цепочку два критически важных элемента:

  1. StringOutputParser: специальный класс, который автоматически извлекает текстовое содержимое из ответа модели, превращая объект в строку.
  2. combineDocuments: кастомная утилита, которая принимает массив найденных объектов из базы данных, извлекает из них свойство pageContent и собирает их в один сплошной текст, разделенный двойным переносом строки.

Когда логика приложения усложняется, стандартного метода .pipe() становится недостаточно. На смену ему приходит класс RunnableSequence.from(), принимающий массив шагов. Он позволяет использовать стрелочные функции прямо внутри конвейера для логирования данных на любом этапе, что незаменимо при отладке комплексных систем.

🔀 Продвинутое управление потоками с RunnablePassThrough 1:02:37

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

Для сохранения и проброса исходных параметров LangChain предлагает инструмент RunnablePassThrough. С его помощью на первом этапе создается объект, где в отдельном поле фиксируются исходные данные (original input). На последующих этапах разработчик может деструктурировать этот объект и извлекать нужные переменные (например, целевой язык), беспрепятственно передавая их в самый конец конвейера.

🧩 Финальная сборка архитектуры и интеграция интерфейса 1:14:40

Финальная архитектура работающего MVP-приложения разделяется на три автономных суб-конвейера, которые затем объединяются в одну глобальную последовательность:

После сборки цепочки Том Чант переносит вызов метода .invoke() внутрь стандартного JavaScript-обработчика событий DOM, привязанного к кнопке отправки формы. Чат-бот начинает успешно отвечать на вопросы, используя информацию из загруженного документа о платформе Scrimba. Тем не менее, в ходе тестирования обнаруживается серьезный недостаток: если сначала представиться боту, а следующим сообщением спросить «Как меня зовут?», ИИ ответит, что не знает этого. Причина очевидна — у системы полностью отсутствует память.

💾 Наделение ИИ памятью: Conversation History 1:24:24

Чтобы бот мог поддерживать полноценный контекстный диалог, создается глобальный массив conversationHistory. При каждом цикле запроса-ответа в этот массив пушатся строки сообщений пользователя и реплики ИИ. Однако обычный массив строк малоэффективен для языковой модели. Том создает вспомогательную функцию formatComHistory, которая размечает историю диалога специальными тегами на основе индекса элемента. Поскольку человек всегда говорит первым, четные индексы получают префикс «Human:», а нечетные — «AI:». На выходе формируется единый размеченный текстовый блок.

Эта история передается в цепочку в качестве переменной conv_history и интегрируется в два места:

  1. В шаблон изолированного вопроса, чтобы модель понимала, к чему относится краткое местоимение (например, слово «он» в цепочке диалога).
  2. В финальный шаблон ответа. При этом Том Чант делает важное замечание по промпт-инжинирингу: ИИ нужно дать специальную инструкцию искать ответ в первую очередь в предоставленном контексте документа, и лишь при его отсутствии обращаться к истории переписки.

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

⚙️ Тонкая настройка производительности и оптимизация ИИ 1:33:50

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

💬 Цитаты

«LangChain — это фреймворк, который помогает разработчикам создавать приложения, способные учитывать контекст и выполнять рассуждения.»

Джейкоб Ли 04:03

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

👥 Спикеры
🔗 Упомянутые сайты и проекты
📖 Термины
Эмбеддинг
Математическое представление семантического смысла текста в виде многомерного вектора чисел.
Ретривер
Компонент LangChain, отвечающий за извлечение релевантных документов из векторного хранилища.
LCEL
LangChain Expression Language — декларативный язык для простого проектирования сложных ИИ-цепочек.
📊 Цифры
⚖️ Другая сторона
Искусственный интеллект LangChain.js OpenAI API Supabase Эмбеддинги LCEL