- BrainTools - https://www.braintools.ru -
Надеюсь, все знают что такое RAG :) Для тех, кто не знает: это такая система, которая позволяет искать информацию и отвечать на вопросы по внутренней документации.
Архитектура RAG может быть как очень простой, так и весьма замысловатой. В самом простом виде она состоит из следующих компонентов:
Векторное хранилище — хранит документы в виде чанков – небольших фрагментов текста.
Ретривер — механизм поиска. Получает на вход искомую строку и ищет в векторном хранилище похожие на нее чанки (по косинусному сходству).
LLM — большая языковая модель, которая на основе найденных чанков формирует окончательный ответ.

Более сложные решения могут включать в себя реранкер, гибридный поиск и другие хитрые плюшки (на Хабре есть много статей [1] с подробным описанием RAG’а).
Но, даже самая навороченная архитектура не справится с некоторыми вопросами. Рассмотрим такой пример:
Какая была прибыль в компании Магнит за 2020
Тут ничего сложного. С таким вопросом справится даже RAG на одном семантическом поиске. Но если его “немного” усложнить:
Найди в годовых отчетах компании Магнит за последние 3 года упоминания ключевых рисков. Выдели, как менялась формулировка этих рисков от года к году.
Это уже не вопрос. Это целая задача. Причем аналитическая. Чтобы ее успешно решить, нужно разбить ее на более мелкие подзадачи:
Определить, какой сейчас год (допустим 2025).
Затем найти в БД информацию за каждый год:
Какие были ключевые риски в годовом отчете компании Магнит в 2022 году?
Какие были ключевые риски в годовом отчете компании Магнит в 2023 году?
Какие были ключевые риски в годовом отчете компании Магнит в 2024 году?
Проанализировать полученные ответы и выдать финальный ответ.
Обычный RAG ни на что подобное не способен. Для такой задачи нужен агентный RAG.
Чем же они отличается? Обычный RAG, хотя и может иметь некоторые ответвления, но это всегда прямолинейный последовательный конвейер: ретривер → реранкер → LLM.
В агентном раге нет никакого жестко заданного пайплайна. Есть агент (на базе LLM) и есть набор инструментов (один из которых — ретривер), к которым он может обращаться. И агент сам решает когда и какой инструмент вызвать для выполнения задачи. Он может вызвать один инструмент, а может все (причем в любой последовательности), а может вызвать один и тот же инструмент множество раз, если предыдущие результаты ему не понравились. В процессе работы над запросом агент накапливает историю всех вызовов. И в конце концов LLM выдает финальный ответ.

А сейчас попробуем реализовать игрушечный пример агентного RAG’а, который сможет ответить на такой вопрос:
Найди в годовых отчетах компании Магнит за последние 5 лет упоминания ключевых рисков. Выдели, как менялась формулировка этих рисков от года к году.
Легенда:
Шаг 4. LLM [5]
Шаг 5. Агент [6]
Вместо вывода [7]
Векторное хранилище необходимо чтобы хранить чанки и вектора на их основе. По этим векторам мы будем сопоставлять запрос пользователя и чанк. И тем самым находить и возвращать наиболее релевантные чанки.
В качестве векторного хранилища будем использовать хорошо зарекомендовавшую себя БД Qdrant:
1.1. Скачиваем docker-образ кудранта:
docker pull qdrant/qdrant
1.2. Запускаем контейнер:
docker run -p 6333:6333 -p 6334:6334
-v "$(pwd)/qdrant_storage:/qdrant/storage:z"
qdrant/qdrant
После этого web-интерфейс Qdrant будет доступен по адресу:
localhost:6333/dashboard
1.3. Создаем коллекцию, в которой будут хранится чанки документов:
from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams
# Создаем подключение
client = QdrantClient(url='http://localhost:6333')
# Создаем коллекцию
client.create_collection(
collection_name = 'rag_agent',
vectors_config = VectorParams(size=1536, distance=Distance.DOT),
)


Для экспериментов я накачал с сайта ИНИОН РАН [8] кучу разных отчетов за разные года и от разный компаний (и положил их все в одной папке).
Если взглянуть на них, то можно обнаружить что они имеют довольно сложную структуру: много колонок, много графиков, много таблиц и т.д. Я перепробовал пару десятков PDF-парсеров. В принципе с задачей справились три: Marker [9], olmOCR [10], Docling [11]. По внешнему виду мне больше всего понравился Marker – его и будем использовать
Для красивого оформления Marker вставляет много служебных символов (тире). Другие кандидаты — olmOCR, Docling — делают это проще. Например, с помощью HTML-тэгов. Поэтому с т.з. RAG может лучше подойдут другие два кандидата — надо тестировать.
2.1. Т.к. отчеты довольно длинные, а Marker работает очень небыстро, то мы предварительно распарсим все PDF файлы и сохраним их текстовое содержимое в TXT-файлах.
import os
import glob
from marker.converters.pdf import PdfConverter
from marker.models import create_model_dict
from marker.output import text_from_rendered
# Формируем PDF-парсер
converter = PdfConverter(artifact_dict=create_model_dict())
# Формируем список всех PDF файлов и путей до них
files = glob.glob(os.path.join('/docs', '*.pdf'))
# Проходимся по каждому файлу
for f in files:
full_file_name = f.split('/')[-1] # вытаскиваем название файла из пути
file_name = full_file_name.split('.')[0] # вытаскиваем название без расширения
# Парсим PDF файл
rendered = converter(f)
text, _, _ = text_from_rendered(rendered)
# Сохраняем файл
with open(f'/docs/{file_name}.txt', 'w', encoding='utf-8') as new_file:
new_file.write(text)
Здесь мы:
Создаем PDF-парсер.
Формируем список всех PDF файлов в указанной папке:
Вытаскиваем текст из PDF файла с помощью Marker’а.
Сохраняем текст в TXT-файле с тем же названием.
2.2. Теперь нам нужно перевести эти отчеты в вектор и загрузить в кудрант. Для получения эмбедингов из чанков мы будем использовать один из топовых (согласно MTEB [12]) энкодеров для русского языка — FRIDA [13].
Сначала скачайте его:
git lfs clone https://huggingface.co/ai-forever/FRIDA
2.3. Для загрузки документов в коллекцию выполните такой код:
import os
import glob
import uuid
from qdrant_client import QdrantClient
from qdrant_client.models import PointStruct
from sentence_transformers import SentenceTransformer
from langchain_text_splitters import RecursiveCharacterTextSplitter
# Создаем подключение к Qdrant
q_client = QdrantClient(url='http://localhost:6333')
# Подгружаем эмбедер
emb_model = SentenceTransformer('/models/FRIDA')
# Создаем сплиттер
text_splitter = RecursiveCharacterTextSplitter(
chunk_size = 1500,
chunk_overlap = 500,
separators = ['nn', 'n', ' ', ''])
# Формируем список всех TXT файлов и путей до них
files = glob.glob(os.path.join('/docs', '*.txt'))
# Проходимся по каждому файлу
for f in files:
print(f)
file_name = f.split('/')[-1] # вытаскиваем название файла из пути
# Подгружаем файл
text = open(f, 'r').read()
# Разбиваем текст на чанки
chunks = text_splitter.split_text(text)
# Проходимся по каждому чанку
for chunk_text in chunks:
# Добавляем к чанку название файла
chunk_text = f'Файл: {file_name}n{chunk_text}'
# Формируем структуру для Qdrant
point = PointStruct(
id = str(uuid.uuid4()),
vector = emb_model.encode(chunk_text, prompt_name='search_document'),
payload = {'file': file_name, 'chunk': chunk_text})
# Отправляем в Qdrant
_ = qclient.upsert(collection_name = 'rag_agent', points = [point], wait = True)
Тут мы:
Создаем:
Подключение к Qdrant.
С помощью SentenceTransformer загружаем эмбедер.
Сплитер.
Вытаскиваем все TXT файлы из папки и проходимся по каждому из них:
Считываем текст из файла.
Разбиваем текст на чанки (по 1500 на чанк с нахлестом в 500).
Проходимся по всем чанкам:
Формируем объект, который содержит:
Уникальный идентификатор
Вектор — эмбединг который выдала нам FRIDA на основе текста чанка.
Название файла
Текст чанка.
Отправляем чанк в кудрант.
З.Ы.1. Обратите внимание [14], что мы в каждый чанк добавляем название файла. Если название файла будет содержательным, то это добавит общий контекст происходящего к каждому чанку. Эта техника называется Contextual Retrieval.
З.Ы.2. При работе с FRIDA для качественного перевода текста в вектора нужно использовать правильные префиксы: search_query, search_document, paraphrase, categorize, categorize_sentiment, categorize_topic, categorize_entailment. Почитайте в документации как это нужно делать: HF [13], Хабр [15].

Теперь нам нужно создать MCP сервер. MCP сервер дает LLM информацию, какие инструменты ей доступны, а также выступает как прокси для вызова этих самых инструментов. Чуть более подробно (и с примером) про MCP-сервера можете почитать в моей статье: Разработка MCP-сервера на примере CRUD операций [16].
Создайте файл python3 mcp_server.py [17]:
from datetime import date
from fastmcp import FastMCP
from qdrant_client import QdrantClient
from sentence_transformers import SentenceTransformer
# Инициализация MCP сервера
mcp = FastMCP('Employee Management System')
# Создаем подключение
q_client = QdrantClient(url='http://localhost:6333')
# Подгружаем эмбедер
emb_model = SentenceTransformer('/models/FRIDA')
# Получить текущую дату
@mcp.tool()
def get_current_date():
'''Возвращает текущую дату'''
return str(date.today())
# Поиск по чанкам
@mcp.tool()
def chunks_search(query):
'''
Ищет релевантные фрагменты текста в векторной базе данных.
Возвращает список наиболее релевантных чанков объединенных в один текст.
'''
search_result = q_client.query_points(
collection_name = 'rag_agent',
query = emb_model.encode(query, prompt_name='search_query'),
with_payload = True,
limit = 10
).points
chunks = [s.payload['chunk'] for s in search_result]
chunks = 'nn'.join(chunks)
return chunks
if __name__ == '__main__':
# Запуск сервера
mcp.run(transport='http', host='192.168.0.108', port=9000)
Здесь мы:
Инициируем сервер.
Создаем подключение к кудрант.
Подгружаем фриду — она нам понадобится для получения эмбединов из искомого запроса.
Объявляем две функции:
get_current_date – возвращает текущую дату.
chunks_search – выполняет поиск чанков на основе входящей строки. Найденные чанки объединяются в одну длинную строку.
Запускаем сервер на 9000 порту.
Запускаем сервер в терминале:
python3 mcp_server.py
Теперь сервис доступен по адресу:
http://192.168.0.108:9000/mcp
LLM это мозг [18] нашего агента. Она обрабатывает информацию и определяет какие инструменты вызвать. Но чтобы использовать LLM в качестве агента она должна обладать одной важной функцией — Function Calling (или Tool Calling). Не многие локальные LLM да еще и небольшого размера могут похвастаться таким функционалом. Одна из них — Qwen3 14B — ее и будем использовать.
4.1. Скачаем LLM:
git lfs clone https://huggingface.co/Qwen/Qwen3-14B
4.2. Сначала скачаем докер-образ vLLM:
docker pull vllm/vllm-openai:v0.10.1.1
4.3. Для запуска модели выполните в терминале примерно такую команду:
docker run
--gpus all
-v /models/qwen/Qwen3-14B/:/Qwen3-14B/
-p 8000:8000
--env "TRANSFORMERS_OFFLINE=1"
--env "HF_DATASET_OFFLINE=1"
--ipc=host
--name vllm
vllm/vllm-openai:v0.10.1.1
--model="/Qwen3-14B"
--tensor-parallel-size 2
--max-model-len 40960
--enable-auto-tool-choice
--tool-call-parser hermes
--reasoning-parser deepseek_r1
Теперь наша модель доступна как сервис по адресу: http://localhost:8000 [19]
Более подробно, как в Qwen можно вызывать инструменты можно почитать в официальной документации [20].
Ну вот мы и добрались до агента :) Запилить его можно и на чистом питоне, но это довольно громоздкая махина, а изобретать велосипед не хочется. Поэтому мы воспользуемся готовым фреймворком — Agno [21]. Это относительно новая библиотека. Она неплохо себя показала в работе, еще не успела обрасти ненужным функционалом как некоторые ее коллеги и обладает приличной документацией (что примечательно со своим AI-ассистентом для ответов на вопросы по этой самой документации).
from agno.agent import Agent
from agno.models.vllm import VLLM
from agno.db.sqlite import SqliteDb
from agno.tools.mcp import MCPTools
from agno.utils.pprint import pprint_run_response
mcp_tools = MCPTools(transport='streamable-http', url='http://192.168.0.108:9000/mcp')
await mcp_tools.connect()
instruction = '''Ты - интеллектуальный ассистент. Твоя задача - отвечать на вопросы пользователей на основе предоставленных документов.
Для обработки запроса тебе доступны два инструмента:
getcurrent_date - возвращает текущую дату.
chunkssearch - выполняет поиск по корпоративной документации.
В корпоративной базе данных хранятся различные отчеты.
Например: "Газпром, Годовой отчет, 2021", "ЛУКОЙЛ, Финансовый отчет по РСБУ, 2020", "X5 Group, Отчет устойчивого развития, 2018".
Информация в корпоративной базе данных разбита на чанки. Каждый чанк содержит название отчета и кусок текста.
Для семантического поиска по чанкам используется инструмент chunkssearch.
Инструкция:
1. Сначала проанализируй запрос и определяйте необходимые подзадачи.
2. Используйте поиск для нахождения релевантной информации.
3. Если необходимо найти информацию из разных периодов, то ищи их с помощью самостоятельных подзапросов.
4. Всегда используй функцию getcurrent_date, если необходимо определить текущую дату.
5. Отвечай на основе найденной информации, не придумывай факты. Если информации недостаточно, укажи это в ответе.
'''
agent = Agent(
model = VLLM(id='/Qwen3-14B', base_url='http://192.168.0.108:8000/v1'),
db = SqliteDb(db_file='/rag_agent/agno.db'),
tools = [mcp_tools], #[current_date,chunks_search]
instructions = instruction
)
query = '''Найди в годовых отчетах компании Магнит за последние 5 лет упоминания ключевых рисков.
Выдели, как менялась формулировка этих рисков от года к году.'''
result = await agent.arun(query, debug_mode=True)
pprint_run_response(result, markdown=True)
Здесь мы:
Подключаемся к MCP серверу и запрашиваем доступные инструменты.
Описываем инструкцию для агента.
Объявляем агента, которому передаем:
Способ подключения к LLM.
Способ хранения истории запросов.
Все доступные инструменты.
Инструкцию.
Выполняем запрос через агента и распечатываем результат.
Если посмотреть на лог вызовов, то можно увидеть, что сначала LLM запросила текущую дату.

А затем выполнила 5 вызовов, в которых извлекла

А финальный ответ получился таким:
Анализ ключевых рисков в годовых отчетах «Магнита» за 2020–2024 гг.
───────────────────────────────────────────────────────2020 год:
Основные риски:
– Риск ликвидности — контроль за резервами, банковскими операциями и денежными потоками.
– Пандемия COVID-19 — изменение потребительского поведения [22], нормативного регулирования и прерывание цепочек поставок.
– Экономическая нестабильность — влияние санкций и геополитических факторов (упомянуты в контексте прогнозов).Формулировка: Акцент на операционную устойчивость, особенно в условиях пандемии. Риски представлены как внешние (пандемия, санкции) и внутренние (ликвидность).
───────────────────────────────────────────────────────
2021 год:
Основные риски:
– Инфляция и рост цен на сырье — давление на потребителей и затраты.
– Геополитическая напряженность — рост волатильности на финансовых рынках и девальвация рубля (с февраля 2022 г.).
– Цифровая трансформация — необходимость адаптации к изменяющимся технологическим требованиям.
– Управление оборотным капиталом — оптимизация запасов и ликвидности.Формулировка: Расширение списка рисков до макроэкономических и технологических. Появление ESG-аспектов (устойчивое развитие, экологические и социальные обязательства).
───────────────────────────────────────────────────────
2022 год:
Основные риски:
– Санкционное давление — ограничения на международные операции и доступ к финансовым ресурсам.
– Импортозамещение — необходимость перестройки поставок и локализации производства.
– Долговая нагрузка — контроль за уровнем задолженности и дивидендными выплатами.Формулировка: Акцент на геополитические и финансовые риски, связанные с санкциями. Упоминается стратегическое реорганизование (например, приобретение «Дикси»).
───────────────────────────────────────────────────────
2023 год:
Основные риски:
– Количественная оценка рисков — внедрение систематического подхода к анализу всех бизнес-процессов.
– Экологические и социальные риски (ESG): сокращение выбросов, расхода воды, пищевых отходов.
– Развитие ответственных источников продукции — сотрудничество с местными поставщиками.
– Цифровизация — переход на ERP-системы и электронную торговлю.Формулировка: Переход от реактивного управления к проактивному. Риски структурированы по категориям (экологические, операционные, стратегические), с акцентом на устойчивое развитие.
───────────────────────────────────────────────────────
2024 год:
Основные риски:
– Интеграция новых структур — риски, связанные с приобретениями и расширением бизнеса.
– Обучение [23] сотрудников — развитие компетенций в управлении рисками.
– Цифровая трансформация — внедрение ИТ-решений для поставщиков и клиентов.
– Долгосрочные обязательства — выполнение целей по снижению долговой нагрузки и росту EBITDA.Формулировка: Упор на стратегическую подготовленность и инновации. Риски рассматриваются как возможность для развития (например, цифровизация и ESG).
───────────────────────────────────────────────────────
Итог: эволюция [24] формулировок
– 2020–2021: Акцент на внешние шоки (пандемия, санкции) и операционную устойчивость.
– 2022–2023: Расширение до ESG-рисков и цифровизации, акцент на долгосрочные стратегии.
– 2024: Формулировки становятся более систематизированными, риски рассматриваются как инструменты для роста, а не только угрозы.
Мы рассмотрели относительно простую реализацию. Но агенты несколько сложнее: у них есть сессии, есть состояния, есть хранилища и много чего другого. Еще можно реализовать кучу агентов, каждый из которых выполняет свою задачу и они взаимодействуют между собой. Но это уже на самостоятельное изучение :)
Из улучшений, которые так и напрашиваются после реализации этого игрушечного примера:
Гибридный поиск (BM25 + семантика).
Метаданные и фильтры для чанков.
Подобрать гиперпараметры для вызова LLM.
Что касается инструментов. В данном пример у нас их всего два инструмента. И даже при проектировании этого уже кажется недостаточно. Возьмем такой пример:
Сравни доходы трех самых крупных компаний за прошлый год.
Он очень похож на уже рассмотренный нами пример. Нам также нужно: определитель текущую дату и выполнить поиск по чанкам. Но очевидно здесь нужен еще один инструмент – какая-то табличка, в которой хранится статистическая информация по доходам компаний, чтобы вернуть три с самым большим доходом.
И нигде нет конечного списка инструментов, которые вам могут понадобится. Нужно самим мониторить запросы пользователей и смотреть что им нужно.
З.Ы. Современные агентные библиотеки уже включают в себя готовые инструменты для многих популярных сервисов.
Из недостатков агентного рага:
Мониторинг и анализ работоспособности требует больше усилий, чем обычный RAG, поскольку генерируется куда больше информации.
Тратится гораздо больше токенов (и не всегда с пользой). Если у вас LLM платная, то это может стать проблемой.
Время выполнения значительно дольше. И что самое плохое, всю эту простыню выполнения нельзя вывести пользователю в режиме стрима, поскольку там много служебной информации. Так что пользователю остается только ждать.
Мои курсы: Разработка LLM с нуля [25] | Алгоритмы Машинного обучения с нуля [26]
Автор: slivka_83
Источник [27]
Сайт-источник BrainTools: https://www.braintools.ru
Путь до страницы источника: https://www.braintools.ru/article/21982
URLs in this post:
[1] много статей: https://habr.com/ru/search/?q=RAG&target_type=posts&order=relevance
[2] Шаг 1. Векторное хранилище: #1
[3] Шаг 2. Загрузка документов: #2
[4] Шаг 3. MCP-сервер: #3
[5] Шаг 4. LLM: #4
[6] Шаг 5. Агент: #5
[7] Вместо вывода: #6
[8] ИНИОН РАН: https://reportcollection.inion.ru/reports
[9] Marker: https://github.com/datalab-to/marker
[10] olmOCR: https://github.com/allenai/olmocr
[11] Docling: https://github.com/docling-project/docling
[12] согласно MTEB: https://huggingface.co/spaces/mteb/leaderboard
[13] FRIDA: https://huggingface.co/ai-forever/FRIDA
[14] внимание: http://www.braintools.ru/article/7595
[15] Хабр: https://habr.com/ru/companies/sberdevices/articles/909924/
[16] Разработка MCP-сервера на примере CRUD операций: https://habr.com/ru/articles/957836/
[17] server.py: http://server.py
[18] мозг: http://www.braintools.ru/parts-of-the-brain
[19] http://localhost:8000: http://localhost:8000
[20] официальной документации: https://qwen.readthedocs.io/en/latest/framework/function_call.html
[21] Agno: https://docs.agno.com/introduction
[22] поведения: http://www.braintools.ru/article/9372
[23] Обучение: http://www.braintools.ru/article/5125
[24] эволюция: http://www.braintools.ru/article/7702
[25] Разработка LLM с нуля: https://stepik.org/a/231306/pay?promo=5e79340ae02bce0d
[26] Алгоритмы Машинного обучения с нуля: https://stepik.org/a/68260/pay?promo=b997c468b105096d
[27] Источник: https://habr.com/ru/articles/966966/?utm_source=habrahabr&utm_medium=rss&utm_campaign=966966
Нажмите здесь для печати.