Недорогой путь к освоению ИИ: создание чат-бота навигатора по сайту на основе 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), вместо классического обратного индекса.

Основная идея удивительно проста:

  1. Подготовка (этап обучения): Все документы (а это пары вопрос-ответ, ссылки на исходный Wiki проекта, а также сами страницы в формате markdown) конвертируются в цифровое представление — векторные эмбеддинги (вот отличное объяснение, если ещё не видели). Для больших корпусов это занимает около часа и создаёт векторный индекс.
  2. Запрос (этап поиска): Вопрос пользователя также преобразуется в векторный эмбеддинг.
  3. Сравнение: Система сравнивает вектор запроса с векторами документов с помощью операции косинусного сходства, чтобы найти ближайшие совпадения. На практике это просто математическая операция между двумя векторами. Если два вектора находятся близко друг к другу — скорее всего, это означает совпадение по контексту. "Скорее всего" здесь ключевое - всё сильно зависит от того на каких данных происходила "тренировка" - то есть что есть в нашей базе векторного индекса.

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

Обеспечиваем согласованность алгоритмов

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

Процесс обучения, который готовит базу контекста, реализован на 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, параллельные вычисления или другие навороты. Мы учимся на базовых вещах и не хотим всё усложнять:

https://asciinema.org/a/736921

 asciicast

За пределами простого поиска

Под капотом наш бот делает всё таки более сложную работу с исходными данными, чем обычный поиск по ключевым словам. Он реализован в трёхфазной архитектуре:

  1. Фаза 1: Подготовка базы контекста. Документы и Q&A конвертируются в векторы и индексируются.
  2. Фаза 2: Обработка запроса. Сначала выполняется умное сопоставление FAQ с порогом сходства (по умолчанию 0.90). Если уверенность высокая, возвращается точный ответ.
  3. Фаза 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

Комментарии

Популярные

Кастомизируем ASP.NET Identity 2.0

Делаем себе бесплатный VPN на Amazon EC2

Выбираем все плюсы из трех парадигм Entity Framework