8 lines
20 KiB
JSON
8 lines
20 KiB
JSON
{
|
||
"index": 101,
|
||
"slug": "editorial-2025-03-mechanism-knowledge-retrieval",
|
||
"title": "Почему высокий vector score не доказывает ответ",
|
||
"excerpt": "Retrieval находит похожие фрагменты, но не подтверждает их свежесть, доступность и смысл. Разбираем границу между ranking и проверяемым источником.",
|
||
"contentHtml": "<p><strong>Проблема.</strong> Поиск по инженерной базе возвращает первый фрагмент с высоким vector score. Фрагмент похож на вопрос, поэтому ответ выглядит уверенно. Через несколько дней выясняется, что документ описывает старый контракт, закрыт для автора запроса или не содержит точного правила, на которое ссылается ответ.</p>\n<p><strong>Симптом.</strong> В результате есть title, excerpt и число вроде <code>0.98</code>, но нет версии источника, срока действия, access decision и anchor. Инженер меняет код по старой инструкции. <strong>Цена ошибки.</strong> Команда тратит время на повторное расследование: нужно найти нужную редакцию, выяснить права и отделить цитату от пересказа. Ошибка также подрывает доверие к базе знаний. Следующий хороший фрагмент начинают игнорировать вместе с плохим.</p>\n<p><strong>Тезис.</strong> Vector score решает одну задачу: упорядочивает похожие candidates в выбранной модели поиска. Он не доказывает истинность claim, право читать источник, актуальность документа и полноту найденных материалов. Поэтому retrieval должен завершаться не выбором top-1, а проверяемым решением: candidate допускается к ответу только после проверок access, freshness, provenance и exact citation.</p>\n<h2>Где заканчивается score</h2>\n<p>Nearest-neighbor поиск сравнивает запрос с векторами документов. Его результат помогает сократить очередь чтения. Это полезно, когда корпус велик и человек не может открыть все записи. Но score не знает, кто задаёт вопрос. Он не знает, отозвал ли владелец документ. Он не знает, поддерживает ли найденный абзац именно тот claim, который собирается сделать система.</p>\n<p>Порог похожести не исправляет эту границу. Представим учебный корпус из трёх записей. Просроченная инструкция получила score <code>0.97</code>, закрытое исключение — <code>0.99</code>, а текущая разрешённая инструкция — <code>0.91</code>. Фильтр <code>score >= 0.90</code> оставит все три. Сортировка поставит опасные записи выше нужной. Это не результат реального сервиса, а минимальный пример, который показывает, почему ranking нельзя использовать как authorization или evidence.</p>\n<table><caption>Что сообщает каждый сигнал</caption><thead><tr><th scope=\"col\">Сигнал</th><th scope=\"col\">Вопрос</th><th scope=\"col\">Действие</th><th scope=\"col\">Чего он не доказывает</th></tr></thead><tbody><tr><td>vectorScore</td><td>Насколько candidate похож на query?</td><td>Ставит запись в очередь проверки.</td><td>Что claim верен, свеж и доступен.</td></tr><tr><td>accessLabels</td><td>Разрешён ли класс источника requester scope?</td><td>Отбрасывает закрытый фрагмент.</td><td>Что выполнена вся реальная policy identity.</td></tr><tr><td>publishedAt / expiresAt</td><td>Входит ли запись в объявленное окно свежести?</td><td>Отбрасывает будущие и просроченные записи.</td><td>Что это единственная актуальная редакция.</td></tr><tr><td>citationUri#anchor</td><td>Можно ли открыть точное место в конкретной версии?</td><td>Делает candidate адресуемым.</td><td>Что источник поддерживает более широкий вывод.</td></tr><tr><td>human verification</td><td>Совпадает ли claim с открытым фрагментом?</td><td>Разрешает сформулировать ответ.</td><td>Что одна проверка заменяет владельца policy.</td></tr></tbody></table>\n<h2>Запись индекса должна сохранять происхождение</h2>\n<p>Chunking часто оставляет в индексе только текст и embedding. Заголовок, версия и срок действия остаются в исходном документе. После retrieval система показывает удобный snippet, но не может ответить на простой вопрос: из какой редакции он взят? Одинаковая фраза может встречаться в новой инструкции, старом RFC и закрытом исключении.</p>\n<p>Минимальная запись должна связывать chunk с source record. Source record хранит <code>sourceId</code>, <code>sourceVersion</code>, <code>owner</code>, <code>publishedAt</code> и URI. Chunk хранит <code>sourceId</code>, текстовый фрагмент, <code>citationAnchor</code>, <code>indexedAt</code>, <code>expiresAt</code> и access labels. Retrieval event сохраняет query, момент поиска, список candidates и причины отказа. Система может не передавать все поля в пользовательский интерфейс, но decision должен иметь к ним доступ.</p>\n<p>Если у chunk нет стабильной связи с редакцией, он остаётся подсказкой для поиска. Если у него нет anchor, он не становится точной цитатой. Если у него нет declared policy свежести, приложение не должно молча называть его текущим. Эти ограничения лучше показать явно, чем восстанавливать по смыслу из текста.</p>\n<figure><img src=\"/assets/editorial/2025/knowledge-retrieval-2025-freshness-access-matrix.svg\" alt=\"Матрица retrieval: свежая разрешённая запись проходит проверки, а записи с более высоким score отклоняются из-за срока, доступа или отсутствия anchor\" loading=\"lazy\" /><figcaption>Score сокращает очередь проверки. Матрица не объявляет самый похожий фрагмент доказательством: доступ, срок и адресуемая цитата имеют отдельные границы.</figcaption></figure>\n<h2>Проверяйте источник до составления ответа</h2>\n<p>Надёжный порядок начинается с evidence, а не с готового текста. Сначала система получает candidates и их metadata. Затем она применяет policy к каждому candidate. Только оставшиеся фрагменты передаются человеку или компоненту, который формулирует ответ. Так нельзя незаметно добавить в draft тезис, которого не было в источнике.</p>\n<pre><code>type 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.</code></pre>\n<p>Код показывает контракт, а не готовую систему доступа. Проверка <code>accessLabels.includes</code> не заменяет authentication, authorization policy и аудит. Сравнение дат работает только после фиксации формата и часового пояса. Human verification всё равно нужен: технически доступный anchor может содержать исключение, которое не подтверждает общий claim.</p>\n<h2>Симптом → причина → проверка → действие</h2>\n<table><caption>Диагностика retrieval без смешения сигналов</caption><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Причина</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>Top-1 отвечает на вопрос, но описывает старый контракт.</td><td>Ranking не учитывает срок действия или version policy.</td><td>Сравнить <code>publishedAt</code> и <code>expiresAt</code> с зафиксированным <code>retrievalAt</code>.</td><td>Отклонить просроченный candidate и показать актуальную редакцию либо остановить ответ.</td></tr><tr><td>Фрагмент релевантен, но его нельзя открыть.</td><td>Индекс получил текст без согласованной access boundary.</td><td>Проверить requester scope, labels и решение владельца ресурса.</td><td>Убрать candidate из answer path; не маскировать отказ новым пересказом.</td></tr><tr><td>Ссылка ведёт на страницу, но не на правило.</td><td>Сохранён URI без версии и anchor.</td><td>Открыть ссылку на том же snapshot и найти точный фрагмент.</td><td>Оставить запись кандидатом, пока источник не станет адресуемым.</td></tr><tr><td>Ответ звучит шире цитаты.</td><td>Генератор обобщил локальное исключение.</td><td>Сопоставить каждое предложение claim с одним или несколькими фрагментами.</td><td>Сузить формулировку, добавить limitation или отправить вопрос на ручную проверку.</td></tr><tr><td>После пустого результата система всё равно отвечает.</td><td>Fallback подменяет отсутствие evidence вероятным текстом.</td><td>Проверить negative path: все candidates expired, закрыты или без anchor.</td><td>Вернуть stop status с причиной и запросить источник или решение владельца.</td></tr></tbody></table>\n<h2>Отрицательный путь важнее удачного top-1</h2>\n<p>Проверять нужно не только случай, где нашлась хорошая запись. Система должна остановиться, если все candidates просрочены, закрыты или не имеют точного locator. Пустой результат честнее уверенного ответа без основания. Его можно объяснить и исправить: обновить документ, выдать доступ, добавить anchor или уточнить вопрос.</p>\n<p>Не смешивайте причины в один статус вроде <code>relevance_low</code>. <code>access_denied</code> ведёт к владельцу policy. <code>expired_at_retrieval</code> ведёт к владельцу документа. <code>missing_citation_anchor</code> ведёт к ingestion или структуре источника. Такое разделение экономит время и не толкает команду сразу менять embedding model.</p>\n<h2>Порядок внедрения проверки</h2>\n<ol><li>Зафиксируйте query, requester scope и <code>retrievalAt</code>. Один и тот же запрос должен иметь воспроизводимый срез времени.</li><li>Сохраните для каждого candidate id, score, source version и все причины будущего отказа. Не передавайте дальше только текстовый snippet.</li><li>Проверьте access до раскрытия фрагмента в answer path. Техническая доступность записи не равна праву показать её requester.</li><li>Примените freshness policy. Запишите, какое поле и какой срок считаются обязательными для operational question.</li><li>Проверьте provenance и citation. URI должен вести к нужной редакции, anchor — к месту, где действительно находится утверждение.</li><li>Сопоставьте claim с источником. Если источник поддерживает только условие или исключение, сузьте ответ до этой границы.</li><li>Если пересечение пусто, остановите ответ, верните понятный reason code и предложите следующий безопасный шаг.</li><li>Закрепите проверки на отрицательных примерах: высокий score у expired, закрытый record и candidate без anchor не должны попасть в citations.</li></ol>\n<h2>Ограничения</h2>\n<p>Эта схема не обещает, что vector search найдёт полный корпус. Она не сравнивает качество embedding-моделей и не утверждает, что keyword search всегда лучше. Она также не превращает metadata в доказательство смысла. Свежая доступная цитата может быть двусмысленной или слишком узкой для вопроса.</p>\n<p>Учебный код использует локальные значения и упрощённые labels. Он не проверяет реальную личность, RBAC, ABAC, SSO, журнал аудита, конкурентное обновление документа или кэш. Применять его как готовую authorization implementation нельзя. В реальной системе policy должен иметь владельца, а source version и правила expiry должны быть частью явного контракта.</p>\n<p>Ограничение есть и у даты. HTTP-поле <code>Last-Modified</code> сообщает время, когда origin считает представление изменённым. Оно не доказывает, что инженерное правило всё ещё применимо. Документ может не меняться и устареть из-за миграции. Поэтому freshness policy должна учитывать смысл вопроса, а не только HTTP timestamp.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Механизм готов к ограниченному применению, когда для одного зафиксированного query можно показать полный decision trace: candidates с score, source version, access result, freshness result, citation URI с anchor и итоговый human verification. На учебном отрицательном наборе expired, closed и no-anchor записи получают отдельные причины и не попадают в answer.</p>\n<p>Дополнительный критерий — повторный запуск с тем же <code>retrievalAt</code> даёт тот же набор допущенных записей, если corpus и policy не менялись. При пустом пересечении система возвращает stop status, а не догадку. Только после этого можно измерять top-k, reranking и стоимость запросов: эти настройки ускоряют поиск кандидатов, но не заменяют границу доказательства.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://raw.githubusercontent.com/opensearch-project/k-NN/150c589849a8ec3bc442d830b43a3eaf4e25fa0c/README.md\" target=\"_blank\" rel=\"noopener noreferrer\">OpenSearch k-NN README, immutable commit 150c589</a> — официальное описание nearest-neighbor similarity search и фильтров. Источник подтверждает механику поиска кандидатов, но не право доступа, свежесть или истинность claim.</li><li><a href=\"https://www.rfc-editor.org/rfc/rfc9110.html#section-8.8.2\" target=\"_blank\" rel=\"noopener noreferrer\">RFC 9110, раздел 8.8.2 Last-Modified</a> — стандартное описание времени изменения выбранного HTTP-представления. Поле не является доказательством семантической актуальности документа.</li><li><a href=\"https://csrc.nist.gov/pubs/sp/800/207/final\" target=\"_blank\" rel=\"noopener noreferrer\">NIST SP 800-207: Zero Trust Architecture</a> — официальный принцип отсутствия неявного доверия и отдельной проверки authorization перед доступом к ресурсу. Документ не задаёт схему vector index или формат citation.</li></ul>"
|
||
}
|