Когда простой склейки PDF уже недостаточно
Представьте: у вас есть сотня PDF-файлов. Технические руководства, вырезки из статей, переведенные главы, заметки по проекту. Формально их можно склеить в один файл командой pdfunite *.pdf output.pdf. Получится цифровая свалка. Оглавление из ста PDF-файлов. Дубликаты. Противоречивая информация на страницах 15 и 347. Полный хаос.
Настоящая задача не в склейке, а в синтезе. Нужна не папка, а книга. С внятной структурой, логичными главами, устраненными повторами, резюме по темам. Раньше для этого требовался редактор-человек, месяцы работы и тонны кофе. В 2026 году эту работу можно делегировать локальной LLM. Главное — не отправить конфиденциальные документы в облако OpenAI или Anthropic.
Почему это сложнее, чем кажется? LLM не видит PDF. Она видит текст, извлеченный из PDF. Качество извлечения определяет 70% успеха. Сканированные документы без OCR, таблицы, формулы, двухколоночная верстка — все это превращает текст в кашу. Сначала нужно получить чистый текст, а уже потом думать о структурировании.
Архитектура: RAG не для вопросов, а для структурирования
Обычно Retrieval-Augmented Generation (RAG) используют для вопросно-ответных систем: загрузил документы, спрашиваешь, получаешь ответ с цитатами. Мы перевернем парадигму. Мы используем RAG как мозг, который обозревает все документы сразу, находит связи, определяет иерархию и генерирует не ответы, а структуру будущей книги.
| Компонент | Наша задача | Инструменты (актуально на 01.02.2026) |
|---|---|---|
| Извлечение текста | Достать текст из 100 PDF с сохранением базового форматирования (заголовки, списки) | Unstructured.io, Docling, pymupdf (fitz) с кастомной постобработкой |
| Чанкинг и эмбеддинги | Разбить текст на смысловые блоки и перевести в векторное пространство для поиска связей | LlamaIndex с рекурсивным сплиттером, BGE-M3 (последняя версия) или nomic-embed-text-v2.5 для эмбеддингов |
| Локальная LLM | Анализ, структурирование, написание связующих текстов | Llama 3.2 11B Vision (для документов с изображениями), Mistral-Nemo 12B, Qwen2.5 14B. Все через Ollama или llama.cpp. |
| Векторная БД | Хранить все чанки и быстро искать тематические кластеры | ChromaDB (простота), Qdrant (производительность), или просто FAISS индекс в памяти для одного запуска |
Ключевая мысль: мы не загружаем все 100 PDF в контекстное окно модели (оно даже у Llama 3.2 11B ограничено 128K токенами). Мы используем RAG для итеративного "опроса" коллекции документов. Сначала модель предлагает возможную структуру книги на основе анализа заголовков и введений. Потом мы запрашиваем у RAG контент для каждой proposed главы. Модель анализирует этот контент, уточняет структуру, выявляет пробелы и дубликаты. И так по кругу, пока не получится внятный план.
1 Подготовка: вытаскиваем текст и превращаем в "понимаемые" чанки
Первая и самая скучная часть. Нужно пробежаться по всем PDF и извлечь текст. Не используйте простой pdfplumber или PyPDF2 для сложных документов — они собьют порядок чтения. Возьмите unstructured[pdf] или библиотеку Docling, которую мы разбирали в гайде по обработке длинных PDF. Они лучше справляются с колонками и таблицами.
# Установка стека для извлечения (актуально на 01.02.2026)
pip install "unstructured[pdf]" llama-index pymupdf
# Пример: извлечение текста с базовой структурой
from unstructured.partition.pdf import partition_pdf
import os
text_elements = []
for pdf_file in os.listdir("./pdfs/"):
if pdf_file.endswith(".pdf"):
elements = partition_pdf(
filename=os.path.join("./pdfs/", pdf_file),
strategy="hi_res", # Для сканов с картинками
languages=["rus", "eng"] # Указываем языки
)
for el in elements:
# Сохраняем тип элемента (заголовок, текст) для будущего чанкинга
text_elements.append({
"source": pdf_file,
"type": el.category,
"text": el.text
})
Ошибка №1: слепое разбиение по символам. Если резать текст каждые 1000 символов, заголовок окажется в одном чанке, а относящийся к нему текст — в другом. RAG потом не найдет связи. Используйте семантический чанкинг: разбивайте по заголовкам (H1, H2) или с помощью рекурсивного сплиттера из LlamaIndex, который учитывает структуру.
2 Загружаем в LlamaIndex и создаем "интеллектуальный" индекс
Теперь берем извлеченные тексты и загружаем их в LlamaIndex (версия 0.10.x на начало 2026 года существенно переработана). Не используйте SimpleDirectoryReader — он плохо сохраняет метаданные. Собираем кастомные документы.
from llama_index.core import Document, VectorStoreIndex
from llama_index.core.node_parser import SemanticSplitterNodeParser
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
from llama_index.vector_stores.chroma import ChromaVectorStore
import chromadb
# 1. Создаем документы LlamaIndex с метаданными (источник PDF)
docs = []
for elem in text_elements:
doc = Document(
text=elem["text"],
metadata={
"source": elem["source"],
"type": elem["type"]
}
)
docs.append(doc)
# 2. Инициализируем модель эмбеддингов ЛОКАЛЬНО
# BGE-M3 или nomic-embed-text-v2.5 — лучший выбор на 2026 год для мультиязычности
embed_model = HuggingFaceEmbedding(
model_name="BAAI/bge-m3", # Или "nomic-ai/nomic-embed-text-v2.5"
device="cuda", # или "cpu", если нет GPU
trust_remote_code=True
)
# 3. Парсер узлов с семантическим разделением
splitter = SemanticSplitterNodeParser(
buffer_size=1,
breakpoint_percentile_threshold=95,
embed_model=embed_model
)
nodes = splitter.get_nodes_from_documents(docs)
# 4. Создаем векторную БД в памяти
chroma_client = chromadb.EphemeralClient()
chroma_collection = chroma_client.create_collection("pdf_book")
vector_store = ChromaVectorStore(chroma_collection=chroma_collection)
# 5. Собираем индекс
index = VectorStoreIndex(
nodes=nodes,
embed_model=embed_model,
vector_store=vector_store
)
Теперь у нас есть индекс, где каждый "чанк" (узел) — это семантически цельный фрагмент текста с привязкой к исходному PDF. Модель сможет запрашивать у этого индекса не только по ключевым словам, но и по смыслу: "найди все, что связано с настройкой PostgreSQL", даже если в тексте слово "PostgreSQL" не встречается, а есть "инициализация кластера БД" и "конфигурация postgresql.conf".
3 Запускаем локальную LLM и начинаем диалог с документами
Самое интересное. Нужно выбрать модель, которая сможет "держать в голове" сложную задачу структурирования и генерировать длинные, связные тексты. На 01.02.2026 я рекомендую Llama 3.2 11B Vision (если в PDF есть схемы) или Qwen2.5 14B для чистого текста. Обе отлично работают с русским и английским. Устанавливаем через Ollama — это самый простой путь.
# Устанавливаем Ollama и качаем модель (пример для Llama 3.2 11B)
curl -fsSL https://ollama.ai/install.sh | sh
ollama pull llama3.2:11b # Или qwen2.5:14b
from llama_index.llms.ollama import Ollama
from llama_index.core import PromptTemplate
# Подключаемся к локальной LLM через Ollama
llm = Ollama(
model="llama3.2:11b",
base_url="http://localhost:11434",
temperature=0.1, # Низкая температура для более детерминированных структур
request_timeout=300.0 # Долгая генерация
)
# Создаем движок запросов к нашему индексу
query_engine = index.as_query_engine(
llm=llm,
similarity_top_k=10, # Берем топ-10 релевантных чанков для анализа
response_mode="tree_summarize" # Режим, который суммирует информацию из нескольких чанков
)
Теперь — магия промптов. Мы не будем просто спрашивать "о чем эти документы?". Мы проведем модель через многоэтапный диалог.
# Шаг 1: Получаем "сырой" список всех основных тем из документов
first_prompt = """
Ты — редактор, который анализирует коллекцию технических документов.
На основе предоставленного контента извлеки список всех основных тем, которые в них затрагиваются.
Перечисли темы в виде маркированного списка. Для каждой темы укажи, в скольких исходных документах она встречается (на основе метаданных 'source').
Контент для анализа:
{context_str}
"""
# Запускаем запрос через наш RAG движок
response = query_engine.query(
first_prompt
)
print("Выявленные темы:\n", response)
Модель вернет что-то вроде:
- Настройка PostgreSQL (встречается в 8 документах)
- Основы Python для анализа данных (в 12 документах)
- Оптимизация SQL-запросов (в 5 документах)
- Введение в Docker (в 3 документах) ... и так далее.
4 Итеративное уточнение: от тем к оглавлению книги
Получив список тем, мы просим модель предложить структуру книги. Это итеративный процесс. После первого наброска мы запрашиваем у RAG детальное содержание для каждой proposed главы, показываем его модели и спрашиваем: "Согласен ли ты с такой группировкой? Какие подтемы упущены? Где есть избыточность?"
# Шаг 2: Генерация структуры книги
structure_prompt = """
На основе списка тем ниже предложи структуру книги для начинающих и средних специалистов.
Книга должна быть практической, от простого к сложному.
Представь структуру в формате:
Название книги: [Предложи название]
Часть I: [Название части]
Глава 1.1: [Название главы] (основные темы: ...)
Глава 1.2: [Название главы] (основные темы: ...)
...
Учти, что темы 'Настройка PostgreSQL' и 'Оптимизация SQL-запросов' логично объединить в один раздел.
Темы, встречающиеся менее чем в 2 документах, вынеси в приложения или исключи.
Список тем:
{themes_list}
"""
# Подставляем ответ предыдущего шага в промпт
structure_response = query_engine.query(
structure_prompt.format(themes_list=str(response))
)
Модель сгенерирует детальный план. Но это еще не все. Теперь мы берем каждую предлагаемую главу и "скармливаем" RAG запрос: "Дай мне весь контент, относящийся к 'индексам в PostgreSQL' и 'EXPLAIN ANALYZE'". Полученные тексты мы снова отправляем модели с вопросом: "Достаточно ли этого материала для главы? Что нужно добавить? Есть ли противоречия между документами?"
Ошибка №2: слепое доверие к структуре от LLM. Модель может предложить логичную, но неполную структуру. Она не знает, чего не знает. Всегда проверяйте с помощью RAG, что для каждой главы есть достаточно исходного материала. Если для "Главы 3.5" RAG возвращает только два коротких чанка — либо объединяйте с другой главой, либо помечайте как требующую дополнения.
5 Финальная сборка: от структуры к единому документу
У нас есть утвержденная структура (оглавление) и для каждого раздела — набор релевантных текстовых фрагментов (чанков) из оригинальных PDF. Теперь задача — сгенерировать связный текст для каждой главы, используя эти фрагменты как источник. Мы не просто копируем чанки, мы просим модель переписать информацию в едином стиле, устранить противоречия, добавить плавные переходы.
# Функция для генерации текста главы
def generate_chapter_content(chapter_title, related_chunks):
chapter_prompt = """
Ты — технический писатель. Напиши главу книги на тему "{chapter_title}".
Используй ТОЛЬКО информацию из предоставленных источников ниже. Не добавляй выдуманных фактов.
Если в источниках есть противоречия, укажи на это в тексте ("В некоторых руководствах рекомендуется X, в других — Y").
Стиль: практический, четкий, без лишней воды.
Объем: примерно 1500-2000 слов.
Начни с краткого введения, затем раскрой тему, закончи резюме или переходом к следующей теме.
Источники:
{sources_text}
"""
sources_text = "\n---\n".join([chunk.text for chunk in related_chunks])
# Используем LLM для генерации главы
chapter_response = query_engine.query(
chapter_prompt.format(
chapter_title=chapter_title,
sources_text=sources_text
)
)
return chapter_response
# Пример: получаем чанки для конкретной главы через поиск в индексе
from llama_index.core import VectorStoreIndex
retriever = index.as_retriever(similarity_top_k=15)
relevant_nodes = retriever.retrieve("настройка PostgreSQL индексы производительность")
chapter_text = generate_chapter_content(
"Оптимизация баз данных: индексы и анализ запросов в PostgreSQL",
relevant_nodes
)
После того как все главы сгенерированы, остается техническая часть: собрать их в один файл. Можно вывести в Markdown (и потом конвертировать в PDF через Pandoc), или сразу в HTML, или даже использовать библиотеку типа ReportLab для генерации PDF прямо из Python. Важно сохранить сгенерированное оглавление как гиперссылки.
Что может пойти не так (и как это чинить)
- Модель "галлюцинирует" структуру. Она предлагает главы, для которых в исходниках нет материала. Лечение: после каждого предложения модели делайте проверочный запрос к RAG: "Покажи все, что связано с предложенной главой 'Квантовые вычисления для DevOps'". Если результатов нет или они скудные — корректируйте промпт: "Предлагай только те разделы, для которых есть хотя бы 5 релевантных текстовых фрагментов".
- Каша из языков. Если PDF на русском и английском, модель может начать миксовать языки в одной главе. Укажите в системном промпте: "Все итоговые тексты должны быть на русском языке. Английские термины приводи в скобках".
- Потеря специфического формата. Код, команды, таблицы из оригинальных PDF могут потеряться или превратиться в простой текст. Для сохранения кода используйте специальные обработчики в Unstructured или Docling, которые выделяют блоки кода в отдельные элементы с типом
Code. Затем в промпте для модели укажите: "Блоки кода сохраняй в точности как в источнике, обрамляя их тройными бэктиками ()". - Огромное время обработки. 100 PDF — это десятки тысяч чанков. Генерация каждой главы может занимать 2-5 минут. Общее время — часы. Оптимизация: 1) Используйте кэширование эмбеддингов (сохраните индекс на диск после создания). 2) Генерируйте главы параллельно, если RAM и VRAM позволяют запустить несколько инстансов Ollama. 3) Для чернового варианта уменьшайте
similarity_top_kдо 5-7.
Альтернатива: когда не хочется писать код
Если весь этот Python-пайплайн кажется слишком низкоуровневым, посмотрите на локальные альтернативы Google NotebookLM. Например, Anything LLM или PrivateGPT (который активно развивается). Это готовые десктопные приложения с GUI, которые под капотом делают примерно то же самое: загружают PDF, строят индекс, позволяют "беседовать" с документами. Вы можете использовать их режим "Summarize" или "Synthesize" для создания конспектов по группам документов, а затем вручную собрать эти конспекты в книгу. Это менее автоматизированно, но не требует программирования.
Однако у готовых решений есть ограничение: они заточены под вопросно-ответный режим. Заставить их пройти многоэтапный процесс структурирования, как в нашем гайде, сложно. Придется вручную задавать цепочку промптов. Поэтому для 100 PDF я все же рекомендую кастомный пайплайн на LlamaIndex — он дает полный контроль.
Итоговый совет: начинайте с малого. Возьмите не 100 PDF, а 10. Отладьте пайплайн, поймите, как ваши конкретные документы извлекаются и индексируются. Поиграйтесь с промптами. Только потом масштабируйтесь на всю коллекцию. И помните — даже лучшая LLM не заменит редактора-человека на финальном этапе вычитки. Она создает отличный черновик, каркас. Но последнее слово, чувство стиля и проверку логики оставьте за собой.