Команда загрузила в индекс Google Drive целиком. Там оказались пять копий регламента, протоколы встреч, выгрузки Jira и PDF без извлечённого текста. На вопрос о лимите система возвращала четыре разных числа. Увеличение top_k только покупало больше мусора за токены.
Четыре разных числа — это, между прочим, честный ответ системы. В корпусе действительно лежали четыре версии, и все четыре кто-то когда-то утвердил. Поиск не соврал, он показал состояние компании: никто не знает, какой регламент действует. Раньше это знание было спрятано в папках, теперь его выдают клиенту в чате за деньги.
Поисковая система начинается не с vector database, а с правил корпуса. У документа должны быть владелец, канонический идентификатор, версия, статус публикации и срок пересмотра. Индексация без этих полей превращает поиск в склад, где новый файл лежит рядом со старым и оба считаются правдой.
Загружать всё — это решение, а не отсутствие решения
«Давайте зальём всё, а модель разберётся» звучит как экономия времени. На деле это перенос стоимости: неделя на разбор корпуса заменяется на бесконечные тикеты «система ответила неправильно», каждый из которых расследуется вручную.
Три вещи, которые дорожают немедленно:
- Токены. Мусор конкурирует за место в контексте с полезным (глава 18) и вытесняет его.
- Доверие. Один ответ по протоколу встречи трёхлетней давности отменяет двадцать правильных.
- Расследование. Без версий и владельцев на вопрос «почему она так ответила» отвечать нечем.
Практический критерий включения документа в корпус: есть ли человек, который отвечает за его актуальность. Нет владельца — нет публикации. Бюрократии тут нет: иначе вы отвечаете за чужой черновик перед клиентом.
Конвейер качества
source -> parse -> normalize -> deduplicate -> classify
-> validate metadata -> publish version -> chunk/embed -> indexПубликацию полезно делать атомарно: новая версия полностью подготовлена, затем становится активной; старая перестаёт участвовать в retrieval. Content hash убирает побайтовые копии, но смысловые дубли приходится находить отдельно.
import hashlib
def canonical_hash(text: str) -> str:
normalized = " ".join(text.split()).casefold()
return hashlib.sha256(normalized.encode("utf-8")).hexdigest()
REQUIRED = {"document_id", "version", "owner", "effective_at", "status"}
def validate_document(doc):
missing = REQUIRED - doc.metadata.keys()
if missing:
raise ValueError(f"missing metadata: {sorted(missing)}")
if len(doc.text.strip()) < 100:
raise ValueError("parser produced suspiciously little text")Проверка на «подозрительно мало текста» стоит четыре строки и ловит самую массовую поломку загрузчика: PDF, из которого извлеклись три слова и номер страницы. Без неё в корпусе появляются документы-призраки — они есть в каталоге, они имеют владельца и версию, и в них нет ни одного факта.
Смысловые дубли
Побайтовые копии убирает хеш. Настоящая проблема — пять документов, где написано примерно одно и то же с разными числами: «Регламент возвратов», «Регламент возвратов (актуальный)», «Регламент возвратов v3 финал», «Регламент возвратов v3 финал 2 (правки Иванова)» и презентация, где тот же регламент пересказан на слайде.
Механически это лечится частично: near-duplicate detection по n-граммам или по близости эмбеддингов ловит кандидатов, но решение «какой из пяти живой» принимает человек. Работающая практика: находить кластеры дублей автоматически, отдавать их владельцу списком и не публиковать кластер, пока в нём не назначен канонический документ.
Заодно это единственный известный способ заставить компанию навести порядок в документах: не призывом, а тем, что помощник иначе не отвечает.
Наблюдаемые признаки помойки
Доля запросов без клика на источник растёт; один документ занимает большую часть top-k; старые версии появляются рядом с новыми; parser выдаёт пустые страницы; у документов нет владельца; индекс расходится с каталогом. Это метрики данных, не модели.
Полезная привычка — дашборд корпуса рядом с дашбордом качества:
| Метрика | Порог внимания |
|---|---|
| документов без владельца | больше нуля |
| кластеров дублей без канонического документа | больше нуля |
| документов, где parser дал меньше N символов | больше 1% |
| расхождение «каталог против индекса» | больше нуля |
| документов с истёкшим сроком пересмотра | растёт месяц к месяцу |
Failure modes тоже лежат в данных. OCR путает цифры. Таблица теряет заголовки. Confluence отдаёт вложения без ACL. Инкрементальная загрузка создаёт дубликат после переименования. Удаление источника не доходит до индекса. Смена embedding-модели оставляет половину корпуса в старом пространстве.
Для эксплуатации нужен quarantine: подозрительный документ сохраняется с причиной отказа, но не участвует в поиске. Иначе загрузчик либо молча теряет данные, либо публикует всё, что смог прожевать.
Срок пересмотра как механизм
Поле review_due — самая недооценённая часть схемы. Документ без срока пересмотра стареет молча: он был правдой в марте, перестал быть правдой в июле, и никто этого не заметил, потому что никто на него не смотрел.
Работает простой цикл: у каждого документа есть владелец и дата; за две недели до срока владельцу приходит задача; просроченный документ помечается как stale и понижается в ранжировании или исключается из retrieval, в зависимости от того, насколько дорога ошибка.
Это единственное место в главе, где технология решает организационную проблему честно: не заставляет людей быть аккуратными, а делает неаккуратность видимой.
Правило главы
Индекс является производной от управляемого каталога. Его можно перестроить. Если источник, версия и владелец неизвестны, фрагмент нельзя выдавать как свидетельство.
Чеклист главы
Практикум
Задача 23.1 — инвентаризация ста документов. ⭑⭑
Возьмите сто документов из настоящего источника и посчитайте состояние корпуса до всякой автоматизации.
Критерии приёмки:
Подсказки:
- Смысловые дубли ищите близостью эмбеддингов внутри корпуса: кластеры видно сразу, и это самый быстрый способ показать масштаб.
- Инвентаризацию делайте до внедрения RAG, а не после первого инцидента. Разговор с бизнесом в этих двух точках выглядит по-разному.
Задача 23.2 — quarantine и атомарная публикация. ⭑⭑
Реализуйте конвейер, где документ проходит валидацию метаданных, а версия публикуется атомарно.
Критерии приёмки:
Подсказки:
- Атомарность проще всего получить переключением alias индекса, а не удалением и вставкой.
- Отчёт по quarantine отправляйте владельцам по расписанию: очередь, которую никто не видит, превращается в кладбище.
Задача 23.3 — жизненный цикл документа целиком. ⭑⭑⭑
Проведите три операции над живым корпусом и проверьте, что система осталась согласованной: удалите один документ, переименуйте другой, загрузите третью версию регламента.
Критерии приёмки:
Подсказки:
- Проверяйте согласованность не глазами, а джобой сверки «каталог против индекса»: она нужна вам навсегда, а не только на время задачи.
- Если после наведения порядка качество ответов выросло сильнее, чем от всех экспериментов с промптами, это нормальный и очень частый результат.