Image

Пример реализации агентного RAG’а

Надеюсь, все знают что такое RAG 🙂 Для тех, кто не знает: это такая система, которая позволяет искать информацию и отвечать на вопросы по внутренней документации.

Архитектура RAG может быть как очень простой, так и весьма замысловатой. В самом простом виде она состоит из следующих компонентов:

  • Векторное хранилище — хранит документы в виде чанков — небольших фрагментов текста.

  • Ретривер — механизм поиска. Получает на вход искомую строку и ищет в векторном хранилище похожие на нее чанки (по косинусному сходству).

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

0c8c5f3b4f72e2fd97b2d4af71ffc1ad

Более сложные решения могут включать в себя реранкер, гибридный поиск и другие хитрые плюшки (на Хабре есть много статей с подробным описанием RAG’а).

Но, даже самая навороченная архитектура не справится с некоторыми вопросами. Рассмотрим такой пример:

Какая была прибыль в компании Магнит за 2020

Тут ничего сложного. С таким вопросом справится даже RAG на одном семантическом поиске. Но если его “немного” усложнить:

Найди в годовых отчетах компании Магнит за последние 3 года упоминания ключевых рисков. Выдели, как менялась формулировка этих рисков от года к году.

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

  1. Определить, какой сейчас год (допустим 2025).

  2. Затем найти в БД информацию за каждый год:

    1. Какие были ключевые риски в годовом отчете компании Магнит в 2022 году?

    2. Какие были ключевые риски в годовом отчете компании Магнит в 2023 году?

    3. Какие были ключевые риски в годовом отчете компании Магнит в 2024 году?

  3. Проанализировать полученные ответы и выдать финальный ответ.

Обычный RAG ни на что подобное не способен. Для такой задачи нужен агентный RAG.

Чем же они отличается? Обычный RAG, хотя и может иметь некоторые ответвления, но это всегда прямолинейный последовательный конвейер: ретривер → реранкер → LLM.

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

83f398257177c02e7cc569aa715e6867

А сейчас попробуем реализовать игрушечный пример агентного RAG’а, который сможет ответить на такой вопрос:

Найди в годовых отчетах компании Магнит за последние 5 лет упоминания ключевых рисков. Выдели, как менялась формулировка этих рисков от года к году.

Легенда:

  • Шаг 1. Векторное хранилище

  • Шаг 2. Загрузка документов

  • Шаг 3. MCP-сервер

  • Шаг 4. LLM

  • Шаг 5. Агент

  • Вместо вывода

Реализация

Шаг 1. Векторное хранилище

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

В качестве векторного хранилища будем использовать хорошо зарекомендовавшую себя БД 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), )

79bd25cd9f0d70030c29591474eb1fe1

Шаг 2. Загрузка документов

00384ff303711774d3a7c12a0c197382

Для экспериментов я накачал с сайта ИНИОН РАН кучу разных отчетов за разные года и от разный компаний (и положил их все в одной папке).

Если взглянуть на них, то можно обнаружить что они имеют довольно сложную структуру: много колонок, много графиков, много таблиц и т.д. Я перепробовал пару десятков PDF-парсеров. В принципе с задачей справились три: Marker, olmOCR, Docling. По внешнему виду мне больше всего понравился 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) энкодеров для русского языка — FRIDA.

Сначала скачайте его:

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. Обратите внимание, что мы в каждый чанк добавляем название файла. Если название файла будет содержательным, то это добавит общий контекст происходящего к каждому чанку. Эта техника называется Contextual Retrieval.

З.Ы.2. При работе с FRIDA для качественного перевода текста в вектора нужно использовать правильные префиксы: search_query, search_document, paraphrase, categorize, categorize_sentiment, categorize_topic, categorize_entailment. Почитайте в документации как это нужно делать: HF, Хабр.

959c32e76082a1ffe7d2108837269345

Шаг 3. MCP-сервер

Теперь нам нужно создать MCP сервер. MCP сервер дает LLM информацию, какие инструменты ей доступны, а также выступает как прокси для вызова этих самых инструментов. Чуть более подробно (и с примером) про MCP-сервера можете почитать в моей статье: Разработка MCP-сервера на примере CRUD операций.

Создайте файл python3 mcp_server.py:

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

Шаг 4. LLM

LLM это мозг нашего агента. Она обрабатывает информацию и определяет какие инструменты вызвать. Но чтобы использовать 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

Более подробно, как в Qwen можно вызывать инструменты можно почитать в официальной документации.

Шаг 5. Агент

Ну вот мы и добрались до агента 🙂 Запилить его можно и на чистом питоне, но это довольно громоздкая махина, а изобретать велосипед не хочется. Поэтому мы воспользуемся готовым фреймворком — Agno. Это относительно новая библиотека. Она неплохо себя показала в работе, еще не успела обрасти ненужным функционалом как некоторые ее коллеги и обладает приличной документацией (что примечательно со своим 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 запросила текущую дату.

cc8d35240f0a28c569ed7b4422ad70f1

А затем выполнила 5 вызовов к ретриверу, в которых извлекла информацию по годовым отчетам за каждый из 5 лет.

56ab56bb9d87b00ae3907e2470e258e7

Обработала всю полученную информацию и выдала вот такой финальный ответ:

Анализ ключевых рисков в годовых отчетах «Магнита» за 2020–2024 гг.
───────────────────────────────────────────────────────

2020 год:

Основные риски:

— Риск ликвидности — контроль за резервами, банковскими операциями и денежными потоками.
— Пандемия COVID-19 — изменение потребительского поведения, нормативного регулирования и прерывание цепочек поставок.
— Экономическая нестабильность — влияние санкций и геополитических факторов (упомянуты в контексте прогнозов).

Формулировка: Акцент на операционную устойчивость, особенно в условиях пандемии. Риски представлены как внешние (пандемия, санкции) и внутренние (ликвидность).

───────────────────────────────────────────────────────

2021 год:

Основные риски:

— Инфляция и рост цен на сырье — давление на потребителей и затраты.
— Геополитическая напряженность — рост волатильности на финансовых рынках и девальвация рубля (с февраля 2022 г.).
— Цифровая трансформация — необходимость адаптации к изменяющимся технологическим требованиям.
— Управление оборотным капиталом — оптимизация запасов и ликвидности.

Формулировка: Расширение списка рисков до макроэкономических и технологических. Появление ESG-аспектов (устойчивое развитие, экологические и социальные обязательства).

───────────────────────────────────────────────────────

2022 год:

Основные риски:

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

Формулировка: Акцент на геополитические и финансовые риски, связанные с санкциями. Упоминается стратегическое реорганизование (например, приобретение «Дикси»).

───────────────────────────────────────────────────────

2023 год:

Основные риски:

— Количественная оценка рисков — внедрение систематического подхода к анализу всех бизнес-процессов.
— Экологические и социальные риски (ESG): сокращение выбросов, расхода воды, пищевых отходов.
— Развитие ответственных источников продукции — сотрудничество с местными поставщиками.
— Цифровизация — переход на ERP-системы и электронную торговлю.

Формулировка: Переход от реактивного управления к проактивному. Риски структурированы по категориям (экологические, операционные, стратегические), с акцентом на устойчивое развитие.

───────────────────────────────────────────────────────

2024 год:

Основные риски:

— Интеграция новых структур — риски, связанные с приобретениями и расширением бизнеса.
— Обучение сотрудников — развитие компетенций в управлении рисками.
— Цифровая трансформация — внедрение ИТ-решений для поставщиков и клиентов.
— Долгосрочные обязательства — выполнение целей по снижению долговой нагрузки и росту EBITDA.

Формулировка: Упор на стратегическую подготовленность и инновации. Риски рассматриваются как возможность для развития (например, цифровизация и ESG).

───────────────────────────────────────────────────────

Итог: эволюция формулировок

— 2020–2021: Акцент на внешние шоки (пандемия, санкции) и операционную устойчивость.
— 2022–2023: Расширение до ESG-рисков и цифровизации, акцент на долгосрочные стратегии.
— 2024: Формулировки становятся более систематизированными, риски рассматриваются как инструменты для роста, а не только угрозы.

Вместо вывода

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

Из улучшений, которые так и напрашиваются после реализации этого игрушечного примера:

  • Гибридный поиск (BM25 + семантика).

  • Метаданные и фильтры для чанков.

  • Подобрать гиперпараметры для вызова LLM.

Что касается инструментов. В данном пример у нас их всего два инструмента. Но даже сейчас этого кажется уже недостаточно. Возьмем такой пример:

Сравни доходы трех самых крупных компаний за прошлый год.

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

И нигде нет конечного списка инструментов, которые вам могут понадобится. Нужно самим мониторить запросы пользователей и смотреть что им нужно.
З.Ы. Современные агентные библиотеки уже включают в себя готовые инструменты для многих популярных сервисов.

Из недостатков агентного рага:

  • Мониторинг и анализ работоспособности требует больше усилий, чем обычный RAG, поскольку генерируется куда больше информации.

  • Тратится гораздо больше токенов (и не всегда с пользой). Если у вас LLM платная, то это может стать проблемой.

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

Мои курсы: Разработка LLM с нуля | Алгоритмы Машинного обучения с нуля

Источник: habr.com

✅ Найденные теги: новости, Пример

ОСТАВЬТЕ СВОЙ КОММЕНТАРИЙ

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Каталог бесплатных опенсорс-решений, которые можно развернуть локально и забыть о подписках

галерея

Фото сгенерированных лиц: исследование показывает, что люди не могут отличить настоящие лица от сгенерированных
Нейросети построили капитализм за трое суток: 100 агентов Claude заперли…
Скетч: цифровой осьминог и виртуальный мир внутри компьютера с человечком.
Сцена с жестами пальцами, где один жест символизирует "VPN", а другой "KHP".
‼️Paramount купила Warner Bros. Discovery — сумма сделки составила безумные…
Скриншот репозитория GitHub "Claude Scientific Skills" AI для научных исследований.
Структура эффективного запроса Claude с элементами задачи, контекста и референса.
Эскиз и готовая веб-страница платформы для AI-дизайна в современном темном режиме.
ideipro logotyp
Image Not Found
Звёздное небо с галактиками и туманностями, космос, Вселенная, астрофотография.

Система оповещения обсерватории Рубина отправила 800 000 сигналов в первую ночь наблюдений.

Астрономы будут получать оповещения о небесных явлениях в течение нескольких минут после их обнаружения. Теренс О'Брайен, редактор раздела «Выходные». Публикации этого автора будут добавляться в вашу ежедневную рассылку по электронной почте и в ленту новостей на главной…

Мар 2, 2026
Женщина с длинными тёмными волосами в синем свете, нейтральный фон.

Расследование в отношении 61-фунтовой машины, которая «пожирает» пластик и выплевывает кирпичи.

Обзор компактного пресса для мягкого пластика Clear Drop — и что будет дальше. Шон Холлистер, старший редактор Публикации этого автора будут добавляться в вашу ежедневную рассылку по электронной почте и в ленту новостей на главной странице вашего…

Мар 2, 2026
Черный углеродное волокно с текстурой плетения, отражающий свет.

Материал будущего: как работает «бессмертный» композит

Учёные из Университета штата Северная Каролина представили композит нового поколения, способный самостоятельно восстанавливаться после серьёзных повреждений.  Речь идёт о модифицированном армированном волокном полимере (FRP), который не просто сохраняет прочность при малом весе, но и способен «залечивать» внутренние…

Мар 2, 2026
Круглый экран с изображением замка и горы, рядом электронная плата.

Круглый дисплей Waveshare для креативных проектов

Круглый 7-дюймовый сенсорный дисплей от Waveshare создан для разработчиков и дизайнеров, которым нужен нестандартный экран.  Это IPS-панель с разрешением 1 080×1 080 пикселей, поддержкой 10-точечного ёмкостного сенсора, оптической склейкой и защитным закалённым стеклом, выполненная в круглом форм-факторе.…

Мар 2, 2026

Впишите свой почтовый адрес и мы будем присылать вам на почту самые свежие новости в числе самых первых