Как я сделал локальный RAG-сервис для SRE: ищем по документации, ранбукам и коду через Ollama

от автора

Недавно я делал учебный проект про автоматизацию документирования инцидентов. Поначалу планы были грандиозными: инциденты, таймлайны, интеграции с мониторингами, чатами, постмортемы, подсказки дежурным инженерам.

Но довольно быстро стало понятно, что с временными и ресурсными ограничениями лучше не пытаться написать маленький PagerDuty. Поэтому я сузил задачу до более реалистичного ядра: локального RAG-сервиса, который ищет по документации, ранбукам и коду, а затем передаёт найденный контекст в LLM.

Так появился llmortem — FastAPI-сервис, который можно подключить к OpenWebUI как OpenAI-compatible backend.

В статье расскажу, как устроена архитектура, почему я начал с BM25, зачем индексировать docstring’и и какие ограничения у такого подхода.

Репозиторий на Гитхабе.

В чем вообще проблема?

Во время инцидента инженер редко работает с одним источником информации. Обычно нужно быстро найти и сопоставить:

  • документацию проекта;

  • SRE-документацию;

  • ранбуки;

  • фрагменты кода;

  • логи;

  • метрики;

  • сообщения из чатов;

  • описание релиза.

Часть информации уже есть в проекте, но она разбросана по разным файлам и директориям. Например, инструкция по queue lag лежит в ранбуке, описание регистрации — в пользовательской документации, а детали поведения функции — только в docstring’е.

LLM тут кажется хорошим помощником: можно спросить “что делать, если растёт queue lag?” или “сгенерируй черновик постмортема”. Но если просто отправить вопрос в модель, она не знает внутреннюю документацию проекта и может ответить слишком общо или начать придумывать детали.

Поэтому вместо обычного “чата с моделью” я сделал RAG-прослойку:

  1. пользователь задаёт вопрос;

  2. сервис ищет релевантные фрагменты в локальных источниках;

  3. найденный контекст добавляется в prompt;

  4. prompt отправляется в локальную LLM;

  5. пользователь получает ответ, привязанный к материалам проекта.

Получилось нечто следующее:

llmortem умеет:

  • индексировать Markdown-документацию;

  • индексировать SRE-документацию и ранбуки;

  • извлекать из кода docstring’и, комментарии и сигнатуры;

  • строить BM25-индекс;

  • искать релевантный контекст;

  • определять тип запроса: документационный, SRE/incident или общий;

  • формировать prompt для LLM;

  • обращаться к локальной модели через Ollama;

  • отдавать ответы через OpenAI-compatible endpoint;

  • подключаться к OpenWebUI;

  • генерировать черновик постмортема.

Конечно же это не полноценная платформа инцидент-менеджмента. Тут нет on-call, эскалаций, автоматического таймлайна и интеграций с Grafana/Slack/GitLab. Это именно RAG-ядро, которое можно развивать дальше.

Архитектура

Общая схема выглядит так:

Пользователь    ↓OpenWebUI    ↓/v1/chat/completions    ↓FastAPI-сервис llmortem    ↓Intent detection    ↓BM25 retrieval по docs / runbooks / code    ↓Prompt builder    ↓Ollama    ↓Ответ пользователю

OpenWebUI ничего не знает о внутренней логике сервиса. Для него llmortem выглядит как обычный OpenAI-compatible backend. А уже внутри FastAPI-приложения происходит поиск по локальным источникам, сборка prompt’а и обращение к Ollama.

Это оказалось удобным решением: не нужно писать отдельный frontend, а RAG-логику можно развивать независимо.

И тут может возникнуть резонный вопрос: почему BM25, а не векторная база?

Когда говорят про RAG, часто сразу вспоминают embeddings и vector database. Я решил начать с BM25. Причина простая: для технической документации часто важны точные совпадения.

Например:

  • конкретные слова;

  • код ошибки;

  • названия endpoint’ов;

  • имена конфигов;

  • названия метрик.

В таких случаях обычный лексический поиск может работать достаточно хорошо. При этом BM25 сильно проще: не нужна отдельная векторная база, не нужно выбирать embedding-модель и не нужно пересчитывать эмбеддинги.

Для прототипа это был хороший компромисс: сначала проверить архитектуру целиком, а semantic search оставить как развитие.

Ограничение тоже очевидное: если пользователь формулирует вопрос сильно иначе, чем написано в документации, BM25 может не найти правильный фрагмент. Поэтому в будущем сюда хорошо ложится гибридный поиск: BM25 + embeddings.

Что индексируется

В прототипе индексируются три типа источников:

docs/  registration.md  sre/    queue-lag.mdrunbooks/  api-5xx-errors.md  auth-401-403.md  database-connection-errors.md  disk-space-full.md  external-api-timeout.md  high-cpu-service.md  queue-lag.md  release-rollback.mdllmortem/  *.py

Для документационных вопросов приоритет получает docs.

Для SRE/incident-запросов — docs/sre и runbooks.

Для вопросов по реализации дополнительно используется поиск по коду.

Например, запрос:

queue lag, what to do?

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

Поиск по коду

Отдельно я добавил поиск по коду. Идея простая: документация может отставать от реализации, а в коде иногда остаются полезные docstring’и и комментарии.

Для Python можно использовать AST и извлекать:

  • docstring модуля;

  • docstring класса;

  • docstring функции;

  • сигнатуры функций;

  • сигнатуры классов;

  • комментарии.

Например, если добавить файл:

"""Authentication helper module.Password reset flow:1. User opens the password reset page.2. User enters email.3. System sends a reset link.4. User opens the link and sets a new password."""def reset_password(email: str) -> None:    """Send password reset link to the user's email address."""    pass

то после переиндексации можно спросить:

curl -X POST http://localhost:8000/search \  -H "Content-Type: application/json" \  -d '{"query":"How to change the password?", "top_k": 5}' \  | python3 -m json.tool

и получить релевантный фрагмент именно из кода.

Конечно, это не полноценный поиск по коду. Но даже такой простой подход полезен, если документация неполная, а в коде есть хотя бы базовые описания.

Endpoint’ы

Основные endpoint’ы сервиса:

GET  /healthPOST /reindexPOST /searchPOST /ask/docsPOST /draft-docPOST /postmortemPOST /v1/chat/completions

/health проверяет состояние сервиса.

/reindex пересобирает индекс после изменения документации, ранбуков или кода.

/search позволяет проверить retrieval отдельно от LLM.

/ask/docs отвечает на вопросы по документации и SRE-сценариям.

/postmortem генерирует черновик постмортема.

/v1/chat/completions нужен для подключения OpenWebUI.

Подключение OpenWebUI

Чтобы не заморачиваться со своим интерфейсом, я реализовал OpenAI-совместимый endpoint.

В OpenWebUI можно добавить backend:

Base URL: http://host.docker.internal:8000/v1API key: any value

После этого пользователь работает с llmortem как с обычной моделью в чате. Но внутри запрос проходит через retrieval:

OpenWebUI → llmortem → поиск контекста → Ollama → ответ

Это удобно, потому что можно сосредоточиться на backend-логике и не тратить время на UI.

А что с производительностью?

В моём случае основная задержка была связана не с RAG-прослойкой, а с ожиданием ответа локальной LLM.

Индекс строится заранее: при старте сервиса или через /reindex. Поэтому на каждый пользовательский запрос не происходит полного обхода репозитория.

Во время запроса выполняются только:

  • определение цели запроса;

  • BM25-поиск;

  • сборка prompt’а;

  • вызов Ollama.

Если сервис начнёт упираться именно в поиск, можно добавить кэширование или гибридный поиск. Но в текущем прототипе узким местом была модель, запущенная локально, ибо она не очень-то и мощная(по понятным причинам). В любом случае хорошая RAG прослойка дает довольно хорошие ответы даже со слабенькой моделью.

Что получилось хорошо

BM25 оказался нормальным стартом для технической документации. Для терминов вроде queue lag, 5xx, database connection, reset_password точный поиск работает достаточно хорошо.

OpenAI-совместимый API сильно упростил интеграцию с OpenWebUI. Не пришлось писать frontend.

Поиск по коду тоже оказался полезным. Даже docstring’и и сигнатуры могут дать модели контекст, которого нет в документации.

RAG-подход делает ответы более привязанными к проекту. Даже компактная локальная модель отвечает лучше, если передать ей релевантный фрагмент ранбука или документации.

Что не получилось идеально

BM25 не понимает семантику. Если вопрос сформулирован не так, как написано в документации, правильный фрагмент может не попасть в контекст.

Локальная LLM может быть медленной, особенно на слабом железе.

В системе пока нет полноценной модели инцидента, хранения истории, интеграций с мониторингами и автоматического таймлайна.

Куда можно дальше расти

Дальше наверное я бы развивал проект в таких направлениях:

  • добавить embeddings и сделать гибридный поиск;

  • подключить Grafana или другой источник алертов;

  • добавить импорт сообщений из Slack или Mattermost;

  • подключить GitLab/GitHub для анализа релизов и merge request’ов;

  • сохранять инциденты и постмортемы в БД;

  • искать похожие прошлые инциденты;

Вывод

Я начинал с идеи системы для автоматизации документирования инцидентов, но в итоге пришёл к более простой и полезной задаче: сделать локальное RAG-ядро для работы с инженерными знаниями проекта.

llmortem индексирует документацию, ранбуки и код, ищет релевантные фрагменты через BM25 и передаёт их локальной LLM через Ollama. Через OpenAI-compatible API сервис подключается к OpenWebUI и выглядит для пользователя как обычный чат.

Это не замена PagerDuty или incident.io. Скорее, это небольшой слой поверх внутренних знаний проекта, который помогает быстрее находить инструкции, задавать вопросы к документации и получать черновики постмортемов.

Главный вывод для меня: даже простая RAG-архитектура с BM25 и локальной моделью может быть полезной, если аккуратно выбрать источники знаний и не пытаться выдавать LLM за абсолютный источник истины.

ссылка на оригинал статьи https://habr.com/ru/articles/1043928/