RAG-агент для PDF с цитатами: LlamaIndex, LlamaParse и Ollama — пошаговое руководство 2026

Вы загружаете в нейросеть отчёт на 40 страниц и спрашиваете: «Что изменилось по сравнению с прошлым кварталом?». Через несколько секунд приходит уверенный ответ с процентами. Звучит правдоподобно — но откуда взялась эта цифра? Из таблицы на 18-й странице, из подписи к графику или модель её просто «додумала»?

Содержание
RAG-агент для PDF с цитатами решает эту проблему. В этой статье разберём, как собрать RAG-агента для работы с PDF, который отвечает на вопросы по длинным документам, ставит в ответе нумерованные цитаты [1], [2] и показывает исходный текст и страницу за каждой ссылкой. Стек: LlamaParse для разбора PDF, LlamaIndex для индекса и поиска, Ollama для локальных эмбеддингов и LLM. Статья основана на открытом проекте Grounded Document Agent инженера Sumanth P (ссылки — в конце).
Почему RAG по PDF так часто ошибается
Для человека PDF выглядит структурированным: заголовки, колонки, таблицы, сноски, графики, понятный порядок чтения. Но обычное извлечение текста почти ничего из этого не сохраняет:
- статья в две колонки читается «поперёк» обеих колонок;
- финансовая таблица превращается в поток чисел без привязки к столбцам;
- от графика остаётся только подпись;
- в сканированных страницах вообще нет машиночитаемого текста.
Главная мысль проекта: многие ошибки, которые выглядят как проблемы поиска (retrieval), на самом деле возникают раньше — на этапе парсинга. Если таблица разобрана неправильно, никакие эмбеддинги не восстановят, какое значение к какому столбцу относилось. Если порядок чтения нарушен, большое контекстное окно просто передаст модели больше перепутанного текста.
Качество ответа ограничено качеством парсинга. Поэтому в проекте для разбора документов используется LlamaParse — облачный парсер от LlamaIndex, который превращает PDF в Markdown с учётом вёрстки и сохраняет метаданные страниц. Для сравнения парсеров LlamaIndex также опубликовал открытый бенчмарк ParseBench: он проверяет таблицы, графики, точность содержимого, форматирование и визуальную привязку.
Как устроен RAG-агент для PDF: парсинг в облаке, всё остальное локально
Схема проекта простая:
- Загрузка PDF. LlamaParse разбирает документ в Markdown с разбиением по страницам.
- Кеширование. Приложение считает хеш файла: если этот документ уже открывали, результат берётся из кеша, и кредиты LlamaParse повторно не тратятся.
- Эмбеддинги. Текст переводится в векторы локальной моделью nomic-embed-text через Ollama.
- Индекс. Векторы хранятся в VectorStoreIndex из LlamaIndex и сохраняются на диск, чтобы не пересобирать индекс при каждом запуске.
- Поиск. На вопрос пользователя находятся 4 самых релевантных фрагмента.
- Ответ с цитатами. CitationQueryEngine делит найденный контекст на пронумерованные источники, а локальная модель qwen3:4b-instruct в Ollama пишет ответ со ссылками [1], [2] и т.д.
В облако уходит только сам PDF на этапе парсинга. Вопросы пользователя, эмбеддинги, индекс и генерация ответов остаются на вашем компьютере. Это логичное разделение: именно разбор сложной вёрстки требует специализированных моделей, а дальше хватает локального стека. Бонус — дешёвая разработка: документ разбирается один раз, а вы сколько угодно экспериментируете с поиском, промптами и моделями.
Установка: что понадобится
- Python 3.10 или новее;
- Ollama, установленная и запущенная локально;
- API-ключ LlamaCloud для LlamaParse.
pip install llama-index llama-parse llama-index-embeddings-ollama llama-index-llms-ollama
ollama pull nomic-embed-text
ollama pull qwen3:4b-instruct
export LLAMA_CLOUD_API_KEY="ваш_ключ"Шаг 1. Парсинг PDF через LlamaParse
Интеграция с LlamaParse занимает несколько строк:
import os
from llama_parse import LlamaParse
parser = LlamaParse(
api_key=os.environ["LLAMA_CLOUD_API_KEY"],
result_type="markdown",
verbose=True,
)
documents = parser.load_data("report.pdf")Важен не только Markdown. Каждый документ на выходе сохраняет метаданные страницы, и они «путешествуют» вместе с текстом через индексацию и поиск. Без них приложение могло бы показать найденный фрагмент, но не смогло бы надёжно сказать, с какой страницы исходного PDF он взят.
Шаг 2. Индекс с кешем по хешу файла
Отправлять модели весь документ на 40–100 страниц для каждого вопроса бессмысленно: ответ обычно лежит в одной таблице или паре абзацев. Поэтому текст превращается в эмбеддинги и сохраняется в векторный индекс. Упрощённый вариант с кешированием:
import hashlib
from pathlib import Path
from llama_index.core import (
Settings,
StorageContext,
VectorStoreIndex,
load_index_from_storage,
)
from llama_index.embeddings.ollama import OllamaEmbedding
from llama_index.llms.ollama import Ollama
Settings.embed_model = OllamaEmbedding(model_name="nomic-embed-text")
Settings.llm = Ollama(model="qwen3:4b-instruct", request_timeout=120.0)
def file_hash(path: str) -> str:
return hashlib.sha256(Path(path).read_bytes()).hexdigest()[:16]
def build_or_load_index(pdf_path: str) -> VectorStoreIndex:
persist_dir = Path("storage") / file_hash(pdf_path)
if persist_dir.exists(): # документ уже обрабатывали
storage = StorageContext.from_defaults(persist_dir=str(persist_dir))
return load_index_from_storage(storage)
documents = parser.load_data(pdf_path) # платный шаг — только один раз
index = VectorStoreIndex.from_documents(documents)
index.storage_context.persist(persist_dir=str(persist_dir))
return indexВ оригинальном проекте кешируется сам результат парсинга, а индекс сохраняется отдельно — идея та же: один и тот же PDF никогда не отправляется в LlamaParse дважды.
Шаг 3. Ответы с цитатами через CitationQueryEngine
Слой запросов тоже компактный. CitationQueryEngine берёт найденный контекст, режет его на небольшие фрагменты, нумерует их и просит модель ссылаться на источники прямо в тексте ответа:
from llama_index.core.query_engine import CitationQueryEngine
index = build_or_load_index("report.pdf")
engine = CitationQueryEngine.from_args(
index,
similarity_top_k=4, # сколько релевантных фрагментов искать
citation_chunk_size=512, # размер одного пронумерованного источника
)
response = engine.query("Что изменилось по сравнению с прошлым кварталом?")
print(response)
for i, src in enumerate(response.source_nodes, start=1):
print(f"\n[{i}] релевантность: {src.score:.2f}")
print("метаданные:", src.node.metadata) # здесь есть информация о странице
print(src.node.get_content()[:300])Два ключевых параметра:
- similarity_top_k — сколько релевантных фрагментов достаётся из индекса (в проекте по умолчанию 4);
- citation_chunk_size — насколько мелко найденный контекст делится на источники (в проекте 512).
Как выбрать размер фрагмента для цитат
Здесь нет универсального значения:
| Размер фрагмента | Плюсы | Минусы |
|---|---|---|
| Маленький | цитату легко проверить, ссылка точная | контекст дробится, модели сложнее рассуждать |
| Большой | сохраняется окружающий контекст | цитата менее точная, проверять дольше |
Подбирайте значение под свои документы и модель: для договоров с короткими пунктами подойдут мелкие фрагменты, для научных статей с длинными рассуждениями — более крупные.
Цитата хороша настолько, насколько хороши доказательства
Это, пожалуй, самый важный вывод проекта. Представьте, что ответ лежит в финансовой таблице, а слабый парсер извлёк все числа, но приписал одно из них не тому столбцу. Поиск всё равно найдёт эту страницу, модель уверенно ответит, и система даже поставит правильную ссылку на страницу. Но ответ будет неверным.
Цитата не исправляет ошибку — она лишь показывает, откуда взяты данные. Поэтому интерфейс проекта показывает за каждым ответом:
- номер страницы;
- найденный текст фрагмента;
- подсвеченные слова из запроса;
- оценку релевантности.
Модель всё ещё может неправильно понять хорошие данные, а поиск — вернуть не тот раздел. Разница в том, что ошибка становится видимой: открыв цитату, вы поймёте, где сбой — в парсинге, поиске или генерации, — а не будете принимать уверенный ответ на веру.
Где пригодится RAG-агент для PDF
- годовые и квартальные финансовые отчёты;
- научные статьи и обзоры;
- договоры и юридические документы;
- технические руководства и документация;
- внутренние регламенты компании — с данными, которые нельзя отправлять во внешние LLM (в облако уходит только этап парсинга, а при необходимости его можно заменить локальным парсером).
Как улучшить RAG-агент для PDF
- Открывать PDF на нужной странице при клике на цитату.
- Отказываться отвечать, если найденные фрагменты слишком слабо связаны с вопросом (порог по score).
- Заменить модель qwen3:4b на более крупную, если позволяет железо.
- Добавить реранкер после поиска, чтобы точнее отбирать фрагменты.
- Сделать гибридный поиск: векторы плюс BM25 по ключевым словам — это помогает с номерами, артикулами и точными терминами.
Частые ошибки при создании RAG по документам
- Чинить поиск, когда сломан парсинг. Сначала посмотрите глазами, что получилось после извлечения текста.
- Терять метаданные страниц. Без них невозможно показать пользователю источник.
- Повторно парсить один и тот же файл. Кешируйте результат по хешу — это экономит деньги и время.
- Отправлять в модель весь документ. Дорого, медленно и не повышает точность.
- Доверять цитатам вслепую. Ссылка показывает источник, но не гарантирует правильность ответа.
Частые вопросы
Что такое RAG?
RAG (Retrieval-Augmented Generation) — подход, при котором модель перед ответом получает релевантные фрагменты из ваших документов и отвечает на их основе, а не только по своим знаниям.
Можно ли сделать всё полностью локально?
Да, если заменить LlamaParse локальным парсером. В проекте облако используется только для разбора PDF, потому что качество парсинга сложных таблиц и сканов сильнее всего влияет на результат.
Какая видеокарта нужна для qwen3:4b?
Небольшие модели размером около 4 млрд параметров в квантованном виде запускаются на обычном ноутбуке и даже на CPU, хотя с GPU ответы генерируются заметно быстрее.
Чем CitationQueryEngine отличается от обычного query engine?
Обычный движок просто генерирует ответ по найденному контексту. CitationQueryEngine дополнительно нумерует источники и заставляет модель ссылаться на них в тексте, а в response.source_nodes возвращает сами фрагменты.
Исходный код и автор
Проект Grounded Document Agent полностью открыт: подключите ключ LlamaCloud, запустите Ollama, загрузите свой PDF и проверьте работу цитат.
- Код проекта: GitHub — grounded_document_agent
- Оригинальный разбор автора: Sumanth P в X
Больше практических разборов про LLM, RAG и Python — на UPROGER.






