8 lines
23 KiB
JSON
8 lines
23 KiB
JSON
{
|
||
"index": 102,
|
||
"slug": "editorial-2025-03-practice-knowledge-retrieval",
|
||
"title": "Поиск по инженерной базе: как не превратить похожий текст в доказательство",
|
||
"excerpt": "Практический маршрут для retrieval по инженерной документации: сначала проверить версию, срок, права и точную цитату, а затем решать, можно ли отвечать.",
|
||
"contentHtml": "<p>Инженер задаёт вопрос по внутренней документации. Поиск возвращает фрагмент с высоким score: заголовок совпадает, формулировка знакома, ответ можно написать за минуту. Позже выясняется, что фрагмент относится к старой редакции контракта, закрыт для этого читателя или ведёт только на страницу без нужного абзаца. Решение уже принято: изменён адаптер, в ответ попал закрытый материал, а reviewer не может быстро восстановить источник.</p><p>Цена ошибки — откат, повторное расследование и потеря доверия к базе знаний. Пустой результат виден сразу. Уверенный пересказ устаревшего правила — нет. Поэтому результат поиска должен пройти три независимых вопроса: имеет ли читатель право использовать запись, применима ли она в зафиксированный момент и можно ли открыть точное место, подтверждающее claim. Score помогает выбрать, что читать первым, но сам по себе ничего из этого не доказывает.</p>\n<h2>Граница задачи: кандидат — ещё не источник</h2>\n<p>Семантический или векторный поиск упорядочивает документы по близости представлений. В OpenSearch, например, запрос возвращает ближайшие векторы и параметр <code>k</code> задаёт число кандидатов. Это полезный механизм retrieval: он сокращает очередь чтения. Но score зависит от модели, метрики и индекса; это не вероятность истинности и не разрешение раскрыть текст.</p>\n<p>Надёжный маршрут выглядит так: <strong>query → candidates → policy checks → exact citation → human verification</strong>. Поиск отвечает на вопрос «что может быть связано с запросом». Политика отвечает на вопрос «что разрешено использовать». Проверка источника отвечает на вопрос «подтверждает ли этот фрагмент конкретное утверждение». Если обязательные условия не выполняются, результатом должен быть остановленный ответ с причиной, а не правдоподобный fallback.</p>\n<h2>Контракт записи индекса</h2>\n<p>Для проверки недостаточно сохранить chunk, embedding и score. Каждый фрагмент должен быть связан с устойчивой записью источника. Минимальный контракт может содержать <code>recordId</code>, <code>sourceRevision</code>, <code>sourceUri</code>, <code>citationAnchor</code>, <code>publishedAt</code>, <code>indexedAt</code>, <code>retrievedAt</code>, область доступа и <code>vectorScore</code>. Время следует хранить в RFC 3339 и сравнивать в одной временной шкале.</p>\n<p><code>publishedAt</code> говорит, когда редакция была опубликована. <code>indexedAt</code> позволяет измерить задержку доставки в индекс. <code>retrievedAt</code> фиксирует срез, на котором принято решение. <code>expiresAt</code>, если он используется, — это внутренняя политика пересмотра, а не универсальное свойство документа. Ссылка и anchor должны вести к конкретной редакции и месту, которое reviewer может открыть.</p>\n<p>После chunking метаданные не должны теряться. Если версия и права лежат только в исходном хранилище, а индекс отдаёт независимый snippet, сервис обязан однозначно разрешать <code>recordId</code> обратно в source record и повторно проверять авторизацию. Индекс может ускорять поиск, но не должен становиться неявной копией security boundary. Отклонённые записи полезно сохранять в диагностике по идентификатору и причине, не показывая закрытый excerpt.</p>\n<div class='table-scroll'><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>Высокий score, но нет sourceRevision</td><td>Есть похожий chunk без доказуемой редакции</td><td>Разрешить recordId в хранилище источника и проверить provenance</td><td>Оставить только кандидатом, не цитировать</td></tr><tr><td>expiresAt раньше retrievedAt</td><td>Фрагмент не проходит объявленную policy свежести</td><td>Сравнить даты в UTC и проверить часовой срез</td><td>Отклонить, запросить действующую редакцию</td></tr><tr><td>Scope читателя не пересекается с accessLabels</td><td>Найденный текст не разрешён этому requester</td><td>Проверить policy до раскрытия excerpt</td><td>Не показывать текст, вернуть безопасную причину</td></tr><tr><td>Есть sourceUri, но нет citationAnchor</td><td>Ссылку нельзя сопоставить с claim</td><td>Открыть источник и найти стабильный раздел или якорь</td><td>Остановить ответ до адресуемой цитаты</td></tr><tr><td>Все проверки пройдены, но claim не совпадает по смыслу</td><td>Технический допуск не равен семантическому подтверждению</td><td>Сопоставить формулировку claim с точным фрагментом</td><td>Изменить claim, найти другой источник или остановиться</td></tr></tbody></table></div>\n<h2>Воспроизводимый пример: допуск до генерации</h2>\n<p>Ниже — полностью синтетический пример на JavaScript. В нём нет сети, часов операционной системы, реального identity provider или production-данных. Функция сортирует кандидатов только после фильтрации и возвращает явную причину отказа. В настоящем сервисе <code>accessLabels</code> должны приходить из доверенной policy, а не из пользовательского запроса.</p>\n<pre><code>const retrievedAt = '2025-03-17T10:00:00Z';\nconst requesterLabels = new Set(['engineering-read']);\n\nconst candidates = [\n {\n recordId: 'adapter-v1',\n sourceRevision: 'v1',\n publishedAt: '2025-02-01T09:00:00Z',\n expiresAt: '2025-03-01T00:00:00Z',\n accessLabels: ['engineering-read'],\n citationUri: 'https://docs.example.test/adapter',\n citationAnchor: '#old-field',\n vectorScore: 0.97,\n },\n {\n recordId: 'adapter-v3',\n sourceRevision: 'v3',\n publishedAt: '2025-03-10T09:00:00Z',\n expiresAt: '2025-04-01T00:00:00Z',\n accessLabels: ['engineering-read'],\n citationUri: 'https://docs.example.test/adapter',\n citationAnchor: '#schema-upgrade',\n vectorScore: 0.91,\n },\n];\n\nfunction admitCandidate(item) {\n if (!item.sourceRevision) return { ok: false, reason: 'missing-revision' };\n if (Date.parse(item.expiresAt) <= Date.parse(retrievedAt)) {\n return { ok: false, reason: 'expired-at-retrieval-time' };\n }\n if (!item.accessLabels.some((label) => requesterLabels.has(label))) {\n return { ok: false, reason: 'access-label-not-granted' };\n }\n if (!item.citationUri || !item.citationAnchor) {\n return { ok: false, reason: 'missing-exact-citation' };\n }\n return { ok: true, item };\n}\n\nconst checked = candidates.map(admitCandidate);\nconst allowed = checked\n .filter((result) => result.ok)\n .map((result) => result.item)\n .sort((a, b) => b.vectorScore - a.vectorScore);\n\nconst decision = allowed.length\n ? { status: 'needs-human-verification', evidence: allowed[0] }\n : { status: 'stop', reasons: checked.filter((x) => !x.ok).map((x) => x.reason) };\n\nconsole.log(decision);</code></pre>\n<p>У <code>adapter-v1</code> score выше, но редакция истекла до <code>retrievedAt</code>, поэтому первым допустимым evidence становится <code>adapter-v3</code>. Это ещё не автоматическое подтверждение ответа: reviewer должен открыть <code>citationUri#schema-upgrade</code> и убедиться, что фрагмент действительно подтверждает нужную замену. Если разрешённых и адресуемых записей нет, система получает <code>stop</code>. Она не должна возвращаться к старой версии только потому, что та лучше ранжирована.</p>\n<h2>Freshness: дата индекса не делает правило действующим</h2>\n<p>Своевременность нужно определить до внедрения поиска. Для части документации достаточно версии источника и даты публикации. Для быстро меняющегося контракта может понадобиться срок пересмотра, статус deprecation, effective date или проверка владельца. Важно записать policy словами и тестами: например, «после expiresAt запись не участвует в генерации».</p>\n<p>HTTP-метаданные могут быть дополнительным сигналом. RFC 9110 описывает <code>Last-Modified</code> как время, когда origin считает изменённым выбранное представление. Это не равно дате вступления инженерного правила в силу, не описывает права читателя и не доказывает смысл claim. Не следует подменять внутреннюю version документа заголовком ответа CDN или датой попадания chunk в индекс.</p>\n<p>Для воспроизводимости сохраняйте входной <code>retrievedAt</code> и фактически применённую policy. Тогда повторная проверка может ответить, почему вчерашний кандидат был допущен, а сегодняшняя редакция — нет. Если сервис использует eventual consistency и источник читается с реплики, это отдельное ограничение: нужно назвать возможное окно рассогласования и не обещать строгую свежесть без соответствующей гарантии хранилища.</p>\n<h2>Доступ: фильтровать нужно до раскрытия фрагмента</h2>\n<p>Проверку прав нельзя оставлять генератору. Генератор получает только evidence, которые уже прошли authoritative authorization для конкретного requester. Фильтр по метке в поисковом индексе может быть оптимизацией, но не заменяет проверку в системе, владеющей политикой доступа. Иначе ошибка индекса или устаревшая роль может раскрыть excerpt ещё до того, как сервис вернёт отказ.</p>\n<p>Разделяйте безопасный ответ и диагностическую информацию. Пользователю можно сообщить, что подходящий источник недоступен или требует другого доступа. Внутреннему журналу можно записать <code>recordId</code>, policy version и код причины. Не кладите в журнал сам закрытый фрагмент без отдельного основания: диагностический канал тоже имеет владельца и срок хранения.</p>\n<h2>Цитата — это связь claim и места в редакции</h2>\n<p>Ссылка на главную страницу не является точной цитатой. Минимальная citation должна позволять восстановить source revision, URI и anchor; для Markdown или HTML это может быть стабильный заголовок, номер раздела или диапазон строк в immutable-документе. Если источник перемещается, храните идентификатор редакции и проверяйте, что anchor по-прежнему разрешается.</p>\n<p>Пишите ответ только после отбора evidence. Удобный формат — таблица claims: для каждого утверждения указать ссылку, фрагмент, ограничение и статус human verification. Если один фрагмент подтверждает только часть предложения, разделите claim или ослабьте формулировку. Citation, добавленная в конце уже готового текста, не исправляет вывод, который возник без источника.</p>\n<figure><img src='/assets/editorial/2025/knowledge-retrieval-2025-query-retrieval-citation.svg' alt='Схема маршрута от запроса через поиск кандидатов и проверки доступа и свежести к точной цитате; при отсутствии условия ответ останавливается.' loading='lazy' /><figcaption>Score сокращает список для чтения. До ответа доходят только свежая, разрешённая и адресуемая запись, после чего человек проверяет её смысл.</figcaption></figure>\n<h2>Порядок проверки в проекте</h2>\n<ol><li><strong>Опишите scope.</strong> Зафиксируйте query, requester, область документации, источник времени и ожидаемый claim. Не меняйте эти входы во время расследования.</li><li><strong>Сохраните retrieval trace.</strong> Запишите top-k, score, recordId и полный набор metadata. Один текстовый snippet без provenance не подходит для аудита.</li><li><strong>Разрешите источник.</strong> По recordId получите authoritative revision, владельца и статус. Если связь не восстанавливается, остановите использование кандидата.</li><li><strong>Проверьте доступ.</strong> Примените policy к requester до показа excerpt. Отдельно проверьте, что индекс не стал единственным местом принятия решения о правах.</li><li><strong>Проверьте свежесть.</strong> Сравните version и даты с retrievedAt по заранее объявленной policy. Зафиксируйте eventual-consistency window, если он существует.</li><li><strong>Проверьте цитату.</strong> Откройте URI и anchor именно в нужной редакции. Убедитесь, что фрагмент подтверждает claim, а не просто содержит похожие слова.</li><li><strong>Сформируйте ответ.</strong> Передайте генератору только допущенные evidence и их ограничения. Для каждого claim сохраните citation и статус human verification.</li><li><strong>Проверьте отрицательные cases.</strong> Прогоните устаревший high-score, закрытый, безверсийный и без-anchor кандидаты. В каждом случае ответ не должен раскрывать запрещённый текст.</li></ol>\n<h2>Тестовый критерий готовности</h2>\n<p>Маршрут можно считать проверенным на базовом уровне, если один и тот же набор входов даёт четыре предсказуемых исхода: свежая разрешённая запись с anchor проходит технический допуск и помечается как <code>needs-human-verification</code>; просроченная запись получает <code>expired-at-retrieval-time</code>; закрытая — <code>access-label-not-granted</code>; запись без anchor — <code>missing-exact-citation</code>. В последних трёх случаях excerpt не попадает в генерацию.</p>\n<p>Повторный запуск с теми же query, <code>retrievedAt</code>, policy и записями должен дать тот же decision. После этого отдельно измеряйте recall, latency и качество ранжирования. Эти метрики улучшают очередь кандидатов, но не превращают score в доказательство и не заменяют проверку claim.</p>\n<h2>Ограничения применимости</h2>\n<p>Описанный маршрут не доказывает полноту корпуса, качество embeddings, корректность модели, истинность утверждения или отсутствие всех уязвимостей. Он не говорит, что vector search лучше keyword search. Он задаёт более узкую гарантию: система не использует найденный фрагмент как evidence, пока не проверены его provenance, доступ, локальная свежесть и адресуемая цитата.</p>\n<p>Синтетический код не является production-библиотекой: в нём нет транзакций, конкурентного обновления policy, ревокации ролей, подписи источника, кешей и сетевых ошибок. В реальном проекте нужно отдельно проверить escaping, изоляцию tenant, права на логи, версионирование policy и поведение при недоступности authoritative source. Если policy неизвестна, нельзя молча выбрать allow или deny как доказанный результат: нужно назначить владельца правила и вернуть stop condition.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href='https://docs.opensearch.org/latest/vector-search/' target='_blank' rel='noopener noreferrer'>OpenSearch Documentation: Vector search</a> — официальная документация показывает хранение embeddings, поиск ближайших векторов, параметр <code>k</code> и отдельные этапы индексации и поиска. Она подтверждает механику retrieval, но не истинность claim и не права читателя.</li><li><a href='https://www.rfc-editor.org/rfc/rfc9110.html#section-8.8.2' target='_blank' rel='noopener noreferrer'>IETF RFC 9110, section 8.8.2: Last-Modified</a> — нормативное описание HTTP-поля, связанного с изменением выбранного представления. RFC не задаёт внутренний срок действия инженерной документации.</li><li><a href='https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-207.pdf' target='_blank' rel='noopener noreferrer'>NIST SP 800-207: Zero Trust Architecture</a> — официальный документ NIST о явной проверке доступа к ресурсам и отказе от неявного доверия. Он не задаёт конкретный формат vector index или citation.</li></ul>"
|
||
}
|