← § BLOG

Локальная LLM в мониторинге сети: Ollama + MCP + Spring Boot за три дня

Как я подключил локальную LLM к системе мониторинга OLT через MCP. Архитектура, in-process диспетчер вместо HTTP, 16 tools, SSE в браузер.

#llm#mcp#spring-boot#ollama#case

В GetOLT, сервисе мониторинга PON-сетей, который я разрабатываю (как он появился) — 350+ OLT и 100K+ ONU. Когда техотделу нужно ответить на «сколько ONU в Алматы с плохим сигналом на CDATA?», никто не пишет SQL. До этой недели — 5 кликов в фильтрах, после — один вопрос в чат.

Бот отвечает на естественном языке и ходит в реальную базу, не в кэш. От первого коммита до прогона на трёх живых OLT — три дня.

Почему локальная LLM

Первый вопрос — почему не GPT/Claude через API. Ответ продуктовый, не технический:

  • Данные клиентов — биллинговые id, IP оборудования, договорные логины — не должны уезжать в облако третьих лиц
  • 152-ФЗ: согласие пользователя на обработку ПДн — есть, на трансграничную передачу — нет, и получать его ради удобства бота незачем
  • gpt-oss:20b на Apple Silicon выдаёт 30-40 tokens/sec — приемлемо для интерактивного чата
  • 0 рублей за запрос, лимит — мощность железа, а не бюджет

Если бы я делал ассистента для open-source инструмента, выбор был бы другим. Для коммерческого продукта в B2B-телекоме — локальная LLM единственный честный путь.

Архитектура: три коробки

[Browser]  ←SSE—  [LlmChatController]  →  [LlmChatService]
                                              │ tool-loop
                                              ▼
                              ┌──────────────┬──────────────────┐
                          [Ollama]     [InProcessMcpDispatcher]
                       gpt-oss:20b              │
                                                ▼
                                          [16 MCP tools]
                                                │
                                                ▼
                                  [OltService, OnuService, ...]
                                                │
                                                ▼
                                            [MySQL]

Главное решение — in-process MCP dispatcher. Стандартный MCP подразумевает отдельный процесс с HTTP или stdio-транспортом. У меня LLM-loop и MCP-сервер живут в одном Spring Boot — поэтому я выкинул сериализацию. Tool-вызов это обычный Java-метод по имени и JSON-аргументам, без HTTP-моста.

Минус: нельзя переиспользовать MCP-сервер из других клиентов (Claude Desktop, например). Плюс: меньше кода, меньше задержки, единый Security-контекст и аудит. Для embedded-ассистента в продукте это правильный tradeoff.

LlmChatService.run — обычный tool-loop:

while (turn < MAX_TURNS) {
    var response = ollama.chat(messages, toolSchemas);
    if (response.toolCalls().isEmpty()) {
        emit(response.content());
        break;
    }
    for (var call : response.toolCalls()) {
        var result = mcpDispatcher.invoke(call.name(), call.args());
        messages.add(toolResultMessage(call.id(), result));
        emit(new ToolEvent(call, result));
    }
    turn++;
}

SSE-streaming передаёт пользователю не только финальный текст, но и каждый tool-вызов с аргументами и результатом. Видно, как бот строит ответ — какой фильтр применил, что вернула БД. Это и UX-фича, и средство отладки.

Что получилось end-to-end

  • 16 MCP-tools поверх существующих сервисов: olt.list, onu.stats_by_olt, olt.distinct_values, data.export, feedback.send и другие. Каждый — узкий, с явной JSON-схемой.
  • Полный append-only аудит: chat_session + chat_message + mcp_chat_log. Любой диалог восстанавливается побайтово.
  • Эфемерные file-attachments: модель может выгрузить агрегат в CSV/JSON, файл живёт 5 минут, ссылка одноразовая.
  • Read-only SQL escape hatch: если задача не покрыта tools, модель может попросить SQL — но через отдельный tool с SELECT-только-валидацией. Это снижает галлюцинации и даёт зону gradual coverage.

Прогон на проде, три живых OLT, реальные данные:

  • «покажи ONU с сигналом хуже -27 dBm на CDATA в Алматы» — отвечает за 4-6 секунд
  • «сколько портов заполнено выше 75% по операторам?» — считает агрегат, рисует таблицу
  • «выгрузи список проблемных ONU за неделю» — генерирует CSV, отдаёт ссылку

Что было сложно

Три категории, каждая тянет на отдельный пост:

  1. SSE + Spring Security + async-dispatch. Три отдельные ловушки, из-за которых SSE падал с AccessDenied на финализации. @PreAuthorize ломал async-dispatch, SecurityContext не пробрасывался в worker thread, SecurityFilterChain бил по DispatcherType.ASYNC. Разбор — в следующем посте.

  2. Prompt engineering как инженерная дисциплина. Модель не звала SQL для агрегатов, выдумывала несуществующие города и IP-адреса, выводила внутренние db-id наружу. Решение — не апгрейд модели, а дизайн tools и system prompt'а как кода. Подробный разбор — в посте про prompt-as-code.

  3. Browser prefetch съедал file-attachments. Классическая ловушка: браузер делал HEAD/GET по ссылке для preview, и одноразовый токен сгорал до клика пользователя. Решилось Cache-Control: no-store + Sec-Purpose: prefetch фильтром на сервере. Стоит отдельной заметки.

Что я понял

  1. In-process MCP > HTTP MCP, если LLM-loop и сервер живут в одном процессе. Сериализация ради сериализации — лишний код.
  2. Tool design > model size. Точность ответов выросла больше от добавления distinct_values tool, чем от любых попыток подобрать модель помощнее.
  3. Prompt — это код. Каждая «странность» поведения это commit в system prompt, а не «AI глупый».
  4. Локальная LLM — это вопрос политики, не возможностей. gpt-oss:20b с tool-calling закрывает 90% задач embedded-ассистента, не выходя за пределы сервера.

AI-ассистент в продукте — это не модель. Это сотня строк tool-loop'а, набор узких MCP-tools поверх существующих сервисов и три страницы system prompt'а. Модель, которую ты выберешь — наименее интересная часть стека.

Поделиться
TelegramX (Twitter)
Discussion

Комментарии работают через Giscus + GitHub. По клику ваши данные передаются в GitHub Inc. (США). Не будете кликать — ничего не отправляется.

Открыть обсуждение на GitHub
Локальная LLM в мониторинге сети: Ollama + MCP + Spring Boot за три дня · Григорий Масич