{ "index": 101, "slug": "editorial-2025-03-mechanism-knowledge-retrieval", "title": "Почему высокий vector score не доказывает ответ", "excerpt": "Retrieval находит похожие фрагменты, но не подтверждает их свежесть, доступность и смысл. Разбираем границу между ranking и проверяемым источником.", "contentHtml": "
Проблема. Поиск по инженерной базе возвращает первый фрагмент с высоким vector score. Фрагмент похож на вопрос, поэтому ответ выглядит уверенно. Через несколько дней выясняется, что документ описывает старый контракт, закрыт для автора запроса или не содержит точного правила, на которое ссылается ответ.
\nСимптом. В результате есть title, excerpt и число вроде 0.98, но нет версии источника, срока действия, access decision и anchor. Инженер меняет код по старой инструкции. Цена ошибки. Команда тратит время на повторное расследование: нужно найти нужную редакцию, выяснить права и отделить цитату от пересказа. Ошибка также подрывает доверие к базе знаний. Следующий хороший фрагмент начинают игнорировать вместе с плохим.
Тезис. Vector score решает одну задачу: упорядочивает похожие candidates в выбранной модели поиска. Он не доказывает истинность claim, право читать источник, актуальность документа и полноту найденных материалов. Поэтому retrieval должен завершаться не выбором top-1, а проверяемым решением: candidate допускается к ответу только после проверок access, freshness, provenance и exact citation.
\nNearest-neighbor поиск сравнивает запрос с векторами документов. Его результат помогает сократить очередь чтения. Это полезно, когда корпус велик и человек не может открыть все записи. Но score не знает, кто задаёт вопрос. Он не знает, отозвал ли владелец документ. Он не знает, поддерживает ли найденный абзац именно тот claim, который собирается сделать система.
\nПорог похожести не исправляет эту границу. Представим учебный корпус из трёх записей. Просроченная инструкция получила score 0.97, закрытое исключение — 0.99, а текущая разрешённая инструкция — 0.91. Фильтр score >= 0.90 оставит все три. Сортировка поставит опасные записи выше нужной. Это не результат реального сервиса, а минимальный пример, который показывает, почему ranking нельзя использовать как authorization или evidence.
| Сигнал | Вопрос | Действие | Чего он не доказывает |
|---|---|---|---|
| vectorScore | Насколько candidate похож на query? | Ставит запись в очередь проверки. | Что claim верен, свеж и доступен. |
| accessLabels | Разрешён ли класс источника requester scope? | Отбрасывает закрытый фрагмент. | Что выполнена вся реальная policy identity. |
| publishedAt / expiresAt | Входит ли запись в объявленное окно свежести? | Отбрасывает будущие и просроченные записи. | Что это единственная актуальная редакция. |
| citationUri#anchor | Можно ли открыть точное место в конкретной версии? | Делает candidate адресуемым. | Что источник поддерживает более широкий вывод. |
| human verification | Совпадает ли claim с открытым фрагментом? | Разрешает сформулировать ответ. | Что одна проверка заменяет владельца policy. |
Chunking часто оставляет в индексе только текст и embedding. Заголовок, версия и срок действия остаются в исходном документе. После retrieval система показывает удобный snippet, но не может ответить на простой вопрос: из какой редакции он взят? Одинаковая фраза может встречаться в новой инструкции, старом RFC и закрытом исключении.
\nМинимальная запись должна связывать chunk с source record. Source record хранит sourceId, sourceVersion, owner, publishedAt и URI. Chunk хранит sourceId, текстовый фрагмент, citationAnchor, indexedAt, expiresAt и access labels. Retrieval event сохраняет query, момент поиска, список candidates и причины отказа. Система может не передавать все поля в пользовательский интерфейс, но decision должен иметь к ним доступ.
Если у chunk нет стабильной связи с редакцией, он остаётся подсказкой для поиска. Если у него нет anchor, он не становится точной цитатой. Если у него нет declared policy свежести, приложение не должно молча называть его текущим. Эти ограничения лучше показать явно, чем восстанавливать по смыслу из текста.
\nНадёжный порядок начинается с evidence, а не с готового текста. Сначала система получает candidates и их metadata. Затем она применяет policy к каждому candidate. Только оставшиеся фрагменты передаются человеку или компоненту, который формулирует ответ. Так нельзя незаметно добавить в draft тезис, которого не было в источнике.
\ntype Candidate = {\n id: string;\n score: number;\n sourceVersion?: string;\n accessLabels: string[];\n publishedAt?: string;\n expiresAt?: string;\n citationUri?: string;\n citationAnchor?: string;\n};\n\nfunction admissible(candidate, requesterLabel, retrievalAt) {\n const allowed = candidate.accessLabels.includes(requesterLabel);\n const fresh = candidate.publishedAt <= retrievalAt &&\n (!candidate.expiresAt || retrievalAt < candidate.expiresAt);\n const citable = Boolean(\n candidate.sourceVersion &&\n candidate.citationUri &&\n candidate.citationAnchor\n );\n\n return { allowed, fresh, citable, ok: allowed && fresh && citable };\n}\n\n// Учебный код: локальные значения, без реального corpus и identity provider.\nКод показывает контракт, а не готовую систему доступа. Проверка accessLabels.includes не заменяет authentication, authorization policy и аудит. Сравнение дат работает только после фиксации формата и часового пояса. Human verification всё равно нужен: технически доступный anchor может содержать исключение, которое не подтверждает общий claim.
| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Top-1 отвечает на вопрос, но описывает старый контракт. | Ranking не учитывает срок действия или version policy. | Сравнить publishedAt и expiresAt с зафиксированным retrievalAt. | Отклонить просроченный candidate и показать актуальную редакцию либо остановить ответ. |
| Фрагмент релевантен, но его нельзя открыть. | Индекс получил текст без согласованной access boundary. | Проверить requester scope, labels и решение владельца ресурса. | Убрать candidate из answer path; не маскировать отказ новым пересказом. |
| Ссылка ведёт на страницу, но не на правило. | Сохранён URI без версии и anchor. | Открыть ссылку на том же snapshot и найти точный фрагмент. | Оставить запись кандидатом, пока источник не станет адресуемым. |
| Ответ звучит шире цитаты. | Генератор обобщил локальное исключение. | Сопоставить каждое предложение claim с одним или несколькими фрагментами. | Сузить формулировку, добавить limitation или отправить вопрос на ручную проверку. |
| После пустого результата система всё равно отвечает. | Fallback подменяет отсутствие evidence вероятным текстом. | Проверить negative path: все candidates expired, закрыты или без anchor. | Вернуть stop status с причиной и запросить источник или решение владельца. |
Проверять нужно не только случай, где нашлась хорошая запись. Система должна остановиться, если все candidates просрочены, закрыты или не имеют точного locator. Пустой результат честнее уверенного ответа без основания. Его можно объяснить и исправить: обновить документ, выдать доступ, добавить anchor или уточнить вопрос.
\nНе смешивайте причины в один статус вроде relevance_low. access_denied ведёт к владельцу policy. expired_at_retrieval ведёт к владельцу документа. missing_citation_anchor ведёт к ingestion или структуре источника. Такое разделение экономит время и не толкает команду сразу менять embedding model.
retrievalAt. Один и тот же запрос должен иметь воспроизводимый срез времени.Эта схема не обещает, что vector search найдёт полный корпус. Она не сравнивает качество embedding-моделей и не утверждает, что keyword search всегда лучше. Она также не превращает metadata в доказательство смысла. Свежая доступная цитата может быть двусмысленной или слишком узкой для вопроса.
\nУчебный код использует локальные значения и упрощённые labels. Он не проверяет реальную личность, RBAC, ABAC, SSO, журнал аудита, конкурентное обновление документа или кэш. Применять его как готовую authorization implementation нельзя. В реальной системе policy должен иметь владельца, а source version и правила expiry должны быть частью явного контракта.
\nОграничение есть и у даты. HTTP-поле Last-Modified сообщает время, когда origin считает представление изменённым. Оно не доказывает, что инженерное правило всё ещё применимо. Документ может не меняться и устареть из-за миграции. Поэтому freshness policy должна учитывать смысл вопроса, а не только HTTP timestamp.
Механизм готов к ограниченному применению, когда для одного зафиксированного query можно показать полный decision trace: candidates с score, source version, access result, freshness result, citation URI с anchor и итоговый human verification. На учебном отрицательном наборе expired, closed и no-anchor записи получают отдельные причины и не попадают в answer.
\nДополнительный критерий — повторный запуск с тем же retrievalAt даёт тот же набор допущенных записей, если corpus и policy не менялись. При пустом пересечении система возвращает stop status, а не догадку. Только после этого можно измерять top-k, reranking и стоимость запросов: эти настройки ускоряют поиск кандидатов, но не заменяют границу доказательства.