Недорогой путь к освоению ИИ: создание чат-бота навигатора по сайту на основе Similarity Search
Перевод моей статьи с HackerNoon.
Мир ИИ и больших языковых моделей (LLM) часто вызывает образы огромных вычислительных мощностей, проприетарных платформ и GPU-кластеров. Такое восприятие создаёт высокий порог входа, отпугивая любопытных разработчиков от изучения основ.
Недавно я совершенно случайно за короткое время создал небольшой проект — простой чат-бот на базе ИИ (код тут https://github.com/tacticaxyz/tactica.faq.similaritysearch), который я называю Wiki Navigator. Он показал на практике, что для изучения основ ИИ излишняя сложность вовсе не обязательна. Сосредоточившись на таких ключевых понятиях, как токенизация, векторные эмбеддинги и косинусное сходство, я построил рабочее решение поиска на основе RAG (Retrieval Augmented Generation), которое успешно находит необходимый контент среди 9 000 документов в кодовой базе Chromium. На запуск ушло всего несколько часов, а на следующий день я уже смог использовать тот же код, чтобы обучить чат-бота на откартых онлайн книгах и ресурсах о языке Rust и получать полезные ответы в процессе изучения.
Главное открытие? Чтобы практически начать использовать LLM и ИИ, на сегодняшний день не обязательно использовать массивы GPU. Это крайне полезный и практичный опыт — учиться на практике и сразу получать результаты без значительных затрат.
Магия векторных эмбеддингов
Задача Wiki Navigator не генерация нового текста, а извлечение релевантных ссылок из исходной документации в соответствии с контекстом вопроса. Мне было важно чтобы пользователь (в первую очередь, я) мог переходить по прямым ссылкам и читать ответ в источнике, а не галлюцинации ИИ. По своей сути, это контекстный поисковик, построенный на Retrieval Augmented Generation (RAG), вместо классического обратного индекса.
Основная идея удивительно проста:
- Подготовка (этап обучения): Все документы (а это пары вопрос-ответ, ссылки на исходный Wiki проекта, а также сами страницы в формате markdown) конвертируются в цифровое представление — векторные эмбеддинги (вот отличное объяснение, если ещё не видели). Для больших корпусов это занимает около часа и создаёт векторный индекс.
- Запрос (этап поиска): Вопрос пользователя также преобразуется в векторный эмбеддинг.
- Сравнение: Система сравнивает вектор запроса с векторами документов с помощью операции косинусного сходства, чтобы найти ближайшие совпадения. На практике это просто математическая операция между двумя векторами. Если два вектора находятся близко друг к другу — скорее всего, это означает совпадение по контексту. "Скорее всего" здесь ключевое - всё сильно зависит от того на каких данных происходила "тренировка" - то есть что есть в нашей базе векторного индекса.
Этот простой процесс отлично работает для навигации по документации и поиска полезных материалов.
Обеспечиваем согласованность алгоритмов
Хотя многие статьи сосредоточены на теории поиска по сходству, настоящий интерес начинается с реализации.
Процесс обучения, который готовит базу контекста, реализован на C# (TacTicA.FaqSimilaritySearchBot.Training/Program.cs
). Данные преобразуются в эмбеддинги с помощью одного из сервисов по выбору пользователя в момент запуска:
SimpleEmbeddingService
(хэш-базированный, для статического сайта без AI-модели),TfIdfEmbeddingService
(TF-IDF/поиск по ключевым словам — расширенная версия),OnnxEmbeddingService
(основан на предобученной трансформер-моделиall-MiniLM-L6-v2
, требует запуска бэкенда с загруженной моделью).
Любопытно, что для запуска упрощённого MVP можно обойтись без какой-либо модели ИИ, используя SimpleEmbeddingService
, что позволяет развернуть даже безсерверное решение, прямо в браузере — идеально для размещения на GitHub Pages. Это достигается засчет того что конвертация символов исходного текста в цифру (вектора) происходит с использованием обычной хеш-функции (см. пример чуть ниже).
В случае использования AI модели, а не хеш функции, нужно скачать и использовать сам файл модели, а также словарь токенов, по которому токенизатор будет заменять символы и делать предподготовку исходного текста для векторизации. Использование AI модели во время тренировки накладывает необходимость её использования и во время запуска, для конвертации пользовтельского вопроса в вектор по тем же правилам что и исходный текст. В этой статье я в основном фокусируюсь на первом варианте — простом хэш-подходе. Хотя у меня есть и продакшн-решение на основе AI-модели, работающее на tactica.xyz. Это полноценное React-приложение, выполняющее сравнения на сервере, но фундаментальные принципы одинаковы.
Статическое развёртывание требует, чтобы обучающая программа (C#) и клиентское приложение (JavaScript) использовали идентичные алгоритмы токенизации и вычисления векторов, чтобы обеспечить корректную работу и одинаковые результаты. Понятно, что два языка сильно отличаются по типам данных и нужно дополнительно убедиться чтобы результаты вычислений совпадали. Но это совсем не rocket-science. Основные математические утилиты для токенизации и работы с векторами во время "тренировки" реализованы в (TacTicA.FaqSimilaritySearchBot.Shared/Utils/VectorUtils.cs
). Чтобы браузерное приложение на JavaScript (TacTicA.FaqSimilaritySearchBot.Web/js/chatbot.js
или TacTicA.FaqSimilaritySearchBot.WebOnnx/js/chatbot.js
) обрабатывало запросы пользователя точно так же, как алгоритм на C#, эти шаги необходимо точно воспроизвести, например, реализовать ровно ту же самую хеш-функцию с соблюдением точности операций:
C# (SimpleEmbeddingService.cs):
// Этот метод взят из chatbot.js, чтобы Simple Embedding Service вообще работал!
private Func<double> SeededRandom(double initialSeed)
{
double seed = initialSeed;
return () =>
{
seed = (seed * 9301.0 + 49297.0) % 233280.0;
return seed / 233280.0;
};
}
JavaScript (chatbot.js):
// Генератор случайных чисел с фиксированным seed
seededRandom(seed) {
return function() {
seed = (seed * 9301 + 49297) % 233280;
return seed / 233280;
}
}
Косинусное сходство
В C#-приложении класс VectorUtils
отвечает за вычисление косинусного сходства — центральной операции сравнения:
// Фрагмент TacTicA.FaqSimilaritySearchBot.Shared/Utils/VectorUtils.cs
// Эта функция вычисляет "сходство" двух векторов (эмбеддингов).
public static double CalculateCosineSimilarity(float[] vectorA, float[] vectorB)
{
// [C#: нормализация и скалярное произведение
// для вычисления метрики от 0.0 до 1.0]
// ... реализация здесь ...
// return similarityScore;
}
Такая же операция есть и в JavaScript.
Поиск в реальном времени на JavaScript
Клиентское приложение должно выполнять то же вычисление в реальном времени при каждом запросе пользователя, используя предрасчитанный индекс. Для этого применяется быстрый поиск по векторам в памяти с помощью простого алгоритма.
function performSimilaritySearch(queryVector, documentIndex) {
let bestMatch = null;
let maxSimilarity = 0.0;
// Преобразуем запрос в вектор (хэш/TF-IDF или ONNX для трансформера).
for (const [docId, docVector] of Object.entries(documentIndex)) {
const similarity = calculateCosineSimilarity(queryVector, docVector);
if (similarity > maxSimilarity) {
maxSimilarity = similarity;
bestMatch = docId;
}
}
if (maxSimilarity >= CONFIG.SimilarityThreshold) {
// Возвращаем FAQ-ответ с цитированием
} else {
// Запускаем RAG-поиск по всему корпусу
}
return bestMatch;
}
Таким образом, обеспечивая идентичность векторных утилит в C# и JavaScript, мы получаем одинаковые результаты как на этапе обучения, так и при поиске.
Тренировка
Запуск обучающего пайплайна занимает около часа, так как мы НЕ используем GPU, параллельные вычисления или другие навороты. Мы учимся на базовых вещах и не хотим всё усложнять:
За пределами простого поиска
Под капотом наш бот делает всё таки более сложную работу с исходными данными, чем обычный поиск по ключевым словам. Он реализован в трёхфазной архитектуре:
- Фаза 1: Подготовка базы контекста. Документы и Q&A конвертируются в векторы и индексируются.
- Фаза 2: Обработка запроса. Сначала выполняется умное сопоставление FAQ с порогом сходства (по умолчанию 0.90). Если уверенность высокая, возвращается точный ответ.
- Фаза 3: RAG-поиск. Если уверенность низкая, включается fallback-механизм: поиск по полному корпусу документов, Top-K выборка и генерация ответа с указанием источников.
Этот механизм гарантирует, что каждый ответ основан на цитатах. Однако качество зависит от количества Q&A, использованных в обучении. При их малом числе бот будет чаще находить нерелевантные ссылки. Тем не менее даже в этом случае он полезен, так как возвращает корректные URL. С ростом числа Q&A качество значительно повышается.
Нюансы поиска по сходству
Практическая работа сразу открывает интересные наблюдения, которые часто остаются скрытыми в теоретических работах.
Например, прямое сравнение подходов показывает, что бот может работать как с AI-моделью (ONNX-эмбеддинги на базе трансформера), так и без неё, используя чисто хэш-подход. Но в общем, самостоятельная эффективность эмбеддингов как таковых ограничена, что подробно обсуждается в статье "On the Theoretical Limitations of Embedding-Based Retrieval".
Кроме того, работа с косинусным сходством наглядно демонстрирует феномен "Cosine Similarity Abuse" — практический пример того, как можно обмануть неинтеллектуальные системы ИИ. Это лишь верхушка айсберга большой проблемы "Prompt Injection" (пример хорошего материала), которая представляет серьёзную угрозу для пользователей и инженеров, создающих ИИ в продакшне.
Выводы
Создание рабочего бота, который обрабатывает 9 000 документов из проекта Chromium, требует технической аккуратности, но не требует гигантской инфраструктуры и глубокого научного понимания ИИ. Этот проект показывает, что ключевые основы LLM и ИИ — токенизация, векторизация и сравнение по сходству — вполне доступны каждому, кто готов покопаться в коде.
Wiki Navigator служит яркой демонстрацией того, что возможно построить поиск по сходству на ваших собственных данных.
Ссылки
- Исходный код:
https://github.com/tacticaxyz/tactica.faq.similaritysearch
- Chromium-демо:
https://tactica.xyz/#/chromium-similarity-search
- Rust-демо:
https://tactica.xyz/#/rust-similarity-search
Комментарии
Отправить комментарий