От Naive RAG до ReAct-агента: как мы строили корпоративного AI-помощника на open-source моделях (часть 1)

от автора

Привет, Хабр!

Меня зовут Константинов Александр, я — старший AI-инженер в Лаборатории искусственного интеллекта «Честного знака». Наша команда развивает «Честного помощника» — мультиагентную LLM-систему для обработки документов, поиска информации по Confluence, Jira, GitLab и генерации текстов. Главная цель команды — повышать эффективность и качество работы сотрудников за счёт расширения числа специализированных агентов в нашей мультиагентной системе. Но давайте будем честны: мы начинали с решения совсем другой задачи. Терминология на тот момент была ещё сырой и непроверенной, рынок open-source решений оставлял желать лучшего — мы выжимали максимум из того, что было доступно. Поэтому дальнейший рассказ будет полезен широкой аудитории: от тех, кто только начинает разбираться в теме, до руководителей отделов, которые хотят внедрить подобное решение у себя.

Это первая часть из двух. Здесь мы разберём ключевые понятия, а затем подробно пройдём весь путь создания MVP на базе Naive RAG — от индексации документов до первых ответов в продакшене и осознания, что этого недостаточно. Во второй части перейдём к Advanced RAG, мультиагентной архитектуре, ReAct-агенту и качественному тестированию.

Основная терминология и понятия

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

Токен – это единица текста, например слово или часть слова, которой присвоен уникальный числовой идентификатор. LLM-модели используют такие числовые представления, чтобы эффективно обрабатывать и «понимать» текст. Для расчета токенов используется модель-токенизатор от LLM, которая преобразует текст в токены.

Контекст — это набор токенов, которые LLM обрабатывает за один проход вперёд (forward pass). Он формирует основу для ответа модели.

Контекстное окно — это максимальное количество токенов, которое модель способна воспринять за один раз. Ограничение длины окна влияет на то, сколько информации можно включить в один запрос.

Галлюцинация — это когда модель генерирует убедительный, но ложный или неподтверждённый текст. Ответ может выглядеть правдоподобно, но не основан на фактах или не соответствует запросу.

Open source LLM — это модели, которые доступны для свободного использования и модификации. Они не имеют лицензионных ограничений и могут быть использованы в коммерческих проектах. Наиболее популярные open source LLM:

  • Qwen

  • Deepseek

  • Mistral (отдам дань уважения :D)

Давайте подробнее остановимся на проблеме галлюцинаций. Представим ситуацию: пользователь задаёт вопрос по своей доменной специфике, которая отражена лишь на закрытой странице в Confluence. Что произойдёт?

LLM сгенерирует ответ, но он не будет соответствовать действительности — у модели нет доступа к этой информации.

Самое неприятное: чтобы понять, что модель галлюцинирует, нужно самому обладать доменной экспертизой. Но тогда зачем вообще спрашивать у LLM, которая не имеет доступа к релевантной информации?

Тогда возникает вопрос, как избежать галлюцинаций и расширить спектр знаний LLM? Для этого существует ряд подходов:

  1. Fine-tuning LLM на конкретной доменной задаче.

  2. RAG (Retrieval-Augmented Generation).

  3. Prompt Engineering

Сравнение подходов: Prompt Engineering, Fine-tuning и RAG в борьбе с галлюцинациями

Сравнение подходов: Prompt Engineering, Fine-tuning и RAG в борьбе с галлюцинациями

RAG (Retrieval-Augmented Generation) — подход, при котором перед генерацией ответа модель сначала извлекает релевантную информацию из внешней базы знаний, а затем формирует ответ на основе найденных данных. Благодаря подключению внешнего хранилища знаний нет необходимости переобучать всю модель под каждую конкретную задачу. RAG особенно эффективен для задач, требующих актуальных или узкоспециализированных знаний. Подробнее архитектуру разберем в следующих разделах.

Prompt Engineering — это процесс создания и оптимизации текстовых запросов для модели, чтобы получить более точные и релевантные ответы. Этот подход позволяет управлять поведением модели, настраивая её на конкретные задачи и данные.

Fine-tuning LLM — это процесс дообучения предобученной модели на конкретном наборе данных. Позволяет «зашить» в модель новые знания или стиль ответов, однако требует значительных вычислительных ресурсов и времени.

Коротко о плюсах и минусах каждого подхода:

Критерий

Prompt Engineering

RAG

Fine-tuning

Сложность внедрения

Низкая

Средняя

Высокая

Стоимость

Минимальная

Средняя (хранилище + inference)

Высокая (GPU, датасет)

Актуальность знаний

Ограничена контекстным окном

Высокая — данные обновляются независимо от модели

Низкая — требует повторного обучения при изменениях

Контроль над источниками

Нет

Да — можно отследить, из каких документов взят ответ

Нет

Качество на доменных задачах

Среднее

Высокое

Высокое

Риск галлюцинаций

Высокий

Низкий — ответ опирается на конкретные документы

Средний

Когда применять

Быстрые эксперименты, общие задачи

Корпоративные базы знаний, FAQ, поиск по документам

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

Скажем честно: когда речь идёт о генерации ответов по доменным знаниям, fine-tuning на практике упирается в одну болезненную проблему — подготовку датасета. Нанять стороннего специалиста не получится: для разметки нужен доменный эксперт, который вручную составит сотни пар «вопрос — эталонный ответ». Это дорого, долго и тяжело масштабируется. Именно поэтому для большинства корпоративных задач RAG оказывается практичнее: не нужно обучать модель — достаточно загрузить актуальные документы, а при необходимости расширять базу знаний — добавлять новые документы.

Именно поэтому мы решили развивать наше решение в направлении RAG и перешли к первому этапу разработки MVP- Naive RAG.

Naive RAG

Naive RAG — это простейший подход к RAG, который не использует никаких дополнительных технологий и просто использует базу знаний в качестве источника информации и LLM для генерации ответа.

Состоит из 3 шагов:

1 шаг — Indexing — подготовка и загрузка документов в базу знаний. Включает три подэтапа

  • Parsing — извлечение текста из исходных документов (PDF, DOCX, Confluence-страницы и др.). На выходе получаем «сырой» текст без форматирования.

  • Chunking — разбивка извлечённого текста на небольшие фрагменты (чанки). Необходимо, потому что LLM имеет ограниченное контекстное окно и не может обработать целый документ за один раз. Размер чанка — типично от 256 до 1024 токенов.

  • Embedding — каждый чанк преобразуется в числовой вектор с помощью модели-эмбеддера. Этот вектор отражает смысловое содержание фрагмента. Все векторы сохраняются в векторную базу данных (в нашем случае — Qdrant).

Скрытый текст

Что такое модель-эмбеддер?

Это отдельная нейронная сеть, задача которой — превратить текст в вектор фиксированной размерности (например, 768 или 1024 числа). В отличие от LLM, эмбеддер не генерирует текст — он только «понимает» смысл и кодирует его в числа. Близкие по смыслу тексты получают близкие векторы, далёкие по смыслу — далёкие. Именно это свойство и лежит в основе семантического поиска в RAG. Среди популярных открытых моделей — intfloat/multilingual-e5-large, Qwen/Qwen3-Embedding-0.6B, deepvk/USER-bge-m3. Для русского языка важно выбирать мультиязычные или специально обученные на кириллице модели — иначе качество поиска резко падает.

  • Векторы бывают двух типов:

    • Dense (плотные) — вектор фиксированной длины, где каждое из сотен или тысяч чисел несёт смысловую нагрузку. Генерируются нейросетевыми эмбеддерами. Хорошо улавливают семантику: находят похожие по смыслу фрагменты, даже если слова в запросе и документе разные. Основной тип, используемый в RAG.

    • Sparse (разреженные) — вектор огромной размерности (десятки тысяч), где большинство значений равны нулю, а ненулевые позиции соответствуют конкретным словам из словаря. По принципу работы близки к классическому поиску по ключевым словам (например, BM25). Хуже справляются с синонимами, но точнее находят точные совпадения терминов, аббревиатур и имён собственных.

Пример работы эмбеддера

Пример работы эмбеддера

2 шаг — Retrieval — поиск релевантных фрагментов по запросу пользователя. Запрос так же преобразуется в вектор тем же эмбеддером, после чего в векторной БД находятся чанки с наименьшим «расстоянием» до вектора запроса — то есть наиболее близкие по смыслу.

3 шаг — Generation — найденные чанки передаются в LLM вместе с исходным вопросом пользователя. Модель формирует финальный ответ, опираясь на этот контекст, а не на свои «внутренние» знания.

Naive RAG

Naive RAG

Давайте разберём, как была устроена архитектура нашего Naive RAG MVP.

MVP состоял из двух независимых бэкендов: один отвечал за взаимодействие с LLM, второй — за работу с векторной базой данных Qdrant. Никакой оркестрации, никаких агентов — просто два сервиса, которые последовательно обрабатывали запрос пользователя.

Parsing + Chunking

В качестве источника знаний мы использовали Confluence. Для его загрузки применяли готовый Confluence Loader из библиотеки LangChain — он забирает страницы по API и возвращает текст. Далее текст разбивался на чанки с помощью Recursive Character Splitter: сплиттер рекурсивно делит текст по иерархии разделителей (\n\n → \n → . → ), стараясь сохранять смысловые границы абзацев.

Итерация 1: как делали — и почему это не работало

На старте мы запускали лоадер в самом простом виде, не задумываясь о параметре content_format — он остался на дефолтном значении ContentFormat.STORAGE:

from langchain_community.document_loaders import ConfluenceLoaderfrom langchain_community.document_loaders.confluence import ContentFormatloader = ConfluenceLoader(    url="https://confluence.yoursite.com/",    token="<your-token>",    space_key="MYSPACE",    # content_format не указан → по умолчанию ContentFormat.STORAGE)documents = loader.load()

Сплиттер — аналогично: взяли большой chunk_size в символах, чтобы «не терять контекст»:

from langchain_text_splitters import RecursiveCharacterTextSplittertext_splitter = RecursiveCharacterTextSplitter(    chunk_size=4096,  # символы, не токены — это важно    chunk_overlap=0,)chunks = text_splitter.create_documents([doc.page_content for doc in documents])

Здесь сразу две проблемы:

  1. ContentFormat.STORAGE — это сырое XML-представление страницы Confluence для внутреннего хранения. В нём макросы не отрендерены: таблицы, блоки кода, вставки дат, упоминания пользователей и другие макросы существуют как XML-теги, а не как текст. При парсинге BeautifulSoup вырезает все теги — и вместе с ними уходит весь контент макросов. В индекс попадает неполный текст без структуры.

    ContentFormat.VIEW, напротив, возвращает страницу в том виде, в каком её видит пользователь в браузере: макросы отрендерены, таблицы раскрыты, даты и имена стоят как обычный текст.

  2. chunk_size=4096 в символах — это не токены. Большинство эмбеддеров (например, multilingual-e5-large) имеют контекстное окно 512 токенов. Чанк из 4096 символов может содержать 2000–3000 токенов — модель молча обрезает всё, что не влезает, и векторизует только начало фрагмента. Качество поиска деградирует незаметно.

Итерация 2: как исправили

Для лоадера явно указали ContentFormat.VIEW и включили конвертацию в Markdown:

from langchain_community.document_loaders import ConfluenceLoaderfrom langchain_community.document_loaders.confluence import ContentFormat# pip install markdownify  ← обязательная зависимость для keep_markdown_formatloader = ConfluenceLoader(    url="https://confluence.yoursite.com/",    token="<your-token>",    space_key="MYSPACE",    content_format=ContentFormat.VIEW,  # рендеренный HTML вместо сырого XML    keep_markdown_format=True,          # конвертировать HTML → Markdown через markdownify    include_attachments=True,    limit=50,    max_pages=1000,)documents = loader.load()# Теперь таблицы → | col | col |, блоки кода → ```code```, даты и имена — обычный текст

Комбинация ContentFormat.VIEW + keep_markdown_format=True даёт двойной эффект: макросы раскрыты через VIEW, а Markdown-разметка помогает сплиттеру резать текст по смысловым границам (заголовки #, списки -, абзацы \n\n) вместо произвольных позиций.

Для сплиттера перешли на подсчёт размера чанка в токенах, а не в символах, и добавили перекрытие:

from langchain_text_splitters import RecursiveCharacterTextSplitterfrom transformers import AutoTokenizertokenizer = AutoTokenizer.from_pretrained("intfloat/multilingual-e5-large")def token_length(text: str) -> int:    return len(tokenizer.encode(text))text_splitter = RecursiveCharacterTextSplitter(    chunk_size=400,                 chunk_overlap=50,               length_function=token_length, # считаем в токенах, а не символах    is_separator_regex=False,)chunks = text_splitter.create_documents([doc.page_content for doc in documents])

Ключевой момент: length_function=token_length — без него chunk_size считается в символах, и реальный размер чанка в токенах непредсказуем. Это тихая ошибка, которую легко не заметить при первых тестах.

Retrieval

Индексация

После того как чанки превращены в векторы, их нужно куда-то положить — и так, чтобы потом можно было быстро искать. Именно здесь появляется понятие индексации.

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

Самый популярный алгоритм индексации для векторного поиска — HNSW (Hierarchical Navigable Small World). Именно его использует Qdrant по умолчанию.

Как работает HNSW

Скрытый текст

Алгоритм строит многоуровневый граф над всеми векторами. Каждый вектор — это узел графа, рёбра соединяют соседей. Уровней несколько: чем выше уровень, тем меньше узлов и длиннее «прыжки» между ними.

  1. Для каждого нового вектора случайным образом выбирается максимальный уровень — большинство точек попадает только на нижние уровни (распределение экспоненциальное).

  2. Начинаем с верхнего уровня с «точки входа» (entry point) и идём вниз, на каждом уровне находя локальный минимум расстояния.

  3. На базовом уровне (layer 0) ищем M ближайших кандидатов; параметр efConstruction задаёт размер очереди кандидатов при построении — больше значение, точнее индекс, но дольше строится.

  4. Новый вектор соединяется с найденными M кандидатами. Если у соседей число связей превышает M_max, лишние (самые дальние) отбрасываются.

Результат: поиск ближайшего соседа выполняется за O(log N) вместо O(N) при полном переборе. Главное преимущество HNSW в том, что он позволяет систематически обновлять индекс без полной переиндексации всей коллекции.

Векторная база данных

На этапе MVP мы рассматривали два варианта хранилища:

Векторная БД

Когда подходит

Qdrant

Большие коллекции, production-нагрузка, гибридный поиск, встроенный HNSW

Faiss

Быстрые эксперименты, тестирование гипотез, оптимизация под GPU

Для первого MVP выбрали Faiss — он позволял быстро проверять гипотезы без поднятия отдельного сервиса. Но уже на следующем этапе перешли на Qdrant: встроенный HNSW, поддержка гибридного поиска (dense + sparse), горизонтальное масштабирование и удобный REST/gRPC API для production-среды.

Generation

GPU для open-source LLM

Поскольку «Честный помощник» работает с закрытыми корпоративными данными, отправлять их в облачные API — OpenAI, Anthropic, Google — невозможно по соображениям безопасности. Только open-source модели, только on-premise.

И здесь большинство команд сталкиваются со стеной.

Видеокарты — это не деталь реализации, это условие существования всего проекта. Без достаточных GPU-мощностей вы физически не можете запустить современную LLM и получить приемлемое качество ответов в реальном времени. Большинство RAG-проектов, с которыми я знаком, разбились именно об этот камень: красивая архитектура на бумаге, но модель либо не влезает в память, либо отвечает слишком медленно, либо галлюционирует из-за своих мелких размеров.

Важно понимать: конечное качество системы определяется не тем, насколько тонко вы настроили индексацию и retrieval. Всё это не имеет значения, если ваша LLM не способна корректно следовать инструкциям в промпте и извлекать нужную информацию из переданного контекста. Размер модели — это не только объём знаний, но и способность «слушаться». Модели на 7B и на 70B параметров ведут себя принципиально по-разному при работе со сложными инструкциями.

Поэтому ещё до написания первой строки кода MVP — считайте ресурсы. Какую модель вы хотите запустить? Сколько VRAM она требует? Какую latency ожидает пользователь? Ответы на эти вопросы должны быть на столе до старта разработки, а не после первого деплоя.

В нашей компании мы прошли путь от попытки запускать модели на CPU до продовой развертки на GPU, результат сейчас кажется очевидным — без GPU вы не сможете получить приемлемое качество ответов в реальном времени. Но стоит понимать, что к моменту старта проекта на рынке еще не было такого количество open-source решений, которые позволяли бы однозначно подтвердить качество работы системы в целом.

Инференс для open-source LLM

Будем периодически возвращаться к этому пункту — он один из ключевых в нашей дальнейшей эволюции системы.

Когда GPU есть, возникает следующий вопрос: как именно запускать модель? Просто загрузить веса через transformers и вызвать model.generate() — можно, но в production это не работает: один запрос блокирует все остальные, нет batching’а, нет управления памятью. Нужен инференс-движок, который позволит держать продовую нагурзку и оптимизировать использование памяти.

Что такое KV-кэш?

Скрытый текст

При генерации текста трансформер на каждом шаге вычисляет матрицы Key и Value для всех токенов в контексте — это и есть механизм внимания (attention). Пересчитывать их заново для уже обработанных токенов на каждом следующем шаге бессмысленно: результат не изменится. Поэтому эти матрицы сохраняются в памяти GPU — в KV-кэше. Чем длиннее контекст, тем больше места занимает кэш: для модели 7B при контексте 8K токенов это может быть несколько гигабайт VRAM. Именно борьба за эффективное использование этой памяти и отличает production-движки от наивного запуска модели. Это позволяет: — обслуживать несколько запросов одновременно (continuous batching) без простоя GPU между ними, — эффективно переиспользовать кэш для повторяющихся префиксов промптов, — масштабироваться горизонтально на несколько GPU через tensor parallelism.

Выделю три варианта, каждый из которых предназначен (по моему мнению) для своей задачи.

Ollama — идеальный вариант для начальных стадией развития проекта.

Когда нужно быстро поднять модель локально и проверить гипотезу, Ollama — лучший выбор. Один командой устанавливается, одной командой запускается, есть REST API. Никакой конфигурации, никакого управления зависимостями.

Но у простоты есть цена. По умолчанию Ollama обрабатывает запросы последовательно (OLLAMA_NUM_PARALLEL=1) — параллелизм настраивается через переменные окружения, но даже с настройкой эффективность ниже, чем у специализированных движков: каждый параллельный слот требует отдельного KV-кэша и «съедает» дополнительные 15–25% VRAM. Для одного разработчика или небольшой команды — вполне приемлемо. Для сервиса с десятками одновременных пользователей — начинаются проблемы с latency и утилизацией GPU.

vLLM — production

vLLM решает главную проблему inference под нагрузкой — эффективное использование GPU-памяти. Ключевая идея — алгоритм PagedAttention: он управляет KV-кешем так же, как операционная система управляет страницами оперативной памяти.

На практике vLLM даёт в 2–4 раза больший throughput по сравнению с наивным запуском той же модели. Именно поэтому он стал нашим production-стеком.

SGLang — смотрим в будущее

SGLang — более молодой движок, оптимизированный под другой профиль нагрузки: короткие запросы с низкой latency. Там, где vLLM оптимизирует throughput (пропускную способность), SGLang оптимизирует время первого токена. Для агентных пайплайнов, где агент делает десятки коротких вызовов к модели последовательно, это критично.

Сейчас SGLang у нас в исследовательском бэклоге — тестируем на агентных сценариях. Если результаты подтвердятся, это станет следующим шагом в эволюции инференс-слоя.

Движок

Когда использовать

Ключевое преимущество

Ollama

Локальная разработка, быстрые эксперименты

Простота запуска

vLLM

Production, высокая нагрузка, много параллельных запросов

Throughput, PagedAttention

SGLang

Агентные пайплайны, streaming, низкая latency на коротких запросах

Время первого токена

API работы с LLM Для начинающих раработчиков

Поскольку наша команда работает только с open-source моделями, для нас с самого начала был важен практический вопрос: как именно взаимодействовать с LLM из продуктового бэкенда? Нужно было понять, как передавать запросы в модель, как получать ответы и как встроить это в существующую серверную архитектуру.

Если вы раньше не работали ни с API closed-source провайдеров, ни с локально развёрнутыми LLM, вопрос возникает почти сразу: допустим, модель мы подняли на своих GPU. Что дальше? Как написать слой, через который приложение будет с ней общаться?

К счастью, разработчики open-source inference-движков уже решили эту задачу. Такие системы, как vLLM, позволяют поднять OpenAI-compatible API, то есть HTTP-бэкенд, совместимый с OpenAI SDK. На практике это означает, что модель можно запустить локально командой вроде vllm serve <model_name> при работе с vLLM или ollama serve <model_name> при работе с Ollama, а дальше обращаться к ней почти так же, как к облачному API.

Это критически важно для разработки решений на базе open-source моделей, потому что такой API закрывает почти все базовые сценарии работы с LLM:

  • completion

  • structured output

  • tool calling

  • streaming

  • embeddings

Квантизация для open-source LLM

Еще на ранних этапах проекта мы поняли ещё одну важную вещь: квантизация — идеальный вариант для production. Без неё многие сильные open-source модели просто не помещаются в доступную VRAM, а стоимость инференса становится слишком высокой для реального продукта.

Квантизация — это перевод весов и иногда активаций модели в формат меньшей разрядности, чтобы сократить потребление памяти и ускорить инференс при минимальной потере качества. На практике чаще всего используют INT8, INT4 и FP8.

Например, подход GPTQ позволяет сжать модель до 4 бит с очень умеренной потерей качества. Для моделей масштаба OPT-175B и BLOOM-176B в статье про GPTQ показано, что:

  • квантование можно выполнить примерно за 4 GPU-часа на A100 80 GB,

  • при 4-битной квантизации потеря перплексии составляет не более 0.25 по сравнению с FP16,

  • кастомные ядра для умножения INT-матрица × FP-вектор дают ускорение до 3.25x на A100 и до 4.5x на A6000

Но здесь есть важная оговорка. Квантизация действительно хорошо подходит для production, если вы заранее проверили модель на собственных доменных сценариях. Снижение качества после квантизации может быть почти незаметным на общих бенчмарках и при этом болезненно проявляться именно в ваших задачах: хуже соблюдаются инструкции, теряются редкие термины, ломается structured output или падает качество работы с длинным контекстом.

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

GPTQ

GPTQ

Итог MVP

К моменту завершения первой версии архитектура выглядела так: два самописных бэкенда, Qdrant в качестве векторного хранилища, эмбеддер ai-sage/Giga-Embeddings-instruct для векторизации и Qwen2.5-72B-Instruct-GPTQ-Int8 на vLLM в качестве основной языковой модели. Ранжирование результатов поиска — через bi-encoder.

Система работала и отвечала на вопросы. Но у неё был один принципиальный изъян: на основе отобранных чанков топ-10 чанковых мы передавали в LLM полные тексты статей из Confluence откуда эти чанки были взяты, размером до 120 000 токенов. Логика была понятна — не хотелось ничего терять при разбивке на чанки. Однако на практике это приводило к трём проблемам:

  • высокая стоимость каждого запроса к модели,

  • заметная latency — пользователь ждал дольше, чем хотелось бы,

  • качество ответов не всегда оправдывало ожидания: при очень длинном контексте модель хуже фокусируется на нужном фрагменте.

Также огромной проблемой была сама LLM, приходилось брать квантованную версию одной из самых больших моделей в open-source сегменте на тот момент, поскольку иные модели не могли справиться с контекстом без галлюцинаций, а качественной работы с контекстом мы не могли себе позволить из-за отсутствия тестовых наборов данных.

MVP дал нам главное — работающий прототип и понимание узких мест. Стало ясно, что следующий шаг — не увеличивать контекст, а научиться передавать в модель только то, что действительно нужно для ответа. Так начался следующий этап — Advanced RAG.

Что дальше

Мы разобрали терминологию и прошли полный цикл создания MVP: от индексации документов до генерации ответов в Naive RAG. Это рабочий фундамент, но его потолок мы почувствовали быстро — качество поиска и генерации упиралось в ограничения подхода. Во второй части статьи покажу, как мы этот потолок пробивали:

  • Advanced RAG — реранжирование, гибридный поиск, продвинутый чанкинг и всё, что превращает наивный поиск в production-grade решение;

  • Modular RAG — переход от монолитного пайплайна к мультиагентной архитектуре, две итерации (детерминированный конвейер → ReAct-агент);

  • Оценка качества через RAGAS — как мы построили бенчмарк, сгенерировали тестовый датасет и измерили разницу между подходами (+7 п.п. к Overall);

Автор статьи: Константинов Александр — Старший AI-инженер, Лаборатории искусственного интеллекта «Честного знака»

ссылка на оригинал статьи https://habr.com/ru/articles/1037024/