· 15 мин чтения

Корпоративный чат-бот по внутренним документам: как мы строим 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-прайсы, иногда сканы с печатью), как часто они меняются (регламенты — раз в квартал, прайсы — раз в неделю, а то и чаще). Эти цифры напрямую определяют выбор компонентов дальше по тексту — от векторной БД до частоты переиндексации.

Архитектура контура: из каких слоёв собран наш 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"} } ] }

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

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

В компании до 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 страхуют от того, что чисто смысловой поиск иногда промахивается на точных идентификаторах и номерах документов.
📄
Скачайте подробный разбор в PDF Кейсы, статистика, типовые ошибки и чек-лист самопроверки — 12 страниц
Скачать PDF

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

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