RAG-чат-бот по регламентам компании: наш подход — АйТи Фреш
Москва, Щёлковское шоссе, д. 92, корп. 7 · Пн–Пт 9:00–19:00 · +7 903 729-62-41
· 15 мин чтения

Корпоративный чат-бот по внутренним документам: как мы строим RAG для клиентов на 50 РМ

Корпоративный чат-бот по внутренним документам: как мы строим RAG для клиентов на 50 РМ

Каждый второй наш клиент на 50 рабочих местах держит регламенты, инструкции и прайсы в разрозненных папках на файловом шаринге или в 1С-Документообороте. Найти нужный пункт там быстрее через звонок коллеге, чем через поиск — это честная правда. Мы в ITfresh закрываем эту проблему корпоративным чат-ботом на базе RAG (Retrieval-Augmented Generation). Сотрудник задаёт вопрос обычными словами — получает точный ответ со ссылкой на конкретный документ и конкретный пункт, а не пачку файлов, которые ещё надо открыть и прочитать. Дальше — наша методология: как мы собираем такого бота от исходного корпуса документов до продакшена, с реальными компонентами, параметрами и порогами, которые применяем на проектах.

Почему поиск по шарингам не работает и в чём именно измеряется провал

Прежде чем предлагать RAG-бота, мы проводим короткий аудит. Просим 5–7 сотрудников из разных отделов найти ответ на конкретный вопрос по действующему регламенту — например, «сколько дней отпуска можно взять авансом» или «какая скидка для дилера 2-го уровня в майском прайсе». На юрлицо до 50 РМ поиск в среднем занимает от 4 до 12 минут. И это ещё не всё: часть сотрудников стабильно находит устаревшую версию документа. На шаринге одновременно лежат «Прайс_2025_final.xlsx» и «Прайс_2025_final_v2_испр.xlsx», а актуальный третий файл — в почте у бухгалтера. Классика.

Дело не в «плохом поиске Windows» — проблема системная. Обычный полнотекстовый поиск, будь то grep по файлам, встроенный поиск SharePoint или Яндекс.Диска, ищет по совпадению слов, а не по смыслу вопроса. Сотрудник спрашивает «можно ли уйти на удалёнку по семейным обстоятельствам», а в регламенте написано «дистанционная работа предоставляется по заявлению сотрудника при наличии уважительных причин» — буквально ни одного совпадающего слова, хотя ответ прямой. Именно эту задачу решает RAG: вопрос и документ сопоставляются не по строкам, а по векторам в пространстве смыслов — embeddings. Релевантный фрагмент передаётся большой языковой модели, которая формулирует ответ на естественном языке и указывает источник.

На аудите мы отдельно фиксируем объём и тип корпуса. Сколько документов — обычно от 80 до 400 файлов на компанию до 50 РМ. В каких форматах — DOCX, PDF, XLSX-прайсы, иногда сканы с печатью. Как часто меняются — регламенты раз в квартал, прайсы раз в неделю или чаще. Эти цифры напрямую определяют выбор компонентов: от векторной БД до частоты переиндексации. Без этих данных разговор про архитектуру вести бессмысленно.

Среднее время поиска по внутренним документам на юрлицо до 50 РМ
Среднее время поиска по внутренним документам составляет от 4 до 12 минут.

Архитектура контура: из каких слоёв собран наш RAG

Контур мы собираем из пяти слоёв, и каждый — заменяемый модуль, не монолит. Это принципиально важно. Если через год клиент решит перейти с Qdrant на pgvector или сменить модель эмбеддингов — переписывается один слой, а не вся система с нуля.

  1. Приём и нормализация документов — конвертация DOCX/PDF/XLSX в чистый текст с сохранением структуры (заголовки, таблицы, нумерация пунктов), извлечение метаданных: название документа, раздел компании, дата последнего изменения, гриф доступа.
  2. Чанкинг и обогащение контекстом — разбиение текста на смысловые фрагменты с добавлением ситуативного контекста к каждому фрагменту (подробно в следующем разделе).
  3. Индексация — построение векторных представлений (embeddings) через Voyage AI и одновременно лексического индекса (BM25 или полнотекстовый индекс Postgres) для гибридного поиска.
  4. Retrieval и reranking — по запросу пользователя система достаёт top-N кандидатов из векторного и лексического индекса, объединяет и переранжирует их моделью-реранкером, оставляя top-K самых релевантных фрагментов.
  5. Генерация ответа — отобранные фрагменты подставляются в системный промпт Claude вместе с вопросом пользователя, модель формулирует ответ строго на основе переданного контекста и указывает источник.

Оркестрацию слоёв 2-4 мы делаем на LangChain (модули langchain-text-splitters и langchain-postgres/langchain-qdrant) или на LlamaIndex — выбор зависит от того, что удобнее интегрировать с уже используемым у клиента стеком; концептуально пайплайн идентичен. Слой 5 всегда — прямые вызовы Claude API (Anthropic Messages API), без прослойки агентских фреймворков: это упрощает диагностику при разборе неверных ответов.

Послойная архитектура нужна ещё по одной причине: у каждого клиента свой «источник правды» для документов. У одних — папка на Яндекс.Диске, у других — 1С-Документооборот с версионированием, у третьих регламенты живут прямо в Confluence или Битрикс24. Слой приёма и нормализации мы пишем под конкретный источник отдельным коннектором — обычно 150–300 строк кода на Python поверх готовых библиотек для парсинга DOCX и PDF. Всё, что ниже по пайплайну, остаётся без изменений. На практике это сильно ускоряет повторные внедрения: второй и третий проект на похожем стеке занимают у нас заметно меньше времени, чем первый.

Чанкинг: почему мы режем документ на фрагменты по 800 токенов и добавляем к каждому «паспорт контекста»

Самая частая ошибка в самодельных RAG-решениях — резать документ по фиксированному числу символов без учёта структуры. Мы используем рекурсивный сплиттер (в LangChain — RecursiveCharacterTextSplitter), который сначала пытается резать по границам абзацев (\n\n), затем по строкам (\n), затем по словам, и только в крайнем случае — посимвольно. Это сохраняет пункт регламента или строку прайса целиком, а не обрывает её на середине фразы.

Целевой размер фрагмента — около 800 токенов с перекрытием (chunk overlap) около 100 токенов между соседними чанками. Это нужно, чтобы фраза на границе не потерялась ни в одном из фрагментов. Для плотных таблиц — прайс-листов, тарифных сеток — мы уменьшаем чанк до границ одной логической строки. Иначе смысл строки «Услуга / Тариф Стандарт / Тариф Премиум» размывается при разрезании между колонками, и бот выдаёт чушь.

from langchain_text_splitters import RecursiveCharacterTextSplitter splitter = RecursiveCharacterTextSplitter( chunk_size=800, chunk_overlap=100, separators=["\n\n", "\n", ". ", " ", ""], length_function=count_tokens, # считаем токены, не символы ) chunks = splitter.split_text(document_text)

Ключевой приём, который мы переняли из инженерной практики Anthropic по контекстному поиску (contextual retrieval) — перед эмбеддингом каждого фрагмента мы добавляем к нему короткую «справку», объясняющую, откуда этот кусок и о чём документ в целом: например, «Фрагмент из Положения об оплате труда, раздел 4 «Компенсации», редакция от 12.05.2026» перед самим текстом пункта. Эту справку генерирует дешёвая модель (у нас — Claude Haiku 4.5, id claude-haiku-4-5), которой на вход подаётся весь документ целиком и конкретный фрагмент, а на выходе — 1-2 предложения контекста. Затем контекст приклеивается к фрагменту и уже эта склейка идёт в эмбеддинг и в лексический индекс. Такой подход снимает главную болезнь наивного чанкинга — «семантическую изоляцию» фрагмента, когда кусок текста без контекста означает не то же самое, что в составе документа. По собственным замерам на пилотных проектах добавление контекста к фрагментам заметно снижает долю ситуаций, когда система не находит нужный пункт среди верхних кандидатов — это согласуется с методикой, которую публично описывал Anthropic применительно к контекстным эмбеддингам и контекстному BM25.

Эмбеддинги: почему Voyage AI, а не «универсальная модель из коробки»

У самого Anthropic нет собственного API эмбеддингов — компания прямо рекомендует для этой задачи модели Voyage AI, с которой у Anthropic партнёрство. Для корпоративных RAG на русском корпусе документов мы используем voyage-3.5 как модель по умолчанию (хорошее соотношение качества и цены на многоязычных текстах) и voyage-3-large, когда клиент готов заплатить за более высокое качество на сложных смешанных корпусах (регламенты, ГОСТы, технические инструкции с терминологией). Если в базе много IT-регламентов или инструкций с фрагментами кода и конфигураций, берём доменную модель voyage-code-3 — она обучена именно на технических текстах и командах.

Важный практический параметр — output_dimension: модели семейства voyage-3.5 и voyage-3-large поддерживают усечённую размерность вектора (256 / 512 / 1024 / 2048), при этом 1024 — значение по умолчанию. Для клиента до 50 РМ с корпусом в несколько сотен документов мы почти всегда берём 1024: это компромисс между точностью поиска и размером индекса — увеличение размерности до 2048 даёт прирост точности в единицы процентов, но вдвое увеличивает объём векторного индекса и расход памяти на HNSW-графе.

Модель VoyageКогда используемРазмерность (output_dimension)
voyage-3.5Базовый выбор: регламенты, инструкции, HR-документы1024 (по умолчанию)
voyage-3-largeСложный смешанный корпус, высокие требования к точности1024, реже 2048
voyage-3.5-liteБольшой корпус (тысячи документов), приоритет скорости и цены512
voyage-code-3IT-регламенты, SOP с командами и конфигами1024

Все запросы к Voyage идут пакетно — batch, а не по одному фрагменту. Это дешевле и заметно сокращает время полной переиндексации базы после массового обновления регламентов. Казалось бы, мелочь — но на практике разница ощутимая.

Векторное хранилище: как выбираем между pgvector, Qdrant и Chroma

Для клиента до 50 РМ выбор векторной БД почти никогда не про «какая быстрее в бенчмарках». Счёт идёт на сотни тысяч, максимум единицы миллионов векторов — с этим справится любое из трёх распространённых решений. Реальный вопрос другой: что уже есть в инфраструктуре клиента и кто будет это администрировать после сдачи проекта.

ХранилищеКогда берёмКлючевые параметры HNSW
pgvector (расширение PostgreSQL)У клиента уже есть PostgreSQL (например, под 1С-совместимый бэкенд или CRM) — не плодим лишний сервисm=16 (по умолчанию), ef_construction 128-200 для прод-индекса, ef_search настраивается на лету под сессию запроса
QdrantКорпус растёт быстро, нужна отдельная фильтрация по метаданным (отдел/гриф доступа) на уровне БД, скалярная квантизация для экономии RAMm и ef_construct в конфиге коллекции, quantization_config со scalar int8, обновление параметров на лету без пересоздания коллекции (начиная с версии 1.4)
ChromaБыстрый пилот/PoC, локальный PersistentClient без отдельного сервераconfiguration с hnsw.space=cosine, ef_construction, max_neighbors — задаются один раз при создании коллекции и не меняются потом

Для типового клиента с уже развёрнутым PostgreSQL мы ставим pgvector прямо в существующий инстанс — отдельная схема rag, отдельная роль с правами только на неё:

CREATE EXTENSION IF NOT EXISTS vector; CREATE TABLE rag.chunks ( id bigserial PRIMARY KEY, doc_id text NOT NULL, department text NOT NULL, access_level int NOT NULL DEFAULT 1, content text NOT NULL, embedding vector(1024) ); CREATE INDEX chunks_hnsw_idx ON rag.chunks USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 160); SET hnsw.ef_search = 80; -- на время сессии, компромисс recall/latency

Если клиент ожидает быстрый рост корпуса — например, планирует загрузить архив договоров за 5 лет — или требует изоляцию по отделам на уровне БД, мы ставим отдельный контейнер Qdrant со скалярной квантизацией int8. Это сокращает объём, который держится в RAM, примерно в 4 раза. Потеря точности есть, небольшая — компенсируем её на этапе reranking.

Retrieval pipeline: гибридный поиск и reranking, а не «просто похожие вектора»

Чисто векторный поиск по косинусному расстоянию регулярно промахивается на точных идентификаторах — номерах приказов, артикулах из прайса, аббревиатурах («ДС» — дополнительное соглашение, а не что-то ещё). Поэтому мы всегда строим гибридный поиск: параллельно с векторным top-N идёт лексический поиск (BM25 или полнотекстовый индекс tsvector в самом PostgreSQL) по тем же чанкам, а результаты объединяются.

Дальше — обязательный этап reranking. Мы берём с векторного и лексического поиска суммарно около 100-150 кандидатов-чанков и прогоняем их через модель-реранкер (Cohere rerank-v3.5, контекстное окно 4096 токенов, поддержка более 100 языков включая русский), которая уже честно «читает» пару вопрос-фрагмент целиком, а не сравнивает векторы, и переупорядочивает кандидатов по реальной релевантности. Из переранжированного списка в промпт модели уходит top-10-20 фрагментов — этого достаточно даже для составных вопросов вроде «какая скидка для нового дилера и какие документы нужны для перехода на следующий уровень».

Публичные замеры связки «контекстные эмбеддинги + контекстный BM25 + reranking», которые Anthropic описывал по своей методике контекстного поиска, показывают снижение доли промахов в top-20 выдачи в разы — по сравнению с наивным векторным поиском без контекста и без реранкера. Наш опыт на пилотах у клиентов эту картину подтверждает. Основная масса случаев «бот не нашёл ответ, хотя он есть в базе» уходит именно после добавления reranking — не после смены модели эмбеддингов, как многие ожидают.

Мы отдельно считаем latency-бюджет. Гибридный retrieval укладывается в 150–300 мс, reranking 100 кандидатов — ещё 200–400 мс. Итоговая задержка до передачи контекста в Claude — около полусекунды. На фоне времени генерации самого ответа пользователь эту паузу просто не замечает.

Отдельно проговариваем с клиентом ограничение самого reranker: контекстное окно у rerank-v3.5 — 4096 токенов на пару запрос-документ, поэтому передавать в реранкер целые документы вместо чанков нельзя технически, и это ещё один аргумент в пользу дисциплинированного чанкинга на этапе индексации, а не «на глазок». Мы также логируем сырые оценки релевантности (score) от реранкера по каждому запросу — если для нового типа вопросов средний score у top-1 кандидата систематически низкий, это сигнал, что в базе просто нет документа с ответом, и вопрос нужно эскалировать не на более дорогую модель, а на владельца регламента, чтобы он написал недостающий раздел.

Генерация ответа: маршрутизация между моделями Claude и prompt caching для системного промпта

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

Модель ClaudeЗадача в контуреТиповой сценарий
Claude Haiku 4.5 (claude-haiku-4-5)Классификация запроса, переформулировка вопроса, генерация контекстной справки к чанкам при индексации«Уточни, к какому отделу относится вопрос»
Claude Sonnet 4.6 (claude-sonnet-4-6)Основная генерация ответов по найденным фрагментам90%+ вопросов сотрудников — прямые факты из одного-двух документов
Claude Opus 4.6 (claude-opus-4-6)Эскалация на сложные, составные или потенциально противоречивые вопросы«В регламенте А написано одно, а в приказе Б — другое, что применимо к моему случаю»

Решение об эскалации с Sonnet на Opus принимает сам Sonnet: если после первой попытки ответа модель не может уверенно сослаться на единственный непротиворечивый фрагмент (это видно по структурированному полю confidence в ответе, которое мы требуем в системном промпте), запрос повторяется с Opus и расширенным набором top-K фрагментов.

Системный промпт — это не только инструкция «отвечай только на основе переданных фрагментов, не выдумывай», но и достаточно объёмный блок с общими правилами компании (структура отделов, принятые сокращения, формат ссылок на источник). Этот блок одинаков для всех запросов в рамках сессии, поэтому мы кэшируем его через cache_control с типом ephemeral: при 5-минутном окне кэша повторное чтение блока стоит 10% от цены обычного входного токена, а первая запись в кэш — 1,25x от обычной цены. Для чат-бота с постоянным потоком вопросов в рабочие часы это ощутимо снижает счёт по API, потому что неизменная часть промпта (правила компании, инструкции по цитированию) может занимать больше токенов, чем сам вопрос и найденные фрагменты.

{ "role": "system", "content": [ { "type": "text", "text": "Ты — ассистент компании ... Отвечай только на основе фрагментов ниже.", "cache_control": {"type": "ephemeral"} } ] }

Каждый ответ бота обязан заканчиваться явной ссылкой на источник: название документа, раздел, дата редакции — те метаданные, которые мы прикрепили к чанку ещё на этапе индексации. Это не косметика. Юрист или бухгалтер клиента должен иметь возможность быстро перепроверить ответ по первоисточнику, а не слепо доверять генерации. Без этого любой RAG-бот — просто красивая игрушка.

Права доступа и контур данных: кто и что может спросить у бота

В компании до 50 РМ почти всегда есть документы с разным уровнем доступа. Общий регламент отпусков видят все, а положение об оплате труда руководителей — только бухгалтерия и топ-менеджмент. Наивная реализация RAG, где все документы лежат в одном общем индексе без разграничения, — прямой путь к утечке. Бот с одинаковой готовностью процитирует зарплату директора рядовому менеджеру, если формально нашёл релевантный фрагмент. Мы с таким сценарием сталкивались на аудитах у новых клиентов — и это всегда неприятный разговор.

Поэтому каждый чанк при индексации получает метаданные department и access_level, а сам retrieval выполняется с обязательным фильтром по этим полям на уровне запроса к БД — не постфактум фильтрацией уже полученных результатов на стороне приложения, а именно в самом SQL/API-запросе к векторному хранилищу. В pgvector это условие WHERE access_level <= :user_level в одном запросе вместе с оператором ANN-поиска; в Qdrant — payload-фильтр, который движок применяет до построения списка кандидатов ANN, а не после.

Уровень доступа пользователя бот берёт не с его слов — self-report в чате доверять нельзя. Источник — корпоративная директория: Active Directory или 1С, через которые у клиента уже работает аутентификация во всех остальных сервисах. Сессия чат-бота привязывается к учётной записи через SSO. Никаких анонимных токенов.

Отдельный вопрос — где физически находится контур. Если регламенты клиента содержат персональные данные или коммерческую тайну, векторную БД и приложение-оркестратор мы разворачиваем либо на собственной инфраструктуре клиента, либо в изолированном контуре у нас. Наружу — к Anthropic и Voyage AI — уходят только текстовые фрагменты для эмбеддинга и генерации. Сами документы периметр не покидают. Во внешние API идут фрагменты, и мы не храним их дольше времени обработки одного запроса.

Как мы внедряем: этапы, сроки и метрики, по которым сдаём проект

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

  1. Неделя 1 — аудит и сбор корпуса. Собираем актуальные версии документов, снимаем дубли и устаревшие копии, размечаем гриф доступа и принадлежность к отделу вместе с ответственным у клиента (обычно HR или руководитель офиса).
  2. Неделя 2 — индексация и настройка pipeline. Чанкинг с контекстным обогащением, эмбеддинги Voyage, построение HNSW-индекса, настройка гибридного retrieval и reranking с параметрами по умолчанию (top-150 → rerank → top-15).
  3. Неделя 3 — пилот на 10-15 сотрудниках. Собираем набор из 40-60 реальных вопросов от разных отделов, прогоняем через бота, вручную оцениваем каждый ответ по шкале «точный / частично точный / неверный / не нашёл» и считаем retrieval failure rate — долю вопросов, где нужный фрагмент вообще не попал в top-K после reranking.
  4. Неделя 4 — донастройка и запуск. По результатам пилота чаще всего донастраиваем не модель, а корпус: находим документы без чёткой структуры заголовков, вручную размечаем таблицы, которые плохо распознались при конвертации PDF. Затем — запуск на всю компанию через привычный канал (Telegram-бот, виджет в интранете или в существующем корпоративном мессенджере).

После запуска ведём два эксплуатационных процесса. Первый — переиндексация: при изменении регламента ответственный сотрудник кладёт новую версию в отслеживаемую папку, вебхук или cron-задача (в нашей практике — раз в ночь плюс ручной триггер «переиндексировать сейчас» для срочных правок вроде обновления прайса) пересчитывает эмбеддинги только для изменившихся документов, а не всей базы — это сокращает время обновления с часов до минут. Второй — журнал вопросов без уверенного ответа: если модель вернула низкий confidence, вопрос и её попытка ответа падают в отдельную очередь на еженедельный разбор, что одновременно служит и метрикой качества бота, и списком пробелов в самих регламентах компании — нередко бот вскрывает вопросы, на которые в документах компании попросту нет чёткого ответа.

Частые вопросы

Нужно ли переносить все документы компании в векторную базу сразу?
Нет, и мы этого не советуем. Начинаем с 2-3 категорий с самой высокой частотой вопросов — обычно это HR-регламенты (отпуска, командировки, компенсации) и актуальный прайс-лист. Остальные категории добавляем после пилота, когда видно, какие вопросы реально задают сотрудники, а какие документы никто никогда не открывает и индексировать их бессмысленно.
Что если бот не найдёт ответ или найдёт неточный фрагмент?
Системный промпт прямо запрещает модели отвечать, если переданные фрагменты не содержат уверенного ответа на вопрос — в этом случае бот сообщает, что не нашёл информацию, и предлагает переформулировать вопрос или обратиться к конкретному человеку/отделу. Такие случаи попадают в отдельный журнал для еженедельного разбора, а не остаются незамеченными.
Может ли бот случайно показать сотруднику документ с закрытым доступом?
Фильтр по department и access_level применяется на уровне самого запроса к векторной базе, до этапа генерации ответа — модель Claude физически не получает на вход фрагменты, к которым у пользователя нет доступа, поэтому процитировать их она не может даже теоретически.
Сколько стоит содержать такого бота в месяц для компании на 50 РМ?
Основные статьи расходов — вызовы Claude API за генерацию ответов (маршрутизация большей части вопросов на более дешёвую модель Sonnet вместо Opus и prompt caching системного промпта снижают эту статью в разы), эмбеддинги Voyage AI (разовая индексация плюс небольшие довычисления при обновлении документов) и хостинг векторной БД, который при выборе pgvector в уже существующем PostgreSQL клиента часто оказывается нулевым — сервис просто становится ещё одной схемой в имеющейся базе.
Чем это отличается от обычного поиска по ключевым словам в 1С или на шаринге?
Ключевое отличие — поиск по смыслу вопроса, а не по совпадению слов: сотрудник получает готовый сформулированный ответ со ссылкой на источник, а не список файлов, которые нужно открыть и прочитать самостоятельно. Плюс гибридный retrieval и reranking страхуют от того, что чисто смысловой поиск иногда промахивается на точных идентификаторах и номерах документов.

Столкнулись с похожей задачей? Обращайтесь — решим

Если у вас происходит что-то из описанного в этой статье — или любая другая проблема с ИТ-инфраструктурой, — обращайтесь в любое время. Мои специалисты и я лично разберём ситуацию, найдём настоящую причину и доведём до решения.

Возьмёмся и за разовую задачу, и за постоянное обслуживание. Первичная консультация — бесплатно и без обязательств.

📞 +7 903 729-62-41 ✈ Telegram @ITfresh_Boss

С уважением, Семёнов Евгений Сергеевич, директор «АйТи Фреш» — IT-аутсорсинг для компаний до 50 рабочих мест, 15+ лет практики

📄
Скачайте подробный разбор в PDF Кейсы, статистика, типовые ошибки и чек-лист самопроверки — 12 страниц
Скачать PDF

Подпишитесь на разборы ITfresh

Раз в неделю — практичные материалы по ИТ для бизнеса. Никакого спама, только польза.

Письмо придёт в течение минутыНе нашли его во «Входящих» — загляните в папку «Спам» или «Промоакции» и нажмите «Не спам». Так все следующие выпуски будут приходить прямо в основную почту.