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

Сначала надо зафиксировать архитектуру
Первое решение было не техническим, а дисциплинарным: зафиксировать MVP и больше ничего в него не добавлять.
Очень хотелось сразу сделать свою веб-морду, граф знаний, автоматическую синхронизацию папок, красивый переключатель моделей, отдельные пайплайны и всё остальное, что обычно превращает рабочий прототип в стройку века.
Я решил внедрять стандартную схему:
Пользователь
↓
RAGFlow Web UI
↓
отдельная база знаний по одному станку
↓
hybrid retrieval
↓
Qwen3-Embedding-8B
↓
Qwen3-Reranker-4B
↓
локальная LLM или внешняя исследовательская модель по API
Важный момент: для первого варианта я сделал два chat-профиля на одной и той же базе знаний. У них одинаковые документы, но разные LLM API: один локальный (который развернут на NVIDIA Jetson), второй внешний исследовательский по API Openrouter (это дает доступ к сильным моделям типо GPT-6).
Это простое решение, но оно сильно снижает риск галлюцинаций. База знаний одна, retrieval один, меняется только генератор ответа.
Почему RAG, а не просто дообучить модель?
В заводской документации нельзя полагаться на память модели. Тут важна точность. RAG дает эту точность, т.к. мы всегда знаем первоисточник. А вот ессли дообученная модель уверенно придумает назначение сигнала, канал модуля или связь с электрической схемой - то мы это даже понять не сможем сразу.
Поэтому главный принцип я зафиксировал так:
LLM не решает, искать ей документы или нет.
Retrieval выполняется обязательно.
Reranking выполняется обязательно.
И только потом LLM получает найденный контекст + мой промт.
То есть модель аккуратный переводчик между человеком и уже найденными фрагментами документации.
Для поиска выбрал hybrid retrieval. Семантический поиск нужен для запросов вроде: найти описание узла по смыслу. А текстовый поиск нужен для заводской реальности:
X161
D5708
W1482
5A25
CH187
Если искать только по смыслу, такие штуки легко потерять. А если искать только по точному совпадению, система плохо понимает человеческие вопросы.
Локальный embedding: без него базы знаний вообще нет
Первой обязательной моделью был embedding. Без него документы не превращаются в векторное пространство, а значит нормального semantic retrieval нет.
Я поднял локальный сервис на Qwen3-Embedding-8B в BF16. Работает через vLLM в Docker и отдаёт OpenAI-compatible /v1/embeddings API.
Критерии были практические:
русский
английский
китайский
техническая терминология
длинные описания
желательно OpenAI-compatible API
Reranker: то, на чем я всегда настаиваю
Reranker я по умолчанию считаю желательным. Без embedding, например, базы нет вообще. Без reranker поиск хуже, но уже можно тестировать.
В итоге поднял Qwen3-Reranker-4B в BF16. Он тоже работает локально, в общей Docker-сети с RAGFlow. Отдельно настроил persistent model cache, чтобы веса модели не скачивались и не пересобирались заново при каждом пересоздании контейнера.
Почему 4B, а не 8B? По бенчмаркам разница для reranking небольшая, а местами 4B даже выглядит лучше. Зато 8B полезнее отдать embedding-модели, потому что именно она строит первичное пространство поиска.
Проверял не на абстрактных фразах, а на промышленном multilingual-примере: русский запрос, русский релевантный фрагмент, китайское описание и нерелевантные документы. Релевантные куски оказались сверху, нерелевантные ушли вниз.

Локальная LLM через OpenAI-compatible API
С генератором ответа я специально сделал слой совместимости. RAGFlow не должен знать, что внутри конкретно Qwen, Ollama или что-то ещё.
Для него это просто OpenAI-compatible endpoint:
/v1/models
/v1/chat/completions
model = qwen3:32b
Bearer auth
Локальная модель Qwen3:32B была подключена через собственный Flask-прокси к Ollama. Это оказалось правильным архитектурным решением: если завтра модель начнёт вести себя плохо, можно заменить backend, но сохранить тот же контракт API, что позволяет быстро менять модель, не меняя архитектуру всего проекта.
Был и интересный момент: после запуска embedding и reranker старый memory guard на NVIDIA Jetson начал блокировать загрузку сразу трех моделей (включая Qwen3:32B). Пришлось разбиратся в причинах и поднимать лимит RAM guard до 85%, отдельно изменить guard по GPU-памяти и оставить reserve под модель. Оказывается, даже мощностей NVIDIA Jetson не всегда хватает под локальные задачи.
Самая важная часть - база знаний
Потом началась менее романтичная, но самая полезная часть: собрать корпус документов.
По одному станку документы лежали в разных источниках: официальные PDF, электрические схемы, выгрузки PLC/HMI/Motion, таблицы регистров, комментарии, alarm maps, сервисные материалы, исследования, ML-артефакты.
Первичная инвентаризация дала:
617 файлов всего
386 RAG-friendly
231 бинарный, графический или проектный файл
Потом прошёлся по дублям:
82 точных дубля по SHA256
43 файла уже были в RAGFlow
261 новый уникальный RAG-friendly файл
В работу ушло около 280 документов, а после всех ремонтов и дополнений итоговая база получилась такой:
320 документов полностью обработаны
4 дали zero chunks (их пришлось "копать" руками)
40 049 chunks
примерно 19 млн tokens
Где всё ломалось
CSV оказались не UTF-8
Часть PLC и HMI exports падала при chunking из-за кодировки. Некоторые файлы были UTF-16, другие cp1251 или cp1250.
Решение простое: автоматически перекодировать в UTF-8-SIG и повторно отправить на индексацию. После этого проблемные CSV нормально попали в repair_ok.
Большие документы превышали контекст embedding-модели
Некоторые Markdown/PDF chunks превышали лимит embedding-контекста. Большие Markdown-файлы пришлось физически разрезать на небольшие самостоятельные документы, зато потом retrieval работает предсказуемо.
JSON не всегда RAG-friendly
Несколько structured JSON-файлов давали zero chunks. Я перевёл их в Markdown, сохранив структуру полей в виде:
object[index].field: value
После этого документы нормально проиндексировались.
UI-режим оказался важнее, чем казалось
Самый неприятный баг был не в моделях, а в режиме работы RAGFlow.
В режимах Low / Medium / High / Ultra система переходила в agentic workflow и пыталась заставить LLM самостоятельно вызвать инструмент rag (так она настроена из коробки). А локальный Qwen в этой конфигурации такой workflow корректно не поддерживал и давал ожидаемый отказ.
В логах было видно, что фактические chunks до модели не доходят. Модель то отвечала странно, то начинала фантазировать из своих весов.
Помог режим Native. После переключения логика стала такой, как и нужно:
retrieval обязателен
↓
reranking обязателен
↓
контекст передаётся LLM
↓
ответ строится по найденным источникам
Для диагностики я добавил audit trail в LLM API: сохраняется исходный запрос, payload в backend, streaming chunks, итоговый текст, usage, длительность, статус и request ID. Это быстро показало, что прокси не искажает запросы, а неправильный prompt приходит выше по цепочке.
Контрольный тест
Финально я проверял систему на точном промышленном вопросе по PLC-сигналу. Нужно было найти конкретный идентификатор, шкаф, модуль/канал, назначение сигнала и соседние сигналы.
Система прошла полный путь:
вопрос
↓
embedding
↓
hybrid retrieval
↓
reranking
↓
контекст
↓
локальная LLM
↓
ответ со ссылкой на источник
Правильная таблица с нужным диапазоном сигналов стабильно находилась среди первых результатов и передавалась генератору.
Это был важный момент: не просто чат ответил красиво, а именно retrieval нашёл нужный технический фрагмент.
Что я бы вынес как практические правила
Если собирать похожую систему для документации, я бы держался таких правил:
- Не смешивать разную документацию в одну базу знаний. Например, одинаковые PLC-адреса на разных машинах быстро превратят поиск в кашу.
- Делать hybrid retrieval, потому что техническая документация живёт одновременно в смыслах и в точных идентификаторах.
- Не давать LLM право решать, нужен ли поиск. В промышленной документации retrieval должен быть обязательным.
- Reranker полезен, его можно считать вторым слоем качества.
- Проверять не демо-вопросами, а реальными PLC/HMI/электрическими идентификаторами.
- Делать audit trail сразу. Когда пайплайн длинный, без логов быстро становится ничего непонятно.
Вывод
В итоге получился рабочий локальный MVP для анализа заводской документации: отдельная база знаний по одному станку, локальные embedding и reranker, локальная LLM через совместимый API, возможность подключать внешнюю исследовательскую модель и нормальный end-to-end путь от вопроса до ответа по источникам.
Самое ценное здесь не в том, что рядом с документацией появился чат. Ценность в том, что появилась инженерно контролируемая прослойка между живым вопросом специалиста и большим техническим архивом: с изоляцией данных, понятным retrieval, логами, repair-процессом документов и возможностью менять модели без перестройки всей системы.