Зачем это вообще нужно
В любой компании старше пары лет знания размазаны по вики, папкам с регламентами, почтовым перепискам, 1С и Битрикс24. Новичок ищет нужный документ полчаса или идёт спрашивать коллегу, и коллега отвлекается.
Очевидный ответ — «давайте сделаем ChatGPT по нашим документам». Но почти всегда тут же звучит требование: данные не должны уходить в чужое облако. Значит, модель, поиск и хранилище должны жить на своём железе.
В статье описываю стек, который собирал на практике для таких задач: RAG поверх XWiki, файлов и почты, доступ к 1С и Битрикс24 через инструменты агента, локальная модель на vLLM. Без маркетинга: что из чего состоит, как связано и где ломается.
Архитектура целиком
Слева документы: они заранее проходят конвейер загрузки и ложатся в индекс. Справа 1С и Битрикс24: агент запрашивает их через MCP в момент вопроса. Все обращения к моделям идут через один шлюз, облачная модель подключается только если это допускает политика компании.
Загрузка данных: самая долгая часть
Качество ответов гораздо сильнее определяется тем, как данные попали в индекс, а не выбором модели. Источники три, и у каждого свои проблемы.
XWiki
Первая версия забирала данные напрямую: SSH на сервер, выгрузка из MySQL, чтение вложений с диска. Работает, но хрупко: любое обновление XWiki может поменять схему БД, а права доступа вики при таком чтении игнорируются.
Правильный путь — REST API XWiki: список пространств и страниц, содержимое страницы, список и скачивание вложений. Инкрементальное обновление строится по дате изменения страницы: храним её в MongoDB и переобрабатываем только изменившееся.
Файлы и вложения
doc, docx, pdf, сканы — всё приводим к тексту с сохранением структуры (заголовки, таблицы). Для сканов и PDF-картинок использую PaddleOCR. Старый .doc лучше сразу конвертировать в docx через LibreOffice в headless-режиме, чем искать парсер под него.
Почта
Самый коварный источник. В цепочках каждое ответное письмо цитирует предыдущие, и в индексе один и тот же текст оказывается по десятку раз. Поиск возвращает пять копий одного фрагмента и вытесняет полезное. Решение: собирать письма в треды по заголовкам In-Reply-To и References и вырезать цитирование до чанкинга.
На большом архиве полная обработка почты и вложений у меня занимала до двух недель. Поэтому новую версию индекса строю в отдельных коллекциях MongoDB и Qdrant, а старая продолжает отвечать пользователям. Переключение — одной переменной окружения.
Аннотации вопросами
Приём, который заметно улучшил поиск: для каждого документа локальная LLM генерирует список вопросов, на которые он отвечает, и краткое описание. Индексируются и чанки текста, и эти вопросы. Пользователь спрашивает своими словами, а его запрос семантически ближе к вопросу, чем к канцелярскому тексту регламента. Аннотации пересчитываются автоматически при изменении документа.
Ещё одна деталь: в метаданных чанка храните дату документа. Иначе на вопрос про текущий порядок модель с равной уверенностью процитирует регламент пятилетней давности.
1С и Битрикс24: не индексировать, а спрашивать
Учётные данные в векторную базу класть не нужно. Остатки, сделки и задачи меняются каждый час, а семантический поиск по таблице с суммами даёт мусор. Вместо этого агент получает инструменты и сам делает запрос в момент вопроса.
Как достать данные
-
1С — стандартный интерфейс OData. Базу публикуют на веб-сервере, нужные справочники и регистры включают в состав интерфейса, дальше обычные HTTP-запросы с
$filterи$format=json. Для сложной логики удобнее HTTP-сервис в конфигурации, который отдаёт уже собранный ответ. -
Битрикс24 — REST API через входящий вебхук с нужными правами:
crm.deal.list,crm.company.list,tasks.task.listи так далее. Для тиражируемого решения правильнее локальное приложение с OAuth.
Инструменты через MCP
Инструменты оформляю как MCP-сервер на FastMCP. Плюс в том, что один и тот же сервер подключается к своему агенту, к Open WebUI и к любому MCP-клиенту без переделок. Упрощённый пример:
import os
import httpx
from fastmcp import FastMCP
mcp = FastMCP("corp-tools")
B24 = os.environ["B24_WEBHOOK"] # https://<портал>/rest/<id>/<токен>/
ODATA = os.environ["ODATA_URL"] # https://<сервер>/<база>/odata/standard.odata/
AUTH = (os.environ["ODATA_USER"], os.environ["ODATA_PASS"])
@mcp.tool
def deals_by_company(company_id: int, limit: int = 20) -> list[dict]:
"""Открытые сделки компании в Битрикс24."""
r = httpx.post(B24 + "crm.deal.list.json", json={
"filter": {"COMPANY_ID": company_id, "CLOSED": "N"},
"select": ["ID", "TITLE", "STAGE_ID", "OPPORTUNITY"],
}, timeout=30)
return r.json()["result"][:limit]
@mcp.tool
def find_counterparty(name: str) -> list[dict]:
"""Поиск контрагента в 1С по части наименования."""
q = f"substringof('{name}', Description)"
r = httpx.get(ODATA + "Catalog_Контрагенты", auth=AUTH, params={
"$filter": q, "$top": 10,
"$select": "Ref_Key,Description,ИНН", "$format": "json",
}, timeout=30)
return r.json()["value"]
if __name__ == "__main__":
mcp.run(transport="http", host="0.0.0.0", port=8100)
Имена объектов и реквизитов зависят от конфигурации, проверяйте их в метаданных ($metadata). В боевом коде строку фильтра нужно экранировать: аргументы инструментов составляет модель, а не доверенный код.
Права доступа
Инструменты только на чтение, технический пользователь с минимальными правами в 1С и вебхук с ограниченным набором разрешений. Если разные сотрудники должны видеть разное, фильтруйте на стороне инструмента по пользователю, а не просите модель «не показывать» лишнее. Запись в учётные системы через агента — только с подтверждением человека.
Модель и инференс
Итоговый выбор: vLLM и MoE-модель семейства Qwen с небольшим числом активных параметров (Qwen3.6-35B-A3B). Для нескольких одновременных пользователей скорость ответа оказалась важнее последних процентов качества.
Почему MoE, а не плотная модель
Я сравнивал три варианта: большую MoE на ~120B, плотную на ~27B и MoE на 35B с ~3B активных. Большая едва помещается в одну карту и оставляет мало места под KV-кэш. Плотная на 27B даёт хорошее качество, но заметно медленнее для пользователя. MoE на 35B с 3B активных отвечает быстро, нормально работает с инструментами и оставляет память под параллельные запросы.
Тестировал на A100 80 ГБ, целевое железо — одна карта класса RTX PRO с 96 ГБ. Ваши цифры скорости будут зависеть от квантизации, длины контекста и числа пользователей, так что замерьте на своём сценарии.
Настройки, которые важны
-
Длина контекста. Я держу 32K. Больше для RAG обычно не нужно, а каждый лишний токен съедает память под параллельные сессии.
-
Вызов инструментов. В vLLM нужно явно включить автовыбор инструментов и указать парсер под семейство модели, иначе вызовы приходят текстом.
-
Ключ API. Даже во внутренней сети — vLLM даёт OpenAI-совместимый API, и без ключа его может дёрнуть кто угодно.
Эмбеддинги и поиск
Эмбеддинг-модель поднимается отдельным контейнером, вектора хранятся в Qdrant, исходные документы и аннотации — в MongoDB. Для русскоязычных документов берите мультиязычную модель и проверяйте её на своих же вопросах: публичные бенчмарки тут плохо предсказывают результат.
Перед поиском локальная модель переписывает запрос с учётом истории диалога. «А для поставщиков?» само по себе ничего не найдёт, а «порядок согласования договора для поставщиков» — найдёт.
Гибрид с облаком — по желанию
Если политика компании допускает облако, полезна схема разделения: предобработка, поиск и работа с инструментами остаются локально, а облачная модель получает только то, что разрешено. Например, обезличенное описание задачи, когда локальный агент застрял. Переключение провайдера делается в шлюзе, сервисы об этом не знают.
Интерфейс и шлюз
Между сервисами и моделями стоит один шлюз с OpenAI-совместимым API, и это решение окупилось больше всего. На нём живут список моделей, проксирование запросов вместе с tools, ключи и статистика по пользователям. Больше ничего: сборка промпта, история и файлы — забота клиентских сервисов.
Что это даёт:
-
замена модели или переезд с Ollama на vLLM затрагивает один конфиг, а не все сервисы;
-
любой новый сервис подключается стандартным OpenAI SDK;
-
видно, кто и сколько нагружает модель.
Исключение — фоновые задачи с большим потоком запросов, например пакетная обработка данных из 1С. Их я пускаю напрямую в vLLM, чтобы не нагружать шлюз.
Open WebUI как фронтенд
Open WebUI закрывает чат, авторизацию, историю и группы пользователей из коробки. RAG-логику я не делаю на его встроенных книгах знаний. Вместо этого свой бэкенд на FastAPI притворяется «моделью» с OpenAI-совместимым API. Open WebUI отправляет ему диалог, а внутри происходит переписывание запроса, поиск, вызов инструментов и ответ. Так логика не зависит от фронтенда, и тот же бэкенд доступен из других систем.
Две детали, которые пользователи ценят больше всего:
-
Ссылки на источники в каждом ответе: страница вики, файл, письмо. Без них ответу не доверяют, и правильно делают.
-
Статус поиска в реальном времени: «ищу в почте», «запрашиваю 1С». Агент с инструментами может работать заметно дольше обычного чата, и пустой экран воспринимается как зависание.
Промпты — в одном месте
Все системные промпты и шаблоны лежат в одном prompts.json, а не разбросаны по коду. Когда промптов становится пара десятков, это единственный способ понять, что именно видит модель.
Грабли и выводы
Главный вывод: локальный ассистент делается не моделью, а конвейером данных и аккуратной интеграцией. Модель меняется раз в полгода, а парсеры, аннотации и инструменты остаются.
Что сломалось или обошлось дороже, чем ожидалось:
-
Полная переиндексация — это дни и недели. Инкрементальное обновление по датам изменения нужно закладывать с первого дня, а новую версию индекса строить рядом со старой.
-
Дубли из почтовых цепочек тихо портят выдачу. Заметить это можно, только если смотреть на найденные чанки, а не только на финальный ответ.
-
Извлечение заранее теряет детали. Если LLM заранее вытаскивает «важное» из документа, часть фактов неизбежно теряется. Я перехожу к схеме, где агенты по почте и файлам находят кандидатов по аннотациям, а детали читают из исходника в момент вопроса.
-
Прямой доступ к БД вики быстрее на старте и дороже потом. REST API сразу сэкономил бы время.
-
Смена длины контекста из клиента в Ollama приводила к перезагрузкам модели и тормозам. В vLLM параметры задаются при запуске сервера, и эта проблема уходит.
Что бы я сделал иначе с самого начала:
-
Собрал бы набор из 50–100 реальных вопросов сотрудников с ожидаемыми источниками и прогонял его после каждого изменения. Без этого любая «оптимизация» — гадание.
-
Сразу поставил шлюз между сервисами и моделями.
-
Делал инструменты к 1С и Битрикс24 как MCP-сервер с первой версии, а не вшивал в код агента.
Если делаете похожее или думаете внедрить такое у себя, пишите в комментариях — разберём ваш случай.
Автор: Stilist


