Для RAG конвертируйте PDF в Markdown, если хотите, чтобы чанки повторяли разделы документа: каждый заголовок становится точкой разбиения, а заголовки над чанком попадают в его метаданные. Перед индексацией проверьте заголовки, таблицы и распознанный текст, а затем проверьте поиск на вопросах, ответы на которые вы уже знаете.
Это руководство для разработчиков, которые строят поиск по закрытым документам: политикам, руководствам, договорам, исследованиям. Предполагается, что модель эмбеддингов и векторное хранилище вы уже выбрали.
Почему по заголовкам получаются лучшие чанки
Сплиттер с фиксированным размером режет текст на куски заданной длины, где бы ни пришёлся разрез. Чанк может начаться посреди раздела и потерять заголовок, который говорит, о чём он. «Не больше 100 запросов в минуту» значит одно в разделе «Sandbox» и другое в разделе «Production». Без заголовка ни поиск, ни модель не поймут, о каком из них речь.
Markdown хранит структуру документа в строках, которые начинаются с #, ## и ###. Сплиттер по заголовкам режет текст по этим строкам, поэтому каждый чанк охватывает один раздел, и записывает заголовки над чанком, то есть путь раздела. Руководство LangChain по разбиению Markdown описывает ту же идею: держать текст вместе с заголовками, к которым он относится.
Разделы бывают разной длины, поэтому длинный раздел всё равно нужно разбить ещё раз по размеру. Путь раздела относится к каждому его куску.
Конвертируйте PDF и проверьте заголовки
В MD Factory добавьте папку с вашими PDF. Каждый файл Markdown сохраняется рядом со своим PDF под тем же именем, а изображения попадают в папку рядом. В сканах PDF текст распознаётся (OCR) на вашем компьютере. Файлы Word и другие форматы в папке конвертируются так же.
Прежде чем писать код для разбиения, откройте несколько результатов:
- Убедитесь, что главы, разделы и подразделы получились как #, ## и ###. В PDF заголовок часто отличается только более крупным или жирным шрифтом, поэтому любому конвертеру приходится угадывать, где начинаются разделы и как они вложены. Исправьте уровни там, где они неверны.
- В файлах Word заголовки переносятся, если автор использовал стили заголовков Word. Текст, который просто набран жирным или крупным шрифтом, остаётся обычным абзацем.
- Поищите посреди текста колонтитулы и номера страниц.
- Нажмите «Требуется проверка» рядом с файлом, чтобы увидеть, что нужно проверить. Для PDF в списке могут быть страницы, где OCR не смог прочитать часть текста или вообще не нашёл текста.
Исправьте ошибки в файле Markdown до индексации. Иначе их унаследует каждый чанк.
Разбиение по заголовкам в коде
MarkdownHeaderTextSplitter из LangChain разбивает текст по указанным уровням заголовков и кладёт каждый заголовок в метаданные чанка. Второй сплиттер ограничивает размер внутри каждого раздела:
from pathlib import Path
from langchain_text_splitters import (
MarkdownHeaderTextSplitter,
RecursiveCharacterTextSplitter,
)
corpus = Path("corpus")
by_heading = MarkdownHeaderTextSplitter(
[("#", "h1"), ("##", "h2"), ("###", "h3")], strip_headers=False
)
by_size = RecursiveCharacterTextSplitter(chunk_size=1500, chunk_overlap=150)
chunks = []
for path in sorted(corpus.rglob("*.md")):
sections = by_heading.split_text(path.read_text(encoding="utf-8"))
for chunk in by_size.split_documents(sections):
chunk.metadata["source"] = path.relative_to(corpus).as_posix()
chunk.metadata["section"] = " > ".join(
chunk.metadata[h] for h in ("h1", "h2", "h3") if h in chunk.metadata
)
chunks.append(chunk)
Размер чанка и перекрытие здесь только отправная точка: подберите их под свою модель эмбеддингов. Чтобы строки заголовков оставались в тексте чанка и текст для эмбеддинга тоже называл свой раздел, в примере задано strip_headers=False.
MarkdownNodeParser из LlamaIndex делает такое же разбиение по заголовкам. Если загружать файлы через SimpleDirectoryReader, каждый узел получает ещё и имя файла:
from llama_index.core import SimpleDirectoryReader
from llama_index.core.node_parser import MarkdownNodeParser
documents = SimpleDirectoryReader(
"corpus", recursive=True, required_exts=[".md"]
).load_data()
nodes = MarkdownNodeParser().get_nodes_from_documents(documents)
print(nodes[0].metadata["file_name"], nodes[0].metadata["header_path"])
header_path перечисляет заголовки над узлом, например /Employee handbook/Expenses/; собственный заголовок узла — первая строка его текста.
Храните источник с каждым чанком
С каждым чанком храните как минимум три вещи:
- Файл. Файл Markdown называется так же, как его PDF, поэтому чанк из handbook.md ведёт к handbook.pdf. Храните путь относительно папки корпуса: два файла terms.md в разных папках — это разные документы.
- Путь раздела, чтобы ответ мог назвать использованный раздел, а читатель сразу перешёл к нужному месту.
- Редакция или дата. Когда политика меняется, вы удаляете чанки старого файла и индексируете новые, а не отдаёте обе версии.
Если к одному индексу будут обращаться люди с разными правами доступа, храните также, кто может читать каждый документ, и фильтруйте по этому полю при запросе. Поиск вернёт любой чанк, подходящий к вопросу.
В Markdown, который создаёт MD Factory, нет номеров страниц. Если в ответах нужны ссылки на страницы, найдите фрагмент в исходном PDF.
Осторожнее с таблицами и распознанным текстом
Строка заголовков есть в таблице Markdown только один раз, в начале. Если таблица длиннее размера чанка, сплиттер по размеру разрежет её, и следующие куски останутся без названий столбцов. В нашем тесте с LangChain (langchain-text-splitters 1.1.3) таблица из 40 строк при разбиении на чанки по 300 символов дала четыре куска, и строка заголовков была только в первом.
Это можно исправить двумя способами: держать каждую таблицу в одном чанке, даже если он длиннее остальных, или повторять строку заголовков в начале каждого куска.
Для файлов Word и Excel MD Factory ставит статус «Требуется проверка», если объединённые или сложные ячейки пришлось упростить. У таблиц из PDF такой пометки нет, поэтому сравните несколько из них с оригиналом, особенно таблицы с объединёнными заголовками.
Распознанный текст требует отдельной проверки. Неверно распознанные числа, имена и коды попадают в ответы без изменений, и по самому чанку ошибку не видно. Сравните числа в нескольких чанках со сканом и не добавляйте в индекс отмеченные страницы, пока не исправите их. На что смотреть, рассказано в нашем руководстве по проверке OCR в сканах PDF.
Проверьте поиск на реальных вопросах
Запишите вопросы, которые люди действительно будут задавать, и для каждого файл и раздел, где лежит ответ. Начните с одного-двух десятков.
- Прогоните каждый вопрос через поиск и посмотрите на первые результаты.
- Отметьте, есть ли среди них нужный раздел.
- Для каждого промаха сравните вернувшиеся чанки с тем, который должен был вернуться. Типичные причины: неверный уровень заголовка, таблица, отрезанная от строки заголовков, ошибка OCR в ключевом слове или раздел настолько длинный, что ответ в нём теряется.
- Меняйте что-то одно, например размер чанка, уровни заголовков или способ разбиения таблиц, и прогоняйте те же вопросы снова.
Храните вопросы в файле рядом с корпусом и прогоняйте их заново каждый раз, когда меняются документы или код разбиения.
Когда использовать MD Factory, а когда библиотеку
MD Factory — настольное приложение для Windows. Оно подходит для закрытой коллекции, которую вы хотите конвертировать на своём компьютере и проверить глазами: добавьте папку, посмотрите статусы, откройте файлы, которым нужна проверка, исправьте их и направьте скрипт индексации на эту папку.
MD Factory держит документы на вашем компьютере, пока конвертирует их. Если затем вы создаёте эмбеддинги через облачный API или отправляете чанки облачной модели, их текст уходит этому поставщику. Если документы не должны покидать вашу сеть, используйте локальную модель.
Если документы поступают постоянно, лежат на сервере или должны конвертироваться без участия человека, используйте в коде библиотеку для конвертации. Варианты смотрите в нашем сравнении конвертеров PDF в Markdown.