Главная / Статьи / Когда цитаты выглядят идеально, а количество людей всё равно неверно

Когда цитаты выглядят идеально, а количество людей всё равно неверно

Издание RAG с цитированием соответствующего PDF всё равно ошибочно считало двенадцать сотрудников одним — гибкий поиск, компрессия с учётом таблиц и отслеживание процессов исправили эту проблему.

3139 слов

Вопрос о заработной плате должен быть скучным. Если спросить, сколько человек указано в реестре зарплат за апрель 2025 года, ожидается просто цифра или ссылка, без чего-то интересного. Система вернула аккуратно сформулированный ответ: один сотрудник, указан источник в виде PDF-файла, страница пронумерована. Однако в реестре было указано двенадцать человек.

Именно такая неисправность делает генерацию с использованием дополнительных данных опасной на практике. Процесс не выдал ошибок; логи оставались пустыми. Ответ выглядел как любой другой правильный ответ за ту же неделю. Мягкая неисправность с примечанием хуже, чем полная ошибка, потому что никто не будет рассматривать вежливый абзац как проблему.

В этом тексте описывается весь процесс, приведший к появлению лжи: этапы её создания, методы диагностики, которые выявили ошибку, и конкретные изменения, позволившие восстановить количество до двенадцати. Используемые инструменты — совершенно обычные: pdfplumber для работы с текстом PDF, учитывающим его форматирование; LangChain для разбиения текста на фрагменты; модели MiniLM для генерации векторов; Qdrant для хранения векторных данных; гибридный механизм поиска, сочетающий плотные и BM25-индексы; кросс-кодер для переупорядочивания результатов; а также компрессор, предназначенный для сокращения объема контекста перед запуском генератора. Ни один из этих инструментов не является чем-то необычным. Проблемы возникали из-за неправильного взаимодействия этих компонентов при обработке таблиц и идентификаторов.

Один раз загрузка, ответы каждый раз

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

Процесс обработки просматривает каждый PDF, извлекает текст с учетом его форматирования, разделяет его на перекрывающиеся фрагменты, встраивает их в Qdrant вместе с метаданными, которые позже могут использоваться для фильтрации:

PDF → extract text per page → cut into chunks
    → convert each chunk to 384 numbers → store in a vector database

Процесс получения ответов основан на другой структуре графа: встраивается вопрос, извлекаются кандидатские ответы, при необходимости смешиваются результаты по ключевым словам, происходит переранжирование и сжатие, после чего модели задается вопрос с приложенными цитатами:

question → search → rerank → compress → ask the LLM → cited answer
              ↓         ↓          ↓
          5 chunks  3 chunks   ~600 chars

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

Почему каждый элемент нашел свое место

pdfplumber — не самая быстрая библиотека для работы с PDF. Это одна из немногих, которые сохраняют достаточно информации о форматировании, чтобы строки с данными о зарплатах не сливались в один длинный абзац. Более быстрые инструменты извлечения, превращающие таблицы в непрерывные предложения, делают последующие задачи по подсчёту сотрудников практически невозможными, поскольку модель не видит границ между строками.

Для разбиения данных в чанки использовался рекурсивный сепаратор символов из LangChain, и ничего другого из этой фреймворк-системы. Целью было достижение предсказуемого размера окон с перекрытием, а не использование какой-либо конкретной схемы RAG. После краткого сравнения в качестве модели для генерации эмбеддингов был выбран all-MiniLM-L6-v2: более мелкие модели упускали токены-идентификаторы, а более крупные увеличивали задержку без решения ключевых проблем. Qdrant оказался лучшим благодаря возможности сохранения целостности данных при переходе от локальной среды к облаку и наличию фильтров нагрузки, соответствующих способу, которым приложение уже определяло типы документов.

Гибридный поиск сочетал семантическое сходство с оценкой ключевых слов в стиле BM25 примерно в соотношении 0,65/0,35. Чистый семантический поиск отлично справлялся с запросом «Какова политика отпуска по уходу за детьми», но плохо справлялся с STL/2025-26/003. Переоценка результатов с использованием кросс-кодера улучшила качество списка. Компрессия предназначалась для того, чтобы перед вызовом LLM оставить только предложения с высокой значимостью. Именно на этом последнем этапе и началась история превращения двенадцати результатов в один.

Уверенный неверный ответ

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

0.781  Invoice_INV2025001_Arvind.pdf     <- returned
0.779  Invoice_INV2025002_Trident.pdf
0.776  Invoice_INV2025003_Welspun.pdf    <- actually wanted

По сути, алгоритм сходства не может выполнять точную обработку отдельных токенов. Использование метода BM25 и смешивания оценок изменило порядок ранжирования, в результате чего вышли на первое место идентификаторы и фрагменты с таблицами:

0.854  Invoice_INV2025003_Welspun.pdf    <- correct
0.549  Invoice_INV2025001_Arvind.pdf
0.545  Invoice_INV2025002_Trident.pdf

Одного этого оказалось недостаточно для восстановления первоначального подсчёта. Это лишь позволило включить нужный документ в процесс. Следующие проблемы скрывались внутри самого документа.

Идентификаторы сотрудников как элемент эксперимента

Вместо обсуждения косвенных признаков были прослежены идентификаторы сотрудников ST001 до ST012 на каждом этапе: извлечение, разбиение на фрагменты, поиск, повторное ранжирование, сжатие и окончательная сборка запроса.

retrieved chunk    ST001 ST002 ST003 ST004 ST005 ... ST012   (12 present)
after rerank       ST001 ST002 ST003 ST004 ST005 ... ST012   (12 present)
after compression  ST001 ST002                                (2 present)
prompt sent        ST001 ST002                                (2 present)

Несколько идентификаторов исчезли между этапами сжатия и обработки моделью. Компрессор использовал фиксированный лимит на «ключевые предложения»: каждая строка таблицы считалась предложением. При наличии объёмной таблицы зарплат лимит быстро исчерпывается из-за первых строк, в то время как более поздние строки, а также текст, их описывающий, никогда не попадают в запрос к модели. В результате модель честно отчитывается только о том наборе данных, который смогла увидеть.

# before — keep the top N
best = scored[:40]
# after — keep the best until the character budget runs out
budget = MAX_CONTEXT_CHARS // TOP_K_AFTER_RERANK
for sentence, score in ranked:
    if best and len(sentence) + 2 > budget:
        continue
    best.append(sentence)
    budget -= len(sentence) + 2

После того как сжатие перетасовало и обрезало строки, таблица зарплат оказалась в виде колоды карт, разданных в неправильном порядке. Каждое число могло существовать где-то в окне контекста, но структура, необходимая для подсчёта уникальных сотрудников, исчезла. Подсчёт людей в таблице, разделённой на отдельные фрагменты, — это другая задача, чем простое чтение таблицы.

Оценки реранкеров, которые казались нормальными, но всё равно мешали

Оценки кросс-кодеров для критически важных строк выглядели «разумными». Однако разумность не означает сохранение полной таблицы.

0.446  keep   ST001 Rajesh Kumar M CFO 75,000 ...
0.323  keep   ST002 Priya Sundaram Finance Mgr 45,000 ...
0.420  keep   ST003 Murugan K Sr Accountant 28,000 ...
0.259  DROP   ST005 Selvam R Production Mgr 38,000 ...   ← below 0.30
0.422  keep   ST006 Karthik P Supervisor 18,000 ...
# pick the best by score…
chosen = set(id(p) for p in best)
# …then put them back in document order before joining
best = [p for p in scored if id(p) in chosen]

Правка заключалась не в повышении порога везде, а в выявлении строк, похожих на таблицы, и освобождении их от агрессивного сжатия. Практический эвристический метод: если строка содержит четыре или более чисел и находится среди по меньшей мере трех подобных строк в одном блоке, её следует считать строкой таблицы и сохранить независимо от оценки предложения.

ROW_MIN_NUMBERS = 4
TABULAR_MIN_ROWS = 3
def is_row(sentence):
    return len(NUMBER_RE.findall(sentence)) >= ROW_MIN_NUMBERSif looks_tabular(sentences):
    survivors = [p for p in scored if is_row(p[0]) or p[1] >= threshold]
else:
    survivors = [p for p in scored if p[1] >= threshold]

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

Небольшие исправления, устранившие другие проблемы

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

# the context cap must be at least CHUNK_SIZE × TOP_K_AFTER_RERANK
MAX_CONTEXT_CHARS = 5000   # was 2000 while 1200 × 3 = 3600

Теперь каждый ответ включает информацию о всем процессе обработки, а не только окончательное предложение — полученные идентификаторы, оценки, решения относительно компрессии и фильтры — что позволяет без догадок проанализировать следующую ошибочную подсчётную информацию:

{
  "answer": "12 employees are listed...",
  "citations": [{"doc": "Salary_Register_April_2025.pdf", "page": 1}],
  "pipeline": {
    "retrieved": 5,
    "after_rerank": 3,
    "chars_before_compression": 3847,
    "chars_after_compression": 2103
  }
}

Фильтрация по типу документа работала корректно в локальной инстанции Qdrant, но так же сбоила в Qdrant Cloud, пока индексы данных и форматы фильтров не стали соответствовать тем, что на самом деле индексировалось в облаке:

500 — Index required but not found for "doc_type"

Аналогичный способ в Python: функция os.getenv("KEY", "default") возвращает пустую строку, когда переменная существует, но содержит пустые значения. Это не является стандартным поведением. Лучше использовать оператор or, если необходимо, чтобы в случае пустоты значение прошло дальше:

QDRANT_URL = os.getenv("QDRANT_URL") or "http://localhost:6333"

Каков итог

После гибридного поиска, сжатия с учётом структуры таблицы и отслеживания процесса через специальные механизмы, вопрос о регистре за апрель дал результат двенадцать, причём цитаты указывали на те строки, которые обосновывали этот показатель.

Retrieval:    hybrid, 0.65 semantic + 0.35 BM25
Reranking:    cross-encoder, 5 in → 3 out
Compression:  sentence-level, table-aware, ~35% reduction
Grounding:    prompt + threshold + temperature 0.0
Evaluation:   13/15 on the golden set, refusals tested
Latency:      ~4s end to end
Cost:         ~$0.003 per fifteen-question run

Для всего этого не требовалась новая базовая модель. Необходимо было рассматривать RAG как систему обработки данных с измеримыми показателями выживаемости важных токенов. В учебниках рекомендуется последовательность «встраивание, поиск, генерация». В производстве же важно «доказать, что строки действительно дошли до промпта».

Практические навыки, обеспечивающие точность подсчётов

Сохраняйте набор «золотых» вопросов, включающий по меньшей мере один вопрос на подсчёт элементов в таблице, один вопрос по точному поиску идентификатора и один вопрос, связанный с семантической политикой. Если проходят только вопросы семантического типа, это означает, что демо-версия врёт относительно готовности к работе.

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

Для корпусов, содержащих как прозу, так и разные стили изложения, предпочтительнее использовать явные гибридные подходы. Один лишь интенсивный поиск будет эффективен для эссе, но не сработает при обработке кода.

Считайте цитаты необходимыми, но недостаточными. Номер страницы рядом с неверным подсчётом элементов всё равно остаётся неверным подсчётом.

При переходе с локальной версии Qdrant в облачную необходимо повторно проверить индексы данных и поведение фильтров с использованием тех же тестовых данных. Совместимость API не подразумевает идентичных параметров индексации.

Бюджетный контекст касается структуры в целом, а не только «важных предложений». Таблицы — это часть структуры. Сжатие их, как абзацев в блоге, приводит к тому, что двенадцать элементов становятся одним.

Почему «мягкие» сбои требуют более строгих проверок

Команды часто разрешают запуск RAG на основе критерия «ответы выглядят хорошо в таблице с примерами успешных запросов». Этот критерий упускает из виду реальные проблемы в области заработной платы, запасов и соблюдения норм: недоучет данных. Необходимо добавить автоматизированные проверки, которые будут убеждаться в том, что выделенные наборы данных сохраняются в запросе для известных элементов. Запуск должен быть отклонён, если после сжатия в элементе заработной платы не появятся все значения от ST001 до ST012.

Тот же механизм проверки может убедиться в том, что в действующей конфигурации действительно используются веса гибридной смеси, а не только указано это в файле README. Различия между экспериментами в ноутбуке и конфигурацией сервиса — частая причина того, почему алгоритм BM25 «исчезает» после рефакторинга.

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

Заключение

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

Именно этот стандарт стоит соблюдать: не в том, может ли модель звучать уверенно, а в том, может ли система доказать факты, обосновывающие эту уверенность.

Более глубокий взгляд на использование гибридной системы оценки на практике

Метод глубокого поиска кодирует вопрос и каждый фрагмент текста в один и тот же векторный пространство, после чего сортирует результаты с использованием косинусного или скалярного произведения. Этот подход эффективен, когда язык пользователя совпадает с языком документа: вопросы о родительском отпуске, выходном пособии или политике удаленной работы. Он терпит неудачу, когда пользователь вставляет непонятный токен, который почти не встречается в данных обучения и редко встречается вместе с близлежащими словами во векторном пространстве. Коды сотрудников, номера счетов-фактур и идентификаторы контрактов относятся именно к такому классу токенов.

BM25 и похожие инструменты оценки лексики решают проблему иначе. Для них важно наличие токена, его редкость в корпусе и частота появления в кандидатском фрагменте. Им безразлично, что «STL/2025-26/003» семантически почти ничего не связано. Сочетание этих двух показателей не является философским принципом; это признание того, что корпуса заработной платы содержат как тексты в стиле эссе, так и таблицы вроде реестров.

Используемое здесь соотношение 0,65 / 0,35 — это лишь отправная точка, а не закон природы. Корпуса, богатые кодами, могут требовать большего веса лексических показателей, тогда как корпусы, ориентированные на повествование, — меньшего. Важно измерять оба типа вопросов в золотом наборе и не использовать смешанный результат, который проходит только тесты на повествовательные задания.

При реализации метода смешивания необходимо нормализовать оценки перед их объединением. Чистые показатели плотности сходства и чистые оценки BM25 имеют разные масштабы. Команды, использующие ненормализованные числа, часто случайно обнаруживают, что один канал доминирует. Методы мин-макс или слияние по рангам (RRF) являются приемлемыми, если они тестируются с примерами, содержащими точные идентификаторы.

Компрессия как сжимающий кодек для таблиц

Компрессия контекста необходима из-за ограниченной длины окна моделей и из-за того, что нерелевантные предложения отвлекают внимание. Стандартный подход заключается в оценке предложений по степени релевантности, сохранении лучших k из них и удалении остальных. Однако этот подход предполагает, что предложения являются взаимозаменяемыми единицами смысла. Строки таблицы не являются взаимозаменяемыми единицами смысла: строка 7 без строк 1–6 — это не просто таблица с незначительно худшим качеством, а поврежденная таблица.

Эвристика исключения — множество чисел в одной строке, несколько таких строк рядом — преднамеренно примитивна. Она позволяет сохранить некоторые неявляющиеся таблицами строки, которые выглядят числовыми, и это приемлемо. Ложноположительные результаты стоят токенов, а ложноотрицательные — точности. При расчете заработной платы и учете запасов точность имеет первостепенное значение.

Более сложные детекторы могут использовать структуру PDF из pdfplumber: рамки ячеек, выровненные столбцы, повторяющиеся координаты X. Эти сигналы отлично подходят, когда они доступны. Эвристика предложения остается полезной в качестве альтернативы, если текст уже был преобразован в markdown или обычный текст перед процессом сжатия.

Еще один способ сбоя — перетасовка элементов. Даже если все строки сохраняются, их перестановка в соответствии с оценкой релевантности может нарушить общие суммы и подсчеты. Для строк таблиц, подлежащих исключению, лучше сохранять стабильный порядок документа. Свободно упорядочивайте прозу; таблицы должны оставаться в порядке чтения.

Цитаты, оправдывающие неверный ответ

Citation UX часто указывает на PDF и страницу, из которых был взят наибольший объем данных. Даже если при сжатии размер таблицы уменьшается вдвое, ссылка по-прежнему указывает на правильный файл. Пользователи воспринимают это как подтверждение. Дизайн продукта должен либо указывать конкретные строки, использованные в итоговом запросе, либо отображать следы поиска. В противном случае интерфейс становится соучастником проблемы.

Для регулируемых областей необходимо хранить точный хеш контекста запроса вместе с ответом. Если аудитор спросит, почему система указала «один сотрудник», можно воспроизвести укороченную таблицу и показать ошибку, вместо того чтобы спорить о характеристиках модели.

Локальные и облачные векторные базы данных

Фильтр типа документов, который работал локально, но отказал в Qdrant Cloud, напоминает о том, что «один и тот же API» не означает «одинаковую конфигурацию индекса». Индексы загрузки данных, фильтры по ключевым словам и обработка значений null различаются в зависимости от режимов развертывания. Тесты должны выполняться в том же режиме развертывания, который используется в продакшене. Зелёный результат тестов локально при красном результате в облаке — так и поступают с запросами на получение пустых данных.

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

Оценка стабильности, а не впечатлений

Эксперимент с выживаемостью идентификаторов сотрудников служит шаблоном. Выбирайте сущности, которые обязательно должны присутствовать для получения правильного ответа. Проверяйте их наличие после извлечения, разбиения на части, поиска, переранжирования и сжатия. Отслеживайте падение качества ответов. Обычно первый резкий спад свидетельствует о ошибке в коде.

Распространите эту идею на числовые агрегаты. Если вопрос касается суммы, убедитесь, что все элементы, участвующие в суммировании, дошли до системы. Если речь идет о подсчёте уникальных элементов, проверьте полноту набора ключей. Такие проверки дешевы по сравнению с реальными инцидентами в производственной среде.

Чего не стоит винить в первую очередь

Когда подсчёт идёт неверно, возникает соблазн винить большие языковые модели. Иногда модель действительно не может считать. Чаще всего она вообще не получала структуру, поддающуюся подсчёту. Меняйте модели только тогда, когда график работоспособности покажет зелёный цвет. В противном случае вы «исправите» ошибку в подсчёте, перейдя на более мощную модель, которая выдаст нужное вам число — лишь до поступления следующей информации.

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

Минимальный чек-лист усиления безопасности

  1. Ключевые вопросы: количество записей в таблице, точный идентификатор, семантическая политика.
  2. Гибридный способ поиска с нормализованным сочетанием данных или методом RRF.
  3. Компрессия с учётом структуры таблицы и сохранением постоянного порядка строк.
  4. Логирование всех внутренних ответов на каждом этапе обработки.
  • Нормализация конфигурации, при которой пустые переменные окружения считаются отсутствующими.
  • Фикстуры индекса в облаке, отражающие фильтры продакшена.
  • Проверки на корректность обработки документов для известных типов записей.
  • Цитаты, связанные с содержимым запроса, а не только с загруженными файлами.
  • Пройдите этот чек-лист перед тем, как считать систему RAG для расчёта заработной платы «готовой». Апрельский отчёт не станет последним документом, который кажется простым, но тем не менее выдаёт ошибки.

    Последствия для команды

    Когда процесс обработки запросов с участием двенадцати сотрудников стабилизировался, та же система мониторинга выявила ещё две проблемы: устаревший псевдоним коллекции после повторной загрузки данных и проблемы с таймаутом ранжировщика, из-за которых использовались неранжированные результаты без указания на снижение качества ответа. Обе проблемы остались бы незамеченными, если бы API возвращал только строку. Возврат структурированных данных — оценок, временных показателей и вариантов резерва — сделал ассистента инструментом, которому операторы могли доверять достаточно, чтобы отлаживать его в 2 часа ночи.

    Вот настоящий урок, связанный с продуктом. Системы RAG — это не просто интерфейсы для чата над PDF-файлами. Это каналы передачи данных, которые работают с текстом в виде абзацев. Относитесь к ним как к таковым: измеряйте потери данных, сохраняйте структуру и никогда не позволяйте цитате заменять собой доказательства.

    Ещё один анализ первоначального неверного ответа

    Повторный анализ некорректного ответа с приложенными отслеживаемыми данными делает ситуацию почти скучной. Система поиска почти нашла правильный PDF-файл; гибридная система оценки завершила эту работу. Затем система сжатия потратила всё своё время на первые числовые строки и проигнорировала остальные. Модель подсчитала то, что осталось, и сослалась на файл. Каждый этап по отдельности выполнял обосновуемые действия; вместе они создавали впечатление надёжности.

    Именно поэтому локальные метрики на этапе обработки вводят в заблуждение. Показатель Retrieval@k может казаться нормальным, в то время как процесс сжатия уничтожает ответ. Наиболее точной метрикой, отражающей ущерб для пользователя, является степень сохранения сущностей на протяжении всего процесса. Применяйте её с самого начала, особенно когда документы представляют собой таблицы, заключённые в формат PDF.

    Если вы хотя бы что-то извлечёте из инцидента с двенадцатью сотрудниками, то пусть это будет следующее: вежливые сбои системы RAG считаются ошибками в работе процесса, пока не будет доказано обратное. Внедряйте систему мониторинга, защищайте структуру данных и обеспечивайте возможность просмотра процесса обработки, прежде чем доверять её результатам. Слабые сбои требуют строгих механизмов контроля, постоянной проверки и специалистов, которые могут отслеживать каждый этап до тех пор, пока пользователи снова не начнут доверять приводимым данным.

    Степень сохранения сущностей при сжатии остаётся самым простым и надёжным критерием для корпусов данных, состоящих в основном из таблиц.

    Когда поступит следующий набор данных, снова проведите анализ сохранения идентификаторов, прежде чем снова доверять новым результатам.