8 lines
22 KiB
JSON
8 lines
22 KiB
JSON
{
|
||
"index": 101,
|
||
"slug": "editorial-2025-03-mechanism-knowledge-retrieval",
|
||
"title": "Почему высокий vector score не доказывает ответ",
|
||
"excerpt": "Vector search ранжирует похожие фрагменты, но не подтверждает их смысл, свежесть и право показа. Разбираем decision trace, отрицательный путь и проверку источника до ответа.",
|
||
"contentHtml": "<p><strong>Проблема.</strong> Поиск по инженерной базе возвращает фрагмент с vector score <code>0.98</code>, и система сразу строит из него ответ. Через несколько дней выясняется, что фрагмент относится к старой редакции API, закрыт для автора запроса или содержит исключение, а не общее правило.</p>\n<p><strong>Симптом.</strong> В интерфейсе есть title, excerpt и число похожести, но нет версии источника, результата проверки доступа, срока действия и точного места цитаты. Инженер меняет код по устаревшей инструкции. Цена ошибки — повторное расследование и потеря доверия к базе знаний: после нескольких ложных попаданий команда начинает игнорировать и полезные результаты.</p>\n<p><strong>Вывод.</strong> Vector score решает задачу ranking: ставит candidates в порядок по выбранной моделью поиска мере близости. Он не является вероятностью истины и не доказывает, что claim (утверждение в ответе) поддержан источником. Перед генерацией нужно получить отдельное решение по доступу, свежести и происхождению. Если пересечение условий пусто, система должна остановиться, а не заполнять пробел правдоподобным текстом.</p>\n<h2>Что именно измеряет score</h2>\n<p>Vector query сравнивает вектор запроса с векторами документов и возвращает ближайшие результаты по настроенной similarity metric. Такая сортировка сокращает объём чтения: вместо тысячи документов инженер получает несколько candidates. Это полезный этап поиска, но не этап доказательства.</p>\n<p>Само число зависит от реализации. При cosine similarity, dot product и L2 distance используются разные шкалы и преобразования. Даже один движок может считать итоговый score по-разному для разных типов полей и запросов. Поэтому порог <code>score >= 0.90</code> нельзя переносить между моделями, индексами и метриками без размеченного набора проверок. Значения <code>0.98</code>, <code>0.91</code> и другие числа ниже — учебные, не измерение качества конкретного сервиса.</p>\n<p>Score также не знает контекст requester. Он не видит, отозвал ли владелец документ, и не умеет определить, что цитата отвечает только на частный случай. Эти свойства должны приходить из источника и policy, а не угадываться из текста фрагмента.</p>\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><code>vectorScore</code></td><td>Насколько candidate близок к query по выбранной метрике?</td><td>Отсортировать очередь чтения.</td><td>Истинность claim, право доступа и актуальность.</td></tr><tr><td><code>sourceVersion</code></td><td>Из какой редакции получен chunk?</td><td>Сопоставить результат с известным snapshot.</td><td>Что редакция подходит к данному вопросу.</td></tr><tr><td><code>accessDecision</code></td><td>Может ли этот requester увидеть ресурс?</td><td>Исключить закрытый candidate из answer path.</td><td>Что открытый текст подтверждает claim.</td></tr><tr><td><code>expiresAt</code></td><td>Не вышел ли source за объявленный срок?</td><td>Отклонить устаревшую запись или направить к владельцу.</td><td>Что документ семантически полон и верен.</td></tr><tr><td><code>citationAnchor</code></td><td>Можно ли открыть точное место в нужной версии?</td><td>Показать адресуемую цитату.</td><td>Что узкий фрагмент поддерживает более широкий вывод.</td></tr></tbody></table>\n<h2>Происхождение нужно сохранить рядом с chunk</h2>\n<p>При индексации документ обычно разбивают на chunks. Если сохранить только текст и embedding, после поиска останется удобный snippet без ответа на вопрос «из какой редакции он взят?». Одинаковая фраза может встретиться в текущем руководстве, старом RFC и закрытом исключении.</p>\n<p>Минимальный контракт связывает каждую запись с исходным документом. У source record есть <code>sourceId</code>, <code>sourceVersion</code>, владелец, статус публикации и URI. У chunk есть тот же <code>sourceId</code>, текст, anchor, время индексации и access labels. У retrieval event есть зафиксированные query, requester scope, <code>retrievalAt</code>, список candidates и причины отказа. Пользовательскому интерфейсу не обязательно показывать всё, но компонент, принимающий решение, должен иметь эти поля.</p>\n<p>Не стоит восстанавливать происхождение по заголовку или совпадению текста. При обновлении документа старый chunk должен быть выведен из индекса или помечен как недопустимый для текущего вопроса. Иначе высокий score может быть честным результатом поиска по неправильному snapshot.</p>\n<figure><img src=\"/assets/editorial/2025/knowledge-retrieval-2025-freshness-access-matrix.svg\" alt=\"Матрица допуска к ответу: запись с score 0.91 проходит при разрешённом доступе, действующем сроке и точном anchor, а записи с более высоким score отклоняются\" loading=\"lazy\" /><figcaption>В учебной матрице score 0.98 у закрытой записи и score 0.96 у просроченной записи не меняют вердикт: answer path допускает только пересечение access, freshness и exact citation.</figcaption></figure>\n<h2>Воспроизводимый decision trace</h2>\n<p>Решение полезно представлять не флагом <code>relevant: true</code>, а набором проверяемых полей. Ниже — самостоятельный пример на Node.js без внешних пакетов. Он фиксирует момент поиска, применяет одну policy к четырём синтетическим candidates и печатает причины отказа. Команда не моделирует identity provider или настоящую БД; она воспроизводит именно порядок принятия решения.</p>\n<pre><code>node <<'NODE'\nconst retrievalAt = new Date('2025-03-18T12:00:00Z');\nconst requester = 'engineering';\nconst candidates = [\n { id: 'fresh-v3', score: 0.91, access: ['engineering'],\n publishedAt: '2025-03-01T00:00:00Z', expiresAt: '2025-04-01T00:00:00Z',\n sourceVersion: 'v3', citation: 'https://docs.example.test/runbook/v3#rollback' },\n { id: 'expired-v1', score: 0.96, access: ['engineering'],\n publishedAt: '2024-01-01T00:00:00Z', expiresAt: '2025-01-01T00:00:00Z',\n sourceVersion: 'v1', citation: 'https://docs.example.test/runbook/v1#rollback' },\n { id: 'closed-v2', score: 0.98, access: ['security'],\n publishedAt: '2025-02-01T00:00:00Z', expiresAt: '2025-04-01T00:00:00Z',\n sourceVersion: 'v2', citation: 'https://docs.example.test/runbook/v2#rollback' },\n { id: 'no-anchor', score: 0.89, access: ['engineering'],\n publishedAt: '2025-03-01T00:00:00Z', expiresAt: '2025-04-01T00:00:00Z',\n sourceVersion: 'v3', citation: null },\n];\n\nfunction decide(candidate) {\n const accessOk = candidate.access.includes(requester);\n const date = retrievalAt.getTime();\n const fresh = Date.parse(candidate.publishedAt) <= date &&\n (!candidate.expiresAt || date < Date.parse(candidate.expiresAt));\n const citable = Boolean(candidate.sourceVersion && candidate.citation);\n const reasons = [];\n if (!accessOk) reasons.push('access_denied');\n if (!fresh) reasons.push('expired_at_retrieval');\n if (!citable) reasons.push('missing_citation');\n return { id: candidate.id, score: candidate.score,\n ok: reasons.length === 0, reasons };\n}\n\nconsole.table(candidates.sort((a, b) => b.score - a.score).map(decide));\nNODE</code></pre>\n<p>Ожидаемый результат: <code>closed-v2</code> окажется первым по score, но получит <code>access_denied</code>; <code>expired-v1</code> — <code>expired_at_retrieval</code>; <code>no-anchor</code> — <code>missing_citation</code>. В answer path попадёт только <code>fresh-v3</code>. Если убрать его из массива, код не должен выбирать следующий candidate: приложение должно вернуть stop-статус с причиной <code>no_admissible_evidence</code>.</p>\n<h2>Проверяйте три независимые границы</h2>\n<p><strong>Доступ.</strong> Проверяйте requester scope до передачи фрагмента генератору. Даже если индекс технически вернул закрытый текст, это не разрешает показать его пользователю или пересказать его содержание. Проверка массива labels в примере — только замена policy для демонстрации. В реальном сервисе authorization должен опираться на подтверждённую identity и правила владельца ресурса.</p>\n<p><strong>Свежесть.</strong> Сравнивайте фиксированный <code>retrievalAt</code> с правилами домена: сроком действия, статусом публикации и версией контракта. HTTP <code>Last-Modified</code> сообщает время, когда origin считает выбранное представление изменённым. Он не говорит, что правило всё ещё подходит к эксплуатационному вопросу. Документ может не меняться, но стать неприменимым после миграции.</p>\n<p><strong>Происхождение и смысл.</strong> Открываем URI в той же редакции и проверяем anchor. Затем сопоставляем каждое предложение ответа с фрагментом. Доступная ссылка может вести на исключение, а не на общее правило; наличие anchor не превращает локальное условие в универсальный вывод.</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>Top-1 описывает старый API-контракт.</td><td>Ranking не учитывает version или expiry policy.</td><td>Сравнить source version, опубликованный статус и <code>retrievalAt</code>.</td><td>Отклонить candidate и вернуть текущую редакцию либо stop.</td></tr><tr><td>Релевантный текст нельзя открыть.</td><td>Индекс и answer path используют разные access boundaries.</td><td>Сверить requester scope с policy владельца ресурса.</td><td>Не раскрывать фрагмент и записать <code>access_denied</code>.</td></tr><tr><td>Ссылка открывается, но правило не находится.</td><td>Сохранён URI без версии или стабильного anchor.</td><td>Открыть тот же snapshot и проверить точное место.</td><td>Оставить candidate вне citations до исправления ingestion.</td></tr><tr><td>Ответ шире приведённой цитаты.</td><td>Генератор обобщил условие или исключение.</td><td>Разметить claim-to-source mapping по предложениям.</td><td>Сузить ответ, добавить ограничение или передать вопрос человеку.</td></tr><tr><td>Все candidates отфильтрованы, но ответ всё равно есть.</td><td>Fallback подменяет отсутствие evidence вероятным текстом.</td><td>Запустить negative case с expired, закрытым и no-anchor набором.</td><td>Вернуть <code>no_admissible_evidence</code>, а не догадку.</td></tr></tbody></table>\n<h2>Почему отрицательный путь обязателен</h2>\n<p>Удачный top-1 показывает лишь, что поиск нашёл похожий текст. Надёжность видна в обратной ситуации: все candidates просрочены, закрыты или не имеют точного locator. Пустой ответ можно объяснить и исправить — обновить документ, запросить доступ, добавить anchor или уточнить вопрос. Уверенный ответ без основания скрывает причину сбоя.</p>\n<p>Разделяйте reason codes. <code>access_denied</code> направляет к владельцу policy, <code>expired_at_retrieval</code> — к владельцу документа, <code>missing_citation</code> — к ingestion или разметке источника, а <code>relevance_low</code> — к качеству retrieval. Такая диагностика не заставляет команду менять embedding model, когда проблема находится в правах или жизненном цикле документа.</p>\n<h2>Порядок внедрения</h2>\n<ol><li>Зафиксируйте query, requester scope и <code>retrievalAt</code>. Без момента проверки нельзя воспроизвести решение по сроку.</li><li>Сохраните у candidate id, score, source version, URI, anchor и исходные metadata. Не передавайте дальше только snippet.</li><li>Примените access policy до раскрытия текста генератору или пользователю. Ошибка индекса не должна становиться утечкой.</li><li>Примените freshness policy и явно назовите владельца срока. Если срок не определён, не называйте документ текущим молча.</li><li>Проверьте provenance: URI должен вести к нужной редакции, а anchor — к месту, подтверждающему claim.</li><li>Сопоставьте claim с источником. Условие и исключение должны остаться условием и исключением в ответе.</li><li>При пустом пересечении верните stop-статус и конкретный reason code. Не запускайте скрытый fallback на «наиболее похожий» текст.</li><li>Закрепите тесты на отрицательных данных и повторите запуск с тем же <code>retrievalAt</code> при неизменных corpus и policy.</li></ol>\n<h2>Границы применимости</h2>\n<p>Эта схема не доказывает полноту поиска, не выбирает лучшую embedding model и не заменяет оценку retrieval на размеченных вопросах. Она также не утверждает, что keyword search всегда лучше vector search. Score помогает ранжировать candidates; качество покрытия корпуса, reranking и стоимость запроса нужно измерять отдельно.</p>\n<p>Учебный код не реализует authentication, RBAC, ABAC, SSO, аудит, отзыв сессий, конкурентное обновление документов или защиту кэша. В нём намеренно используются домен <code>example.test</code>, четыре записи и упрощённый массив access labels. Перед применением в сервисе нужно описать policy, владельцев полей, формат дат и поведение при отсутствии версии или anchor.</p>\n<p>Критерий готовности ограниченного запуска — полный decision trace для фиксированного запроса: candidates со score, access result, freshness result, source version, citation URI с anchor и результат проверки claim. В отрицательном наборе expired, closed и no-anchor записи должны получать разные причины и не попадать в citations.</p>\n<h2>Проверяемые источники</h2><ul><li><a href=\"https://www.elastic.co/guide/en/elasticsearch/reference/current/vector-queries.html\" target=\"_blank\" rel=\"noopener noreferrer\">Elastic: Vector queries</a> — официальное описание поиска ближайших векторов по similarity metric. Источник подтверждает роль vector query как поиска и ранжирования, но не задаёт правила доступа к документу.</li><li><a href=\"https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-dense-vector-query\" target=\"_blank\" rel=\"noopener noreferrer\">Elastic: Dense vector query</a> — показывает, что similarity function и способ подсчёта score задаются контрактом поля и запроса. Поэтому учебные числа нельзя сравнивать между разными конфигурациями без проверки.</li><li><a href=\"https://www.rfc-editor.org/rfc/rfc9110.html#section-8.8.2\" target=\"_blank\" rel=\"noopener noreferrer\">RFC 9110, section 8.8.2 Last-Modified</a> — определяет поле как timestamp, отражающий мнение origin о времени изменения выбранного представления. Это не стандарт семантической свежести инженерного правила.</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> — разделяет authentication и authorization и не предполагает доверие только из-за расположения ресурса или аккаунта. Документ не задаёт формат vector index или citation.</li></ul>"
|
||
}
|