Как создать умного чат-бота на JavaScript с помощью LangChain.js

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

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

🌐 Эволюция фреймворка: почему веб-разработчикам необходим LangChain.js 0:00

Создание сложных систем искусственного интеллекта долгое время оставалось прерогативой разработчиков на Python. Однако, по словам ведущего инженера-разработчика LangChain Джейкоба Ли, с ростом популярности веб-технологий возникла очевидная необходимость в создании качественного инструментария для JavaScript-сообщества. Именно так появился LangChain.js — адаптация популярного фреймворка, призванная сделать работу с языковыми модели и техники взаимодействия с ними более доступными для широкого круга фронтенд-разработчиков.

Как отмечает Джейкоб Ли, идея создания фреймворка родилась из понимания того, что процесс разработки продвинутых ИИ-приложений можно значительно упростить. Для этого требовалось выделить и стандартизировать общие абстракции, встречающиеся в большинстве проектов. Основной фокус в текущем курсе сделан на концепции conversational retrieval (диалогового поиска по документам). Эта технология позволяет наделить большую языковую модель (LLM) доступом к специфической информации, содержащейся в конкретном документе, что выводит систему за рамки данных, полученных ею при первоначальном обучении.

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

🧠 Анатомия эмбеддингов: как машины начинают понимать смысл слов 9:11

Понимание того, как современные платформы вроде Amazon, Spotify, Netflix или YouTube подбирают идеальные рекомендации, кроется в концепции эмбеддингов. Как утверждает специалист платформы Gil, продвинутые модели ИИ, несмотря на свои впечатляющие возможности, изначально не понимают окружающий мир так, как человек. Они не способны напрямую воспринять истинный смысл названия видеоролика, песни или статьи. Для взаимодействия с реальностью им необходим перевод данных в математический формат.

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

Для простоты понимания разработчики предлагают рассмотреть двухмерный график с осями X и Y:

Особый интерес вызывает векторная арифметика, демонстрирующая семантическую глубину эмбеддингов. Если взять вектор слова «King» (Король), вычесть из него контекст мужчины («man») и добавить контекст женщины («woman»), результирующий вектор окажется максимально близок к координатам слова «Queen» (Королева).

В реальных коммерческих системах двухмерное пространство уступает место многомерным структурам. Векторные эмбеддинги могут состоять из сотен и тысяч измерений. Например, используемая в данном проекте модель от OpenAI работает с 1536 измерениями. Каждое число в таком векторе отвечает за тончайшие смысловые и контекстуальные аспекты слова, что позволяет ИИ распознавать, идет ли речь о королеве как о монархе, персонаже истории о Клеопатре или о шахматной фигуре.

🗄️ Настройка базы данных: интеграция Supabase и расширения PGVector 15:17

В качестве векторного хранилища для проекта Том Чант выбирает облачную платформу Supabase, которая пользуется большой популярностью благодаря своему удобству. Процесс развертывания базы данных включает в себя создание проекта с именем «scrimbot» на серверах в Западной Европе.

Вся техническая подготовка таблиц внутри Supabase автоматизирована со стороны LangChain при помощи SQL-скрипта, выполняемого в редакторе запросов платформы. Данный скрипт выполняет следующие ключевые операции:

  1. Активирует специализированное расширение pgvector в базе данных Supabase.
  2. Создает таблицу documents со столбцами для уникального идентификатора (ID), текстового содержимого (content), метаданных (metadata) и непосредственно эмбеддинга (embedding).
  3. Жестко фиксирует размерность векторов на уровне 1536 измерений, что строго соответствует техническим спецификациям моделей OpenAI.
  4. Регистрирует функцию match_documents, которая отвечает за математический поиск ближайших векторов.

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

✂️ Разрезание текста на части: стратегии токенизации и оверлапа 19:27

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

Загрузка столь массивного документа целиком при каждом запросе к API OpenAI неэффективна, так как это привело бы к колоссальным финансовым затратам на токены. Экономически оправданный и масштабируемый подход требует предварительного разделения текста на небольшие фрагменты (чанки). Фрагмент должен быть достаточно большим, чтобы содержать в себе законченную мысль, но достаточно компактным для экономии вычислительных ресурсов.

Разработчикам доступны два основных инструмента для разделения: стандартный CharacterTextSplitter и более продвинутый RecursiveCharacterTextSplitter. Последний использует сложный алгоритм, работающий с упорядоченным массивом разделителей:

Алгоритм стремится в первую очередь сохранить целостность абзацев, затем предложений и отдельных слов. По умолчанию размер чанка в LangChain составляет 1000 символов, однако Том Чант в ходе экспериментов уменьшает этот параметр до 500 символов для достижения большей гранулярности семантической информации.

Важным элементом настройки является перекрытие фрагментов (chunk_overlap), значение которого по умолчанию составляет 200 символов. В проекте этот показатель снижается до 50 символов (10% от размера чанка), что считается хорошей отправной точкой в веб-разработке. Перекрытие необходимо для того, чтобы важная информация, оказавшаяся на стыке двух фрагментов, не потеряла свой контекст и не стала бесполезной для ИИ.

🚀 Загрузка данных и концепция изолированных вопросов (Standalone Questions) 27:52

Процесс одновременного создания эмбеддингов и их выгрузки в облачную базу данных реализуется методом SupabaseVectorStore.fromDocuments. На вход методу подаются подготовленные текстовые чанки, экземпляр класса OpenAIEmbeddings с API-ключом и сконфигурированный клиент Supabase с указанием целевой таблицы documents. После успешного выполнения операции в личном кабинете Supabase можно наблюдать заполненную таблицу, где для каждого текстового блока рассчитан массив из 1536 чисел.

После подготовки базы данных фокус разработки смещается на обработку пользовательского ввода. Здесь программисты сталкиваются с серьезным вызовом: пользователи формулируют свои вопросы хаотично и многословно.

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

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

Для решения этой проблемы в LangChain применяется концепция изолированного вопроса (Standalone Question). С помощью языковой модели OpenAI развернутый пользовательский запрос очищается от эмоционального и второстепенного шума, сокращаясь до минимального набора слов: «Какова ваша политика возврата?». Именно эта лаконичная фраза затем отправляется в векторную базу данных для поиска совпадений.

🔗 Составление простых цепочек и синтаксис LCEL 35:27

Для проектирования логики приложения используется современный стандарт LangChain Expression Language (LCEL). В основе взаимодействия лежат два базовых класса: ChatOpenAI для инициализации языковой модели и PromptTemplate для формирования структуры запросов.

При создании шаблонов LangChain использует синтаксис F-строк (F-strings), заимствованный из языка Python. Переменные внутри шаблона оборачиваются в фигурные скобки без знака доллара (например, {question}). При компиляции промпта фреймворк автоматически формирует массив inputVariables, контролируя обязательное наличие всех входных параметров перед отправкой запроса к LLM.

Связывание компонентов в единый конвейер осуществляется с помощью метода pipe (символ вертикальной черты | в синтаксисе). Простая цепочка берет выходные данные первого элемента (скомпилированный промпт) и автоматически передает их на вход второму элементу (модели ИИ). Под капотом этой конструкции создается объект класса RunnableSequence.

Для очистки ответов модели от служебных метаданных разработчики внедряют специальный компонент — StringOutputParser. По умолчанию вызов модели возвращает сложный JSON-объект с детальной информацией о транзакции, в то время как выходной парсер строк извлекает исключительно текстовое содержимое ответа, преобразуя его в стандартную строку JavaScript.

🛠️ Полноценный конвейер поиска: от ретривера до сложного RunnableSequence 46:21

Для извлечения данных из Supabase создается объект-ищейка (Retriever) с помощью вызова метода asRetriever() на экземпляре векторного хранилища. Этот метод автоматически связывается с SQL-функцией match_documents, инкапсулируя внутри себя весь сложный процесс математического поиска ближайших соседей.

При интеграции ретривера в общую цепочку сборки возникает архитектурный конфликт типов данных. Ретривер ожидает на вход чистую строку, тогда как предыдущие шаги конвейера могут передавать объекты. Для решения этой проблемы создается утилитарная функция combineDocuments, которая принимает массив найденных объектов, извлекает из каждого поле pageContent и склеивает их в единый текстовый блок, разделенный двойным переносом строки.

Сложные конвейеры со множеством ветвлений выстраиваются через явное объявление класса RunnableSequence.from([ ... ]). Том Чант демонстрирует мощь этого подхода на примере многошагового лингвистического приложения, которое последовательно исправляет пунктуацию, затем правит грамматику и на финальном этапе переводит текст на французский язык.

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

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

🧠 Наделение бота памятью: сохранение контекста беседы 1:21:56

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

Для интеграции механизма памяти Том Чант разворачивает глобальный массив conversationHistory на стороне JavaScript. При каждом цикле взаимодействия в этот массив методом .push() добавляются пары строк: текущий вопрос пользователя и полученный ответ от нейросети.

Чтобы языковая модель могла корректно интерпретировать эту историю, создается вспомогательная функция formatComHistory. Она принимает массив сообщений и на основе проверки индекса элемента (четный или нечетный) размечает строки служебными метками:

Вся размеченная история объединяется в одну длинную строку с переносами строк. Полученный блок данных передается в цепочку под ключом com_history и внедряется сразу в два шаблона — в промпт для генерации изолированного вопроса (чтобы ИИ понимал, к чему относятся местоимения в новых репликах) и в финальный шаблон формирования ответа. Тестирование подтверждает: после такой модернизации бот безошибочно вспоминает имя пользователя, названное в начале беседы.

🎛️ Тонкая настройка и оптимизация ИИ-приложений 1:34:03

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

  1. Изменение параметров разделения текста. Если боту не хватает контекста для ответа, следует увеличить размер чанка. Если же ответы размыты, уменьшение размера чанка поможет добиться большей семантической точности.
  2. Регулирование количества извлекаемых фрагментов (k). По умолчанию ретривер возвращает 4 наиболее похожих фрагмента текста. Передавая конфигурационный объект, это число можно увеличить (например, до 10 для расширения кругозора модели) или уменьшить до 1, чтобы жестко сфокусировать ИИ на самом качественном совпадении.
  3. Качественный промпт-инжиниринг. Разработчик может существенно повысить точность системы, усложняя и детализируя инструкции в шаблонах, добавляя конкретные правила поведения и примеры успешных ответов.
  4. Управление температурой модели OpenAI. По мнению Тома Чанта, при создании корпоративных справочных ботов параметр temperature целесообразно принудительно устанавливать в значение 0. Это минимизирует смелость и креативность ИИ, исключая галлюцинации и заставляя модель отвечать строго по предоставленным фактам.
  5. Смена языковой модели и штрафы. Разработчики могут легко переключаться между моделями (например, с GPT-3.5 на более мощную GPT-4), а также задействовать параметры frequency_penalty и presence_penalty для тонкой регулировки вариативности речи бота.
💬 Цитаты

«LangChain.js делает большие языковые модели и техники работы с ними более доступными для широкой аудитории веб-разработчиков.»

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

«Вектор представляет собой семантическое значение, а не точные слова.»

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

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