Самый неприятный результат поиска по инженерной базе — не пустой список. Хуже, когда retriever возвращает точное, но закрытое исключение, или старую инструкцию с отличным semantic match. Если скрыть это за уверенным ответом, ошибку трудно заметить: человек не видит, что цитата отсутствует, а change уже попал в review. Цена ошибки выше, чем задержка: неверная миграция, раскрытие ограниченного материала или спор о том, откуда взялось правило.
В такой ситуации система должна уметь сказать «стоп» до генерации текста. Это не отказ от помощи и не пустая формальность. Stop path превращает неизвестность в конкретный запрос: нужен источник с другим access scope, свежая редакция, точный anchor или owner, который подтвердит policy. Взрослый процесс не заставляет retriever угадывать, какой компромисс допустим. Он показывает, что найдено, что отброшено, почему отброшено и кто может снять ограничение.
Stop condition — часть контракта ответа
У ответа есть положительные и отрицательные условия. Положительное: есть candidate, который разрешён, свеж и содержит exact citation. Отрицательное: хотя бы одно обязательное свойство отсутствует у всех candidates. Тогда system не пишет «вероятно», не делает summary из закрытого snippet и не подменяет stale rule свежей интерпретацией. Она возвращает code stop-no-fresh-authorized-citable-source, query и reject reasons. Это уже достаточный артефакт для следующего человека.
Важная деталь: stop не означает, что corpus пуст. В fixed stale case corpus содержит record с score 0.96; в closed case — record с 0.98. Оба фрагмента подходят по словам, но не проходят другой contract. Если UI показывает только «0 results», он теряет диагностическую ценность. Если UI показывает закрытый excerpt, он нарушает rights. Нужен третий вариант: показать безопасный reason без текста, который нельзя раскрывать, и route для human review.
| Состояние candidate | Что можно показать requester | Ответ на вопрос | Следующий владелец |
|---|---|---|---|
| fresh + allowed + exact citation | version, timestamp, URI#anchor, permitted excerpt | draft только после human verification | reviewer и owner source |
| expired | recordId, sourceVersion, expiry и reason без ложной актуальности | stop | owner documentation или index freshness policy |
| closed | recordId и access reason без restricted excerpt | stop | owner access policy |
| no anchor | recordId и missing-anchor reason | stop | owner source structure или ingestion pipeline |
| unknown policy | не притворяться allow или deny | stop / clarify | назначенный owner policy |
Честная остановка дешевле скрытой догадки
Польза ответа не равна числу слов. Closed source нельзя пересказывать, expired document нельзя назвать текущим. Stop path удерживает спор у границы evidence: query, record id, version, reject reason и requested action. Вместо «модель сомневается» команда получает проверяемое «expired at retrieval time» или «access label not granted» и направляет его владельцу.
Минимальный воспроизводимый пример
import {
createFixedSyntheticKnowledgeRetrievalInput,
inspectFixedSyntheticKnowledgeRetrieval,
prepareSyntheticKnowledgeHumanReview,
} from './upgrade-2025-03.mjs';
const stale = inspectFixedSyntheticKnowledgeRetrieval(
createFixedSyntheticKnowledgeRetrievalInput('fixed-stale-only-v1'),
);
const review = prepareSyntheticKnowledgeHumanReview(stale);
console.log({
answerAllowed: stale.accepted,
stop: stale.decision.code,
reason: stale.retrieval.candidateOutput[0].reasons[0],
reviewStatus: review.status,
});
// Expected: false, stop-no-fresh-authorized-citable-source,
// expired-at-fixed-retrieval-time, stopped-before-answer.
// The model is fixed synthetic in memory. It performs no external read or side effect.
Этот example не спрашивает настоящую wiki и не пытается определить, имеет ли реальный человек доступ. Он использует record, timestamp и expiry, жёстко записанные в JS module. Поэтому его можно запускать как fixture: вход имеет строгую shape, report совпадает только с canonical fixed case, extra telemetry field отвергается, forged date не проходит сравнение, cyclic input и report возвращают reject вместо исключения. Такая строгость не делает пример production system, но не позволяет тесту незаметно превратиться в генератор произвольных ответов.
Accepted branch не менее важна. Fresh allowed case формирует citation и status requires-human-verification. Он всё ещё не выдаёт готовый fact. Отдельная функция создаёт только in-memory review draft с шагами: открыть source outside model, сравнить claim с exact anchor, подтвердить access и freshness policy с owner, сохранить answer вместе с citation. Если draft надо остановить, stopSyntheticKnowledgeAnswer удаляет только этот in-memory artefact и явно не пытается менять production, source или document.
Сравнить варианты до того, как спор уйдёт в prompt
| Вариант | Краткосрочная выгода | Цена ошибки | Решение автора |
|---|---|---|---|
| Сгенерировать ответ по top-1 | быстро выглядит полезным | stale или closed rule превращается в implementation decision | не применять: score не является evidence |
| Скрыть candidate и ответить «ничего нет» | не раскрывает fragment | теряется причина и owner не понимает, что исправлять | показать safe reject reason без закрытого excerpt |
| Остановить answer с structured reason | нужен следующий человек или источник | есть задержка и явный ownership | применять: это сохраняет access и freshness boundary |
Human verification проверяет claim, а не доверяет pipeline
После положительного filter остаётся последний переход, который нельзя выкинуть: человек открывает exact source и сравнивает его с proposed claim. Нужны четыре коротких вопроса. Тот ли это version? Есть ли в anchor именно это условие, а не похожая рекомендация? Не потерялось ли ограничение из соседнего абзаца? Разрешает ли policy использовать source в этом контексте? Если любой ответ отрицательный или неизвестный, candidate возвращается в stop path.
Retrieval уже сократил очередь до нескольких candidates, но final verification нельзя заменить confidence: связный текст может опираться на неверный fragment. В fixture human verification остаётся status, а не true; функция не открывает source и не принимает policy. Поэтому automated report не выдан за утверждение о реальном документе.
| Поле | Что записать | Что считается stop |
|---|---|---|
| Claim | одно проверяемое предложение, не summary всей темы | claim шире, чем exact fragment |
| Citation | URI, anchor, sourceVersion, retrievedAt | любое поле отсутствует или ведёт не к той редакции |
| Freshness | publishedAt, expiry и policy reason | record expired или period не определён |
| Access | разрешённый source class без раскрытия секретной policy | label не допускает reader или purpose |
| Decision | accept с limitation либо stop с reason | нет named owner для остаточного риска |
Откуда берётся дата и почему её нельзя переоценивать
RFC 9110 даёт полезную, но ограниченную модель Last-Modified: origin server сообщает время, в которое он считает selected representation изменённым. Это подходящий input для indexer, если source отдаёт header. Но из него не следует, что документ не утратил применимость, что все составные части страницы синхронизированы или что previous cached copy идентична current source. Поэтому рабочая карточка содержит несколько дат, а policy прямо называет, какое правило использует answer.
publishedAt отвечает на historical boundary, indexedAt — на lag pipeline, expiresAt — на local rule, retrievedAt — на срез answer. Эти поля отделяют ошибку document owner от ошибки indexer и не позволяют лечить retriever там, где нужно обновить source.
Access check не должен раскрывать закрытый source
NIST SP 800-207 отдельно выделяет authentication и authorization до доступа к enterprise resource. В поисковой системе из этого не следует, что labels из example достаточны для enterprise policy. Следствие уже практическое: decision deny должен произойти до того, как excerpt станет частью answer context. Иначе retrieval формально откажет после того, как закрытый текст уже обработан downstream component или увиден пользователем. В real integration scope и порядок enforcement определяет security owner, а не автор статьи.
В fixed closed case restricted excerpt не выводится. Report содержит record id, version, timestamp, required labels и access-label-not-granted. Это не реальная authorization decision, но интерфейс уже сохраняет нужную границу: причина доступна для диагностики, а текст остаётся закрытым. Уровень раскрытия определяет policy, не layer генерации.
Практический маршрут для одной базы
- Выберите один question type. Разделите operational и historical вопросы; у них могут быть разные freshness rules.
- Опишите source record. Добавьте stable id, version, owner, даты, access class, URI и exact anchor до настройки ranker.
- Сделайте reject reasons явными. Минимум: denied, expired, not-yet-published, missing-anchor и unknown-policy.
- Проведите один denial test. Высокий score закрытого или expired record не должен попасть в citation output.
- Назначьте human review. Reviewer открывает разрешённый source и подтверждает claim, а не доверяет составленному answer.
- Зафиксируйте stop owner. Вопрос без acceptable citation идёт к documentation, access или index owner с короткой карточкой причины.
Ограничения и следующий проверяемый шаг
Полевая статья не описывает реальный incident, не предоставляет реальные правила доступа и не измеряет время ответа. Все score, names, URI, dates, excerpts, allow/deny decisions и outputs принадлежат fixed synthetic in-memory model. Она не читает файлы, Git, сеть, CI, production, часы или telemetry; не извлекает документы и не определяет identity. Поэтому её fixture проверяет только shape и negative branches кода. Она не может подтвердить, что конкретная организация соблюдает access policy или что конкретная база свежа.
Следующий шаг: возьмите один вопрос, на который ваша система сегодня отвечает без citation. Добавьте structured result с accepted, reason, retrievedAt и source card. Затем составьте один test, где highest-score candidate expired или denied. Ожидаемый результат — stop before answer и короткий маршрут к owner. Если этот test нельзя написать из-за отсутствующих metadata, это уже полезный результат: сначала исправьте corpus contract, затем улучшайте модель поиска.
Историческая граница марта 2025
Для этой статьи OpenSearch используется только как подтверждение mechanics similarity search в immutable commit от 12 июня 2024. RFC 9110 используется только для смысла Last-Modified, а NIST SP 800-207 — только для принципа явной authorization boundary. Все три источника существовали до марта 2025 и были закреплены versioned release, RFC либо dated final publication. Stop policy, fields fixed model и human-review sequence — собственная инженерная конструкция автора; она не выдана за обязательную норму этих документов.
Проверяемые источники
- OpenSearch k-NN README, tag 2.15.0.0, immutable commit 150c589 (12 June 2024) — Официальный README описывает k-NN как nearest-neighbor similarity search по документам и измерениям, а также возможность уточнять similarity search фильтрами. Граница: Источник описывает поиск похожих кандидатов и фильтры. Он не говорит, что score подтверждает истинность утверждения, актуальность документа, право пользователя читать фрагмент или полноту корпуса.
- RFC 9110: HTTP Semantics, section 8.8.2 Last-Modified (June 2022) — Last-Modified сообщает время, в которое origin server считает выбранное представление изменённым; способ определения значения остаётся implementation detail. Граница: Это метаданные представления HTTP, а не доказательство семантической правильности текста, владельца документа, прав читателя или отсутствия более нового источника.
- NIST SP 800-207: Zero Trust Architecture (August 2020) — NIST описывает отсутствие неявного доверия по сетевому положению и отдельные authentication/authorization functions до установления сессии к enterprise resource. Граница: Публикация не задаёт схему vector index, формат цитаты или policy свежести инженерной документации. Она поддерживает только принцип явного контроля доступа к ресурсу.