{ "index": 102, "slug": "editorial-2025-03-practice-knowledge-retrieval", "title": "Поиск по инженерной базе: как не превратить похожий текст в доказательство", "excerpt": "Практический маршрут для retrieval по инженерной документации: сначала проверить версию, срок, права и точную цитату, а затем решать, можно ли отвечать.", "contentHtml": "
Инженер задаёт вопрос по внутренней документации. Поиск возвращает фрагмент с высоким score: заголовок совпадает, формулировка знакома, ответ можно написать за минуту. Позже выясняется, что фрагмент относится к старой редакции контракта, закрыт для этого читателя или ведёт только на страницу без нужного абзаца. Решение уже принято: изменён адаптер, в ответ попал закрытый материал, а reviewer не может быстро восстановить источник.
Цена ошибки — откат, повторное расследование и потеря доверия к базе знаний. Пустой результат виден сразу. Уверенный пересказ устаревшего правила — нет. Поэтому результат поиска должен пройти три независимых вопроса: имеет ли читатель право использовать запись, применима ли она в зафиксированный момент и можно ли открыть точное место, подтверждающее claim. Score помогает выбрать, что читать первым, но сам по себе ничего из этого не доказывает.
\nСемантический или векторный поиск упорядочивает документы по близости представлений. В OpenSearch, например, запрос возвращает ближайшие векторы и параметр k задаёт число кандидатов. Это полезный механизм retrieval: он сокращает очередь чтения. Но score зависит от модели, метрики и индекса; это не вероятность истинности и не разрешение раскрыть текст.
Надёжный маршрут выглядит так: query → candidates → policy checks → exact citation → human verification. Поиск отвечает на вопрос «что может быть связано с запросом». Политика отвечает на вопрос «что разрешено использовать». Проверка источника отвечает на вопрос «подтверждает ли этот фрагмент конкретное утверждение». Если обязательные условия не выполняются, результатом должен быть остановленный ответ с причиной, а не правдоподобный fallback.
\nДля проверки недостаточно сохранить chunk, embedding и score. Каждый фрагмент должен быть связан с устойчивой записью источника. Минимальный контракт может содержать recordId, sourceRevision, sourceUri, citationAnchor, publishedAt, indexedAt, retrievedAt, область доступа и vectorScore. Время следует хранить в RFC 3339 и сравнивать в одной временной шкале.
publishedAt говорит, когда редакция была опубликована. indexedAt позволяет измерить задержку доставки в индекс. retrievedAt фиксирует срез, на котором принято решение. expiresAt, если он используется, — это внутренняя политика пересмотра, а не универсальное свойство документа. Ссылка и anchor должны вести к конкретной редакции и месту, которое reviewer может открыть.
После chunking метаданные не должны теряться. Если версия и права лежат только в исходном хранилище, а индекс отдаёт независимый snippet, сервис обязан однозначно разрешать recordId обратно в source record и повторно проверять авторизацию. Индекс может ускорять поиск, но не должен становиться неявной копией security boundary. Отклонённые записи полезно сохранять в диагностике по идентификатору и причине, не показывая закрытый excerpt.
| Наблюдение | Что это означает | Проверка | Решение |
|---|---|---|---|
| Высокий score, но нет sourceRevision | Есть похожий chunk без доказуемой редакции | Разрешить recordId в хранилище источника и проверить provenance | Оставить только кандидатом, не цитировать |
| expiresAt раньше retrievedAt | Фрагмент не проходит объявленную policy свежести | Сравнить даты в UTC и проверить часовой срез | Отклонить, запросить действующую редакцию |
| Scope читателя не пересекается с accessLabels | Найденный текст не разрешён этому requester | Проверить policy до раскрытия excerpt | Не показывать текст, вернуть безопасную причину |
| Есть sourceUri, но нет citationAnchor | Ссылку нельзя сопоставить с claim | Открыть источник и найти стабильный раздел или якорь | Остановить ответ до адресуемой цитаты |
| Все проверки пройдены, но claim не совпадает по смыслу | Технический допуск не равен семантическому подтверждению | Сопоставить формулировку claim с точным фрагментом | Изменить claim, найти другой источник или остановиться |
Ниже — полностью синтетический пример на JavaScript. В нём нет сети, часов операционной системы, реального identity provider или production-данных. Функция сортирует кандидатов только после фильтрации и возвращает явную причину отказа. В настоящем сервисе accessLabels должны приходить из доверенной policy, а не из пользовательского запроса.
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);\nУ adapter-v1 score выше, но редакция истекла до retrievedAt, поэтому первым допустимым evidence становится adapter-v3. Это ещё не автоматическое подтверждение ответа: reviewer должен открыть citationUri#schema-upgrade и убедиться, что фрагмент действительно подтверждает нужную замену. Если разрешённых и адресуемых записей нет, система получает stop. Она не должна возвращаться к старой версии только потому, что та лучше ранжирована.
Своевременность нужно определить до внедрения поиска. Для части документации достаточно версии источника и даты публикации. Для быстро меняющегося контракта может понадобиться срок пересмотра, статус deprecation, effective date или проверка владельца. Важно записать policy словами и тестами: например, «после expiresAt запись не участвует в генерации».
\nHTTP-метаданные могут быть дополнительным сигналом. RFC 9110 описывает Last-Modified как время, когда origin считает изменённым выбранное представление. Это не равно дате вступления инженерного правила в силу, не описывает права читателя и не доказывает смысл claim. Не следует подменять внутреннюю version документа заголовком ответа CDN или датой попадания chunk в индекс.
Для воспроизводимости сохраняйте входной retrievedAt и фактически применённую policy. Тогда повторная проверка может ответить, почему вчерашний кандидат был допущен, а сегодняшняя редакция — нет. Если сервис использует eventual consistency и источник читается с реплики, это отдельное ограничение: нужно назвать возможное окно рассогласования и не обещать строгую свежесть без соответствующей гарантии хранилища.
Проверку прав нельзя оставлять генератору. Генератор получает только evidence, которые уже прошли authoritative authorization для конкретного requester. Фильтр по метке в поисковом индексе может быть оптимизацией, но не заменяет проверку в системе, владеющей политикой доступа. Иначе ошибка индекса или устаревшая роль может раскрыть excerpt ещё до того, как сервис вернёт отказ.
\nРазделяйте безопасный ответ и диагностическую информацию. Пользователю можно сообщить, что подходящий источник недоступен или требует другого доступа. Внутреннему журналу можно записать recordId, policy version и код причины. Не кладите в журнал сам закрытый фрагмент без отдельного основания: диагностический канал тоже имеет владельца и срок хранения.
Ссылка на главную страницу не является точной цитатой. Минимальная citation должна позволять восстановить source revision, URI и anchor; для Markdown или HTML это может быть стабильный заголовок, номер раздела или диапазон строк в immutable-документе. Если источник перемещается, храните идентификатор редакции и проверяйте, что anchor по-прежнему разрешается.
\nПишите ответ только после отбора evidence. Удобный формат — таблица claims: для каждого утверждения указать ссылку, фрагмент, ограничение и статус human verification. Если один фрагмент подтверждает только часть предложения, разделите claim или ослабьте формулировку. Citation, добавленная в конце уже готового текста, не исправляет вывод, который возник без источника.
\nМаршрут можно считать проверенным на базовом уровне, если один и тот же набор входов даёт четыре предсказуемых исхода: свежая разрешённая запись с anchor проходит технический допуск и помечается как needs-human-verification; просроченная запись получает expired-at-retrieval-time; закрытая — access-label-not-granted; запись без anchor — missing-exact-citation. В последних трёх случаях excerpt не попадает в генерацию.
Повторный запуск с теми же query, retrievedAt, policy и записями должен дать тот же decision. После этого отдельно измеряйте recall, latency и качество ранжирования. Эти метрики улучшают очередь кандидатов, но не превращают score в доказательство и не заменяют проверку claim.
Описанный маршрут не доказывает полноту корпуса, качество embeddings, корректность модели, истинность утверждения или отсутствие всех уязвимостей. Он не говорит, что vector search лучше keyword search. Он задаёт более узкую гарантию: система не использует найденный фрагмент как evidence, пока не проверены его provenance, доступ, локальная свежесть и адресуемая цитата.
\nСинтетический код не является production-библиотекой: в нём нет транзакций, конкурентного обновления policy, ревокации ролей, подписи источника, кешей и сетевых ошибок. В реальном проекте нужно отдельно проверить escaping, изоляцию tenant, права на логи, версионирование policy и поведение при недоступности authoritative source. Если policy неизвестна, нельзя молча выбрать allow или deny как доказанный результат: нужно назначить владельца правила и вернуть stop condition.
\nk и отдельные этапы индексации и поиска. Она подтверждает механику retrieval, но не истинность claim и не права читателя.