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

Источник: https://www.youtube.com/watch?v=HSZ_uaif57o
Канал: freeCodeCamp.org
Опубликовано: 20.11.2023

---

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

## 🌐 Экосистема LangChain.js и новая эра веб-разработки
[[JUMP:04:03]]

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

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

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

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

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

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

* Слова со схожим значением (например, «cat» и «feline») превращаются в близкие по значениям координаты и располагаются рядом.
* Слово «kitten» окажется чуть дальше из-за возрастного нюанса, а «dog» — еще дальше, хотя и останется в семантической зоне домашних животных.
* Слово «building», никак не связанное с животными, получит координаты в совершенно другой области графика.

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

$$King - Man + Woman = Queen$$

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

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

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

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

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

* `chunkSize`: Том уменьшает дефолтное значение с 1000 до 500 символов, чтобы добиться большей гранулярности семантической информации.
* `chunkOverlap`: размер перекрытия между соседними чанками устанавливается на уровне 50 символов (10% от размера чанка), что исключает потерю контекста на границах.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* `standaloneQuestionChain`: принимает сырой запрос пользователя, прогоняет через промпт и ChatOpenAI, выдавая чистую строку вопроса с помощью `StringOutputParser`.
* `retrieverChain`: берет изолированный вопрос, обращается к векторному хранилищу Supabase и с помощью функции `combineDocuments` формирует текстовый контекст.
* `answerChain`: соединяет полученный контекст с оригинальным вопросом пользователя и отправляет итоговый промпт в модель для генерации финального ответа.

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

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

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

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

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

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

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

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

* **Изменение параметров сплиттера**: увеличение `chunkSize` дает ИИ больше контекста, а уменьшение — повышает гранулярность и точность совпадений.
* **Регулирование количества извлекаемых чанков**: метод `asRetriever()` принимает конфигурационный параметр количества документов. Увеличение этого числа (например, с 4 по умолчанию до 10) расширяет кругозор модели, но перенасыщение лишней информацией может сделать ответы размытыми.
* **Промпт-инжиниринг**: детализация инструкций, добавление примеров правильного поведения (few-shot prompting) помогают существенно скорректировать тон и логику ответов.
* **Параметр Temperature**: при работе с корпоративными базами знаний Том настоятельно рекомендует выставлять этот параметр строго на `0`. Как отмечает инструктор, это лишает ИИ «смелости» и креативности, сводя к минимуму риск галлюцинаций.
* **Смена модели**: фреймворк позволяет в одну строку переключить `ChatOpenAI` с базовой GPT-3.5 на более мощную GPT-4 или любые новые модели, выходящие на рынке.