Доступ к документам до поиска
Публичный помощник уже не видит служебную заметку, но редактору иногда нужен ответ по более широкому набору документов. Добавим доверенную роль на стороне локального приложения и проследим, где она влияет на данные. Новый результат — два разных разрешённых корпуса: reader работает только с public, editor может включать staff. Это учебное разграничение набора, а не готовая система входа. Строка роли сама по себе не подтверждает личность человека, который её передал.
В состоянии lesson-11 функция ask(question, role) впервые передаёт роль в read_rows. Командная строка может получить вторым аргументом editor. Такой вызов разрешён доверенному оператору, имеющему доступ к файлам корпуса и базе. Публичная веб-страница появится позже и всегда будет использовать reader. Не добавляйте к ней переключатель роли, пока нет настоящей авторизации: пользовательский параметр не должен становиться источником полномочий.
Кто задаёт роль
В локальной лаборатории границу доверия определяет доступ к компьютеру и запуск программы. Оператор уже может открыть Markdown служебной заметки напрямую, поэтому разрешение editor в CLI не предоставляет ему новые скрытые полномочия. Оно лишь показывает, как приложение выбирает допустимые источники. В реальном сервисе роль должна приходить из подтверждённой сессии, списка прав или проверенного контекста организации. Такой контекст не должен строиться из вопроса посетителя, произвольного HTTP-заголовка или аргумента, который сам посетитель может менять.
Функция read_rows принимает только reader и editor. Неизвестное значение вызывает исключение. Мы не используем правило «любая строка кроме reader означает расширенный доступ», потому что опечатка стала бы повышением прав. Подобная ошибка часто появляется в коротких примерах с условием role != "guest". Явный допустимый набор делает решение наблюдаемым и удерживает отказ по умолчанию. Если понадобятся новые роли, их нужно добавить вместе с точным перечнем источников, а не оставлять неявную широкую ветку.
Идентификаторы public и staff пока задаются на уровне документа. Все его фрагменты наследуют видимость. Мы не пытаемся закрывать один абзац внутри публичной статьи. Такой подход проще для редактора и согласован с обновлением целого источника. Если документ одновременно содержит открытый текст и внутренние комментарии, лучше разделить исходники либо реализовать более точную схему импорта. Простое удаление слова «секретно» из ответа не предотвращает передачу оставшегося содержания в модель и не создаёт настоящую границу доступа.
Место фильтрации
Порядок остаётся прежним: получить разрешённые фрагменты, посчитать BM25, упаковать контекст, решить достаточность, сформировать цитаты. Важно, что роль влияет на набор до всех этих операций. Тогда служебная заметка не участвует в публичном ранжировании и не вытесняет открытый источник. Это также уменьшает риск случайного утекания через диагностику. Если подробный журнал top_k содержал закрытый текст ещё до публичного фильтра, интерфейс мог бы быть аккуратным, а журнал уже нарушал бы границу корпуса.
В памяти доверенного процесса остаётся общая база. Файл SQLite не разделён на разные базы ролей, и read_rows читает payload перед фильтрацией. Значит, пользователь ОС, способный открыть knowledge.sqlite3, увидит обе группы. Урок не обещает защиту от такого оператора. Для реального сервиса нужны права файлов, ограничение резервных копий, доверенный серверный процесс и безопасный путь выдачи. Политика retrieval дополняет эти меры, но не заменяет хранение и управление доступом на уровне окружающей системы.
Рассмотрим вопрос учебную дату ноября. В открытом корпусе нет этих термов, а служебная заметка содержит вымышленную дату. Для reader ожидается insufficient. Для editor возможен supported с цитатой из editor-notes. Именно вымышленный характер примера позволяет рассмотреть утечку без настоящих внутренних данных. Не заменяйте его реальным расписанием только ради эффектного показа: лабораторный корпус должен быть безопасен для копирования и понятен читателю, который впервые открывает архив.
Ответ и последующее использование
Получение разрешённого ответа редактором ещё не разрешает публиковать его текст всем. Если сохранить editor-ответ в общий кэш и затем отдать его reader по тому же вопросу, фильтрация поиска уже не поможет. Поэтому роль обязана входить в ключ кэша. То же относится к организации, набору разрешённых документов и версии политики. Мы добавим кэш в пятнадцатом уроке. Уже сейчас полезно видеть, что полномочия сопровождают не только первоначальный запрос, но и все производные результаты, которые система собирается повторно использовать.
Список sources также может быть чувствительным. Иногда само существование внутреннего документа раскрывает планы организации. Наш публичный отказ поэтому не сообщает «найден editor-notes, доступ запрещён». Он описывает отсутствие достаточного основания в выбранных документах. Для оператора можно сохранять отдельную диагностику в доверенном канале. Не следует отправлять такой подробный reason в обычный JSON ответа каждому посетителю. Сокрытие тела документа и сокрытие сведений о его существовании являются разными проектными требованиями.
В будущем права могут изменяться во время существования сессии. Редактор потерял роль, документ стал закрытым, организация удалила участника. Постоянный кэш без ревизии доступа продолжит выдавать старые сведения. Наша лаборатория обновляет visibility через синхронизацию и связывает будущий кэш с текущими разрешёнными фрагментами. Но она не реализует внешний identity provider и мгновенный отзыв роли. Для этого нужен самостоятельный курс единого входа и точный срок действия полномочий в вашем приложении.
Сравнение двух запросов
Полный app.py ниже показывает единственное важное изменение — передачу доверенного role в read_rows. Для самостоятельного разбора выполните один вопрос с reader и editor: python3 app.py "учебную дату ноября" reader, затем такой же с editor. Ожидаемый состав sources различается. Эти команды не выполнялись при подготовке. После этого задайте неизвестную роль: программа должна завершиться ошибкой, а не вернуть расширенный набор. Сопоставление трёх случаев делает решение понятнее, чем общий лозунг «AI учитывает права».
Не пытайтесь скрыть служебный текст инструкцией языковой модели. Она появится в следующем уроке как необязательный локальный адаптер, но не получит права решать, кто editor. Код сначала создаст разрешённый контекст и только затем передаст выбранные части. Такой порядок позволяет менять модель без переноса политики доступа в prompt. Он также сохраняет полезную извлекающую версию, если модель недоступна: права и точные цитаты остаются свойствами приложения, а не побочным результатом удачной генерации.
Полный файл app.py из состояния lesson-11:
import json
import sys
from pathlib import Path
from loader import load_documents
from chunks import flatten
from ranking import rank
from storage import sync, read_rows
from context import pack
from answer import extract
def ask(question, role="reader"):
if len(question) > 500:
raise ValueError("Question is too long")
rows = read_rows(role)
candidates = rank(question, rows)
candidates = pack(candidates)
def build():
baseline = extract(question, candidates, strict=True)
return baseline
return build()
def main():
changes = sync(load_documents(), incremental=True)
question = sys.argv[1] if len(sys.argv) > 1 else "сохранить старых URL"
role = sys.argv[2] if len(sys.argv) > 2 else "reader"
print(json.dumps(ask(question, role), ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
Оглавление серии · Исходники всех шагов · Учебные маршруты
Пример остаётся локальным черновиком. При подготовке он не выполнялся; все описанные эффекты являются ожидаемыми, а не результатами запущенных проверок.