Практический разбор решения на примере реального корпоративного FAQ-бота

Начальник смены на складе открывает корпоративного ИИ-ассистента и пишет в чате: «Что делать, если водитель не успел с доставкой?» Через несколько секунд появляется ответ — чёткий, правильный, со ссылкой на источник: регламент логистических операций. Его можно скачать.

Теперь представим, что тот же сотрудник задаёт другой вопрос: «Сколько мы платим перевозчикам за километр?» Ввести такой вопрос ему ничто не мешает. Важно лишь то, что произойдёт дальше. Если файл с тарифной сетке лежит в той же базе данных, что и регламент, ассистент ответит вежливо, точно и приложит ссылку на скачивание.

В этом-то и проблема. Семантический поиск мы решили и добились хороших ответов. Но следующая сложная задача — сделать так, чтобы ассистент отвечал только на основе того, что конкретному человеку разрешено знать.

Кратко: что такое RAG

Большие языковые модели много знают о мире и ничего — о вашей компании. Они никогда не читали ваш онбординг, шаблоны договоров или отчёты об инцидентах за прошлый квартал. Retrieval-Augmented Generation (RAG), то есть генерация с дополнением через семантический поиск, закрывает этот пробел. Термин появился в статье Патрика Льюиса и коллег из Facebook AI Research 2020 года, и идея в нём простая. Прежде чем модель ответит, система находит (через векторный поиск) самые релевантные фрагменты в ваших собственных документах и передаёт их модели в качестве контекста. Затем модель генерирует ответ, опираясь на эти фрагменты, и может указать, откуда взята информация.

Под капотом документы нарезаются на фрагменты (chunks, чанки). Каждый чанк превращается в эмбеддинг — набор чисел, вектор, отражающий его смысл, — и сохраняется в векторной базе данных. Когда приходит вопрос, его переводят в эмбеддинг тем же способом, а база возвращает чанки, чьи векторы ближе всего к нему. Поэтому запрос «водитель опоздал» находит абзац под заголовком «Обработка прибытий не вовремя», хотя общих слов у них нет.

Подход распространился быстро. Morgan Stanley выдал своим финансовым консультантам похожего ассистента, работающего поверх примерно 100 000 внутренних аналитических отчётов и документов, и, по словам компании, им пользуются более 98% команд консультантов. Тот же подход можно найти почти везде, где документов больше, чем люди способны запомнить:

  • Логистика и транспорт

  • Юридические фирмы

  • Онлайн-школы и продавцы курсов

  • Медицина, страхование, банки, HR-отделы, IT-поддержка: везде, где люди снова и снова задают одни и те же вопросы к массе PDF-файлов.

Но не всем положено видеть всё

У каждой из этих организаций есть иерархия доступа. Помощник юриста не должен листать дела клиентов другой команды. Сотруднику поддержки онлайн-школы нужны правила возврата, но не таблица партнёрских комиссий. В отделе безопасности сотрудник должен знать инструкции по своим объектам, и ему незачем читать отчёт об уязвимостях других объектов и тем более клиентов.

Причём границы не всегда совпадают с границами документов. Справочник сотрудника может быть открыт для всех, кроме одной главы — о дисциплинарных процедурах и зарплатных вилках, которую должны читать только руководители и HR. Один файл — две аудитории.

Без контроля доступа RAG-система всё это стирает. Она становится самым эффективным инструментом утечки данных из всех, что компания когда-либо внедряла: отвечает простым языком, но и прикладывает ссылки на источники.

Это не гипотеза. Внедрения Microsoft Copilot столкнулись именно с этой проблемой, которую назвали oversharing — избыточным доступом. Ассистент соблюдал существующие права, но из-за многолетней небрежной раздачи доступов в SharePoint данные о зарплатах, HR-файлы и стратегические документы стали находиться одним запросом. Вендоры в сфере безопасности формулируют прямо: ИИ не создал новых доступов, он сделал существующие плохие доступы тривиально обнаружимыми. OWASP теперь включает недостаточный контроль доступа к векторным хранилищам в свой Top 10 для LLM-приложений (LLM08: Vector and Embedding Weaknesses).

Хорошая новость: это всё ещё обычная сегрегация данных

RAG может казаться системой нового типа, но в основе у неё — запрос к базе данных. «Найди пять чанков, ближайших к этому вектору» — это SELECT с необычным ORDER BY. А значит, старые правила разграничения данных по-прежнему действуют.

Главное правило — фильтровать до того, как модель что-либо увидит. Системный промпт «не раскрывай конфиденциальную информацию» — это не контроль доступа. Языковую модель можно "уговорить" нарушить инструкции. Как только запрещённый абзац попал в контекстное окно, уже поздно. Безопасен только тот документ, который вообще не был извлечён.

С учётом этого правила есть четыре распространённых способа разграничить данные:

  1. Физическая изоляция: отдельная база данных или отдельное развёртывание на каждого клиента или зону безопасности. Самая прочная граница и самая дорогая. Подходит для регулируемых отраслей и разных клиентов.

  2. Логические разделы: одна база, но отдельная коллекция, индекс или пространство имён на каждую роль или группу. Каждый запрос идёт ровно в один раздел, остальные из этого запроса недостижимы.

  3. Фильтрация по метаданным в общем индексе: каждый чанк несёт метки вроде allowed_groups: ["hr", "managers"], и каждый поиск включает фильтр по группам пользователя. Гибко и детально. Слабое место — одного пропущенного фильтра в одной ветке кода достаточно, чтобы утекло всё. Хорошие разборы есть у Pinecone и Oso.

  4. Авторизация в момент запроса: сначала извлекаются кандидаты, затем каждый проверяется по правам в исходной системе (SharePoint, Google Drive, сервис авторизации вроде OpenFGA), и только потом передаётся дальше. Права всегда актуальны, но это медленнее и сложнее в реализации на больших объёмах.

Реальные системы часто сочетают несколько подходов. Наш FAQ-бот использует второй.

Наш подход: одна папка на роль

В нашем проекте — бэкенд на FastAPI с агентом на LangGraph и PostgreSQL/pgvector — правило такое: роль — это папка, а папка — это коллекция.

Пример:

documents/

├── faq/ → общая база знаний для всех ├── support/ → служба поддержки: регламенты, эскалация, правила возврата └── management/ → руководители: всё вышеперечисленное + зарплатные вилки, дисциплинарная глава.

Мэппинг в конфигурации связывает роли с этими коллекциями ({"administrator": "faq", "support": "support", ...}). Когда администратор вручную запускает индексацию (разбивки на чанки и векторизацию) или это происходит автоматически, система обходит каждую папку и записывает эмбеддинги её файлов в собственную коллекцию pgvector этой папки. Документ из support/ никогда не попадёт в faq/.

Это же решает проблему со справочником сотрудника. Заказчик держит две версии: полную — в management/, и версию без чувствительной главы — в общих папках. Редактирование происходит там, где люди его и так понимают, — в самих документах, а не в коде.

Почему папки хорошо работают

  • Понятные границы. Любой может открыть дерево каталогов и увидеть, кто что читает. Никому не нужно разбираться в комбинациях меток или файлах политик.

  • Привычный интерфейс для заказчика. Владельцы бизнеса и так умеют работать с файлами и папками. Решить, что видит роль, — значит перетащить файл в папку. Та же структура ложится на общий диск вроде Google Drive или SharePoint, так что заказчик может управлять доступом в инструменте, которым уже пользуется.

  • Надёжная изоляция по построению. Поисковик на каждый запрос создаётся ровно для одной коллекции, поэтому запрос не может дотянуться до данных другой роли, даже если модель об этом попросит.

  • Простой аудит. Вопрос «какие документы видит поддержка?» решается командой ls documents/support.

И где они не справляются

  • Дублирование. Документ, нужный трём ролям, лежит в трёх папках и индексируется трижды. Копии расходятся, когда кто-то обновляет только одну из них. Компромисс: больше данных в пользу простоты.

  • Одна роль на пользователя. Сотрудник, который одновременно и «поддержка», и «продажи», в схему вписывается плохо — нужна либо составная роль, либо общая папка.

  • Высоко-уровневое разбиение. Доступ на уровне главы означает ручную подготовку отдельных версий документа.

Для малой или средней компании с несколькими чётко определёнными ролями эти издержки невелики, а ясность дорогого стоит. В масштабах крупного предприятия с тысячами пересекающихся групп обычно выигрывают фильтрация по метаданным или авторизация в момент запроса.

Путь пользователя шаг за шагом

Вот что происходит, когда сотрудник задаёт вопрос:

  1. Вход. Пользователь авторизуется и получает подписанный JWT с его идентификатором и ролью. Заблокированные пользователи отсекаются на этом этапе и при каждом последующем запросе.

  2. Вопрос. Сообщение уходит на бэкенд с токеном в заголовке Authorization.

  3. Определение коллекции. Сервер находит пользователя в базе и сопоставляет его роль с названием коллекции. Роль берётся из собственных записей сервера. Ничто, отправленное клиентом, не может её изменить.

  4. Поиск. Результат работы инструмента агента search_documents возвращается только для этой коллекции. С точки зрения этого запроса документов других ролей не существует.

  5. Ответ. Модель пишет ответ на основе найденных чанков и передаёт его потоком через SSE вместе со ссылками на источники (имя файла и фрагмент текста), которые отображаются в боковой панели.

  6. Приватная история. Переписки хранятся отдельно для каждого пользователя, поэтому никто не откроет чужой чат, угадав его ID.

Проверка доступа происходит на шаге 3. К моменту, когда в дело вступает модель, данные уже сужены до того, что этому пользователю разрешено видеть.

Незаметная утечка: кнопка «Скачать»

Ссылки на источники — одно из лучших свойств RAG. Пользователи доверяют ответам, которые могут проверить, поэтому в нашей панели источников можно кликнуть по имени файла и скачать оригинал.

Но эта кнопка — ещё и лёгкий путь для утечки. Соблазнительно сделать её статической ссылкой вида /files/management/salary-bands.pdf, отдаваемой прямо с диска. Тогда любой пользователь, угадавший этот URL или получивший его от коллеги, скачает файл независимо от роли. Поиск будет надёжно закрыт, а скачивание — открыто всем.

Мы отдельно сфокусировались на эндпоинте скачивания:

  • Только с аутентификацией. GET /documents/{filename} требует тот же JWT, что и чат. Фронтенд запрашивает файл с токеном в заголовке, превращает ответ в blob и вызывает браузерное «Сохранить как». Токен никогда не попадает в URL, откуда он мог бы утечь в логи или историю браузера.

  • Только в пределах папки роли. Сервер заново определяет коллекцию пользователя (ничему из того, что прислал клиент, он не доверяет) и ищет файл только внутри documents/{collection}/.

  • Защита от хаков с путями. Имена файлов экранируются. Всё, что содержит .., разделители каталогов/файлов или абсолютные пути, отклоняется, а итоговый путь должен пройти строгую проверку relative_to(collection_root).

  • Никаких утечек фактов существования. Файл, который лежит в папке другой роли, возвращает 404 — ровно как файл, которого нет вовсе. Ответ 403 подтвердил бы атакующему, что salary-bands.pdf действительно существует.

  • Никаких особых случаев. Даже администратор скачивает только из своей коллекции. Каждое исключение из правила — это место, где что-то может сломаться.

  • Покрыто тестами. Автоматическе юнит-тесты проверяют стандартный сценарий, отказ при доступе к чужой роли, обход путей, запросы без аутентификации и отсутствующие файлы.

Принцип такой: каждый канал, по которому данные покидают систему, нуждается в собственной проверке — ответ, фрагмент в ссылке на источник, скачивание, история чата и любые логи.

Другие способы изоляции

Если папки не подходят вашей организации, рассмотрите такие варианты:

  • ACL в метаданных в той же таблице pgvector: каждый чанк помечается разрешёнными группами, и каждый запрос фильтруется по ним. Хорошо работает, когда документы разделяются между множеством пересекающихся групп.

  • Row-Level Security в PostgreSQL. Поскольку pgvector живёт внутри Postgres, сама база может обеспечивать правило «эта сессия видит только строки своих групп» — даже если код приложения забудет про фильтр.

  • Синхронизация ACL из исходной системы (Google Drive, SharePoint, Confluence) в метаданные чанков, чтобы ИИ-ассистент наследовал права, которые люди и так поддерживают в источниках данных. История с Copilot здесь служит предупреждением: плохие права наследуются тоже, поэтому сначала проверьте их.

  • Авторизация на основе отношений (системы в духе Zanzibar, например OpenFGA или SpiceDB) для сложных графов «кто что может видеть» с проверкой в момент запроса.

Вывод

RAG-ассистент заслуживает доверия ровно настолько, насколько защищён самый слабый путь к данным. Выберите модель разграничения, которую можно объяснить одним предложением. Фильтруйте данные ещё до того, как модель увидит хоть какой-то контекст. А затем повторите ту же проверку для каждого другого канала, по которому данные покидают систему, — особенно для кнопки «Скачать».

Наша версия в одном предложении: ваша роль — это ваша папка, и всё, что за её пределами, для вас не существует. Это не самый гибкий дизайн, зато офис-менеджер заказчика может его понять, проверить и поддерживать, просто перетаскивая файлы между папками.

Комментарии (3)


  1. ToxaBes
    23.09.2026 12:02

    это SELECT с необычным ORDER BY

    Эм, это SELECT с необычным LIKE

    Фильтрация по метаданным в общем индексе: каждый чанк несёт метки вроде allowed_groups: ["hr", "managers"], и каждый поиск включает фильтр по группам пользователя. Гибко и детально. Слабое место — одного пропущенного фильтра в одной ветке кода достаточно, чтобы утекло всё. Хорошие разборы есть у Pinecone и Oso.

    Самый грамотный способ на текущий день. Чтобы сделать его пуленепробиваемым нужно развернуть отношение, те у пользователяя не может быть больше одной роли, а у данных в RAG - может (роли кстати id задаются) . Тогда роль берется из аккаунта пользователя и нет описанных в методе проблем.

    Ваше решение:

    роль — это папка, а папка — это коллекция.

    Буквально повторяет это решение, просто зачем-то переносит отношение сущностей в папки. На этапе первоначального наполнения это удобнее, но дальше не отследить кто закинул в неправильную папку документ и перестроил индекс.


    1. VictoriaPuzhevich Автор
      23.09.2026 12:02

      Согласна с вами. Перенос всех данных в БД, как источник правды оправдан, но мы подходим чуть по-другому: источник правды для нас это всё-таки файлы (зачастую в формате .md). Таким образом, мы более гибки к изменению имплементации гибридного поиска (fulltext search + векторный через БД или grep по файлам + векторный через БД). А также остаёмся гибки к изменению harness или добавленю новых. Допустим, корпоративнный AI-агент встроен в основное приложение, а Hermes выполняет другие функции, основывась на тех же файлах. То есть мы делаем ставку на файловую систему как источник правды.


      1. ToxaBes
        23.09.2026 12:02

        но мы подходим чуть по-другому

        Имеете полное право, если скорость не критична или документов не так чтобы много, то вполне рабочее решение.