Files
progcode/editorial/agent-rewrites/102.json
T

8 lines
23 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"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) &lt;= Date.parse(retrievedAt)) {\n return { ok: false, reason: 'expired-at-retrieval-time' };\n }\n if (!item.accessLabels.some((label) =&gt; 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) =&gt; result.ok)\n .map((result) =&gt; result.item)\n .sort((a, b) =&gt; b.vectorScore - a.vectorScore);\n\nconst decision = allowed.length\n ? { status: 'needs-human-verification', evidence: allowed[0] }\n : { status: 'stop', reasons: checked.filter((x) =&gt; !x.ok).map((x) =&gt; 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>"
}