Забудьте про мониторинг: как создать самоисцеляющийся микросервис, который сам фиксит свои баги

Пошаговое руководство по развертыванию автономного сервиса на стеке Ray Serve + OpenAPI + Агентский LLM. Рабочие промпты, конфигурации YAML и код для production-окружения 2026 года.

1. Архитектурный обзор: от спецификации к самоисцелению

Традиционный подход (OpenAPI → ручная реализация → деплой → мониторинг → реакция инженера) умирает. Вместо этого мы строим пайплайн, где LLM-агент становится частью runtime-окружения:

  1. OpenAPI-спецификация — единственный источник истины о контрактах.
  2. Генерация кода — Llama 3.1/Mistral Large создаёт сервис с обработкой ошибок на основе спецификации.
  3. Ray Serve — развёртывание с pipeline-параллелизмом и автоскейлингом.
  4. Агент самоисцеления — парсит логи, детектирует аномалии, генерирует патч и применяет его без downtime.
  5. A/B тестирование стратегий — сравнение Instant Rollback vs In-Place Fix по метрике MTTR.
"Ключевой инсайт 2025 года: самоисцеление ≠ автономное исправление. Это цикл Detect → Diagnose → Patch → Verify с человеческим в loop только для стратегических решений. MTTR сокращается с часов до минут, но false-positive rate должен оставаться <0.1%."
— Исследование arXiv:2502.09766

2. Установка окружения: Ray, BentoML и зависимости

Используем Python 3.11+ и изолируем окружение через uv (альтернатива pipenv/poetry):

Файл pyproject.toml

[project]
name = "self-healing-service"
version = "0.1.0"
dependencies = [
    "ray[serve]>=2.40.0",
    "bentoml>=1.2.0",
    "fastapi>=0.110.0",
    "pydantic>=2.0",
    "openapi-spec-validator>=0.7.0",
    "httpx>=0.27.0",
    "prometheus-client>=0.20.0",
    "langchain>=0.2.0",  # для LLM-агента
    "ollama>=0.3.0",     # локальный Llama 3.1
]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

Установка и проверка

# Инициализация
uv venv .venv
source .venv/bin/activate  # Linux/Mac
# или .venv\Scripts\activate  # Windows

# Установка
uv pip install -e .

# Проверка Ray
ray start --head --port=6379
ray status

# Запуск BentoML (для сборки контейнеров)
bentoml --version

* Для production используйте Docker-образ с предустановленным Ray и BentoML. Настройте ray start --head --dashboard-host=0.0.0.0 для доступа к Dashboard.


3. OpenAPI спецификация: контракты как код

Спецификация должна быть валидной и содержать примеры ошибок. Ray Serve умеет автоматически генерировать FastAPI-сервер из OpenAPI, но нам нужна кастомизация.

Файл api/spec.yaml

openapi: 3.1.0
info:
  title: Self-Healing Payment Service
  version: 1.0.0
  description: |
    Пример сервиса с самоисцелением. Обрабатывает платежи и генерирует 
    искусственные ошибки для демонстрации агента.

paths:
  /process:
    post:
      summary: Обработка платежа
      operationId: process_payment
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PaymentRequest'
      responses:
        '200':
          description: Успех
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '500':
          $ref: '#/components/responses/InternalError'

components:
  schemas:
    PaymentRequest:
      type: object
      required: [amount, currency]
      properties:
        amount:
          type: number
          example: 100.50
        currency:
          type: string
          example: "USD"
        fail_with:
          type: string
          enum: [none, division, timeout]
          default: none
          description: |
            Искусственная ошибка для тестирования агента:
            - division: ZeroDivisionError
            - timeout: asyncio.TimeoutError

    PaymentResponse:
      type: object
      properties:
        transaction_id:
          type: string
        status:
          type: string
          enum: [completed, failed]

  responses:
    BadRequest:
      description: Некорректные данные
      content:
        application/json:
          schema:
            type: object
            properties:
              error:
                type: string
                example: "Invalid currency"

    InternalError:
      description: Внутренняя ошибка сервера
      content:
        application/json:
          schema:
            type: object
            properties:
              error:
                type: string
                example: "Division by zero"
              traceback:
                type: string
                example: "Traceback (most recent call last): ..."

* Поле fail_with — это "дырявый" контракт для инъекции ошибок. В production его не будет, но агент должен уметь обрабатывать такие исключения.


4. Генерация кода через LLM: промпты, которые работают

Не генерируйте весь сервис целиком. Разбейте на микро-задачи. Пример промпта для генерации обработчика ZeroDivisionError:

Промпт для Claude 3.5 Sonnet / Llama 3.1 (через Ollama)

Ты — senior Python-разработчик. Напиши обработчик исключения для FastAPI эндпоинта.

Контекст:
- Эндпоинт: POST /process
- Вход: JSON с полями amount (float) и fail_with (str)
- Логика: 
  if fail_with == "division": 
      result = 100 / 0  # искусственная ошибка
  else:
      result = amount * 0.9  # комиссия 10%

Требования:
1. Перехвати ZeroDivisionError и верни 500 с JSON: {"error": "Division by zero", "traceback": "..."}.
2. traceback должен содержать только последний фрейм (используй traceback.extract_tb()).
3. Добавь structured logging в JSON-формате: {"event": "error", "error_type": "...", "path": "/process"}.
4. Код должен соответствовать PEP8, использовать type hints.

Пример вывода:
{
  "error": "Division by zero",
  "traceback": "ZeroDivisionError: division by zero"
}

Не пиши полный сервис, только функцию-обработчик.

Результат (сгенерированный LLM, проверенный вручную):

Генерируемый код: service/error_handler.py

import traceback
from typing import Any, Dict
from fastapi import Request, HTTPException
import logging

logger = logging.getLogger("self_healing")

async def handle_division_error(request: Request, exc: ZeroDivisionError) -> Dict[str, Any]:
    """Обработчик ZeroDivisionError с структурированным логом."""
    tb_lines = traceback.format_exception(type(exc), exc, exc.__traceback__)
    # Берём только последний фрейм
    last_frame = tb_lines[-1] if tb_lines else str(exc)
    
    logger.error(
        "division_error",
        extra={
            "event": "error",
            "error_type": "ZeroDivisionError",
            "path": request.url.path,
            "traceback": last_frame.strip(),
        }
    )
    
    return {
        "error": "Division by zero",
        "traceback": last_frame.strip(),
    }

* Вставьте этот обработчик в FastAPI-приложение через app.exception_handler(ZeroDivisionError). LLM может сгенерировать и другие обработчики (TimeoutError, ValidationError) по аналогии.


5. Развертывание на Ray Serve: конфигурация автоскейлинга

Ray Serve позволяет настраивать pipeline-параллелизм и автоскейлинг на основе метрик. Вот рабочий конфиг для GPU-сервиса:

Файл serve.yaml

applications:
  - name: payment_service
    import_path: service.app:app  # FastAPI(app) в service/app.py
    runtime_env:
      working_dir: .
      pip:
        - -r requirements.txt
    deployments:
      - name: payment_deployment
        num_replicas: 2  # начальное число реплик
        max_concurrent_queries: 100
        ray_actor_options:
          num_gpus: 0.5  # каждая реплика использует 0.5 GPU
        autoscaling_config:
          target_utilization_fraction: 0.7  # скейлим при 70% загрузке GPU
          min_replicas: 1
          max_replicas: 10
          initial_replicas: 2
        health_check_period_s: 10
        health_check_timeout_s: 30

Запуск сервиса

# Запуск кластера Ray (если не запущен)
ray start --head --port=6379 --dashboard-host=0.0.0.0

# Деплой через Ray Serve CLI
serve deploy serve.yaml

# Проверка статуса
serve status

# Доступ через Ray Dashboard: http://localhost:8265

Важно: Ray Serve не поддерживает hot-reload для Python-файлов. При изменении кода делайте serve deploy serve.yaml --force — это перезапустит реплики без поранения запросов (если настроено graceful shutdown).


6. Агент самоисцеления: детекция и патчинг в runtime

Агент работает как фоновый Ray-актор, который каждые 30 секунд:

  1. Парсит логи из stdout/stderr Ray-реплик (или отправляет их в Loki/Prometheus).
  2. Детектирует аномалии: рост 5xx ошибок >5% за 5 минут или повторяющиеся traceback.
  3. Генерирует промпт для LLM с контекстом ошибки и исходным кодом.
  4. Применяет патч через Git-хук или прямо в файловую систему (если контейнер read-write).
  5. Перезапускает реплику для применения изменений.

Промпт для агента (агент/repair_prompt.txt)

Ты — senior инженер, специализирующийся на исправлении багов в Python-сервисах.

Контекст ошибки:
- Сервис: Self-Healing Payment Service
- Эндпоинт: POST /process
- Ошибка: ZeroDivisionError: division by zero
- Traceback (последний фрейм):
  File "service/app.py", line 42, in process_payment
    result = 100 / 0

Исходный код эндпоинта (фрагмент):
```python
@router.post("/process")
async def process_payment(request: PaymentRequest):
    if request.fail_with == "division":
        result = 100 / 0  # ← ОШИБКА ЗДЕСЬ
    else:
        result = request.amount * 0.9
    return {"transaction_id": uuid4(), "status": "completed"}
```

Задача:
1. Предложи минимальный патч, который:
   - Исправляет ошибку (деление на ноль).
   - Сохраняет логику искусственной ошибки для тестов (fail_with == "division").
   - Добавляет валидацию: если amount == 0, возвращай 400.
2. Верни только diff в формате unified:
   --- a/service/app.py
   +++ b/service/app.py
   @@ -40,7 +40,11 @@
     async def process_payment(request: PaymentRequest):
         if request.fail_with == "division":
   -        result = 100 / 0
   +        if request.amount == 0:
   +            raise HTTPException(status_code=400, detail="Amount cannot be zero")
   +        result = 100 / request.amount  # делим на amount, а не на 0
         else:
             result = request.amount * 0.9
         return {"transaction_id": uuid4(), "status": "completed"}

3. Если патч неясен, верни "UNCERTAIN: need more context".

Агент на Python (агент/self_healing_agent.py)

import asyncio
import httpx
import os
from ray import serve
from typing import List, Dict

@serve.deployment(
    num_replicas=1,
    ray_actor_options={"num_cpus": 0.5}
)
class SelfHealingAgent:
    def __init__(self):
        self.ollama_url = os.getenv("OLLAMA_URL", "http://localhost:11434")
        self.model = "llama3.1:70b"  # или mistral-large
        self.service_code_path = "/app/service"  # путь внутри контейнера
        
    async def detect_anomalies(self, logs: List[Dict]) -> List[Dict]:
        """Детектор аномалий на основе простых правил."""
        anomalies = []
        error_counter = {}
        
        for log in logs:
            if log.get("level") == "ERROR" and "ZeroDivisionError" in log.get("message", ""):
                key = f"{log['path']}:{log['error_type']}"
                error_counter[key] = error_counter.get(key, 0) + 1
                
        for key, count in error_counter.items():
            if count >= 5:  # более 5 одинаковых ошибок за интервал
                path, error_type = key.split(":")
                anomalies.append({
                    "path": path,
                    "error_type": error_type,
                    "count": count,
                    "sample_traceback": log["traceback"]
                })
        return anomalies
    
    async def generate_patch(self, anomaly: Dict) -> str:
        """Запрос к LLM для генерации патча."""
        async with httpx.AsyncClient() as client:
            # Читаем исходный код
            file_path = f"{self.service_code_path}/app.py"
            with open(file_path, "r") as f:
                source_code = f.read()
            
            prompt = f"""... (используем промпт из repair_prompt.txt, подставляя anomaly и source_code) ..."""
            
            response = await client.post(
                f"{self.ollama_url}/api/generate",
                json={
                    "model": self.model,
                    "prompt": prompt,
                    "stream": False
                },
                timeout=30.0
            )
            return response.json()["response"]
    
    async def apply_patch(self, patch: str, anomaly: Dict):
        """Применение патча через git или прямое редактирование."""
        # Вариант 1: через git (предпочтительно)
        #   git apply patch.diff && git commit -m "auto-fix"
        # Вариант 2: прямое редактирование (если контейнер read-write)
        #   with open(file_path, "w") as f: f.write(patched_code)
        
        # После применения перезапускаем реплику Ray Serve
        serve.get_app_handle("payment_service").options(
            stream=True
        ).deploy()  # или ray restart для контейнера
        
    async def run_cycle(self):
        """Основной цикл агента (вызывается по расписанию)."""
        logs = await self.collect_logs()  # реализация зависит от системы логов
        anomalies = await self.detect_anomalies(logs)
        
        for anomaly in anomalies:
            patch = await self.generate_patch(anomaly)
            if "UNCERTAIN" not in patch:
                await self.apply_patch(patch, anomaly)
                await self.log_action(f"Applied patch for {anomaly['path']}")
            else:
                await self.escalate_to_human(anomaly)  # отправка в Telegram/Slack

* Агент должен работать в том же кластере Ray, но в отдельном deployment. Настройте квоту ресурсов, чтобы он не мешал основному сервису.


7. A/B тестирование стратегий восстановления

Сравниваем две стратегии на production-трафике (5% трафика на эксперимент):

Стратегия Механизм MTTR (цель) Риски
Instant Rollback Откат на последний стабильный коммит (git revert) < 2 мин Потеря новых фич, возможен регресс
In-Place Fix Применение патча LLM без отката < 5 мин False-positive патчи могут усугубить проблему

Эксперимент через Prometheus + Grafana

# prometheus/rules.yml
groups:
  - name: self_healing
    rules:
      - alert: HighErrorRate
        expr: rate(http_requests_total{status=~"5.."}[5m]) / rate(http_requests_total[5m]) > 0.05
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "High error rate on {{ $labels.path }}"
          description: "Error rate > 5% for 2 minutes"

      - alert: SelfHealingAction
        expr: self_healing_patches_applied_total > 0
        for: 1m
        labels:
          strategy: "{{ $labels.strategy }}"  # rollback или fix
        annotations:
          summary: "Self-healing action taken: {{ $labels.strategy }}"

В Grafana создайте дашборд с метриками:

  • MTTR: среднее время от детекции до восстановления (по стратегиям).
  • Success Rate After Fix: % успешных запросов в течение 10 минут после патча.
  • False Positive Rate: % патчей, которые привели к новым ошибкам.

8. Production-ready чек-лист

Перед запуском в production проверьте:

Код и конфигурация

  • OpenAPI-спецификация валидна (openapi-spec-validator)
  • Все зависимости зафиксированы в requirements.txt (через uv pip freeze)
  • Патчи агента проходят code review (или есть approve-flow для production)
  • Конфиг Ray Serve: лимиты CPU/GPU, автоскейлинг, health checks

Мониторинг и безопасность

  • Структурированные логи в JSON (для парсинга агентом)
  • Метрики Prometheus: error rate, latency, patches_applied
  • LLM-агент работает с read-only контейнером (или через PR)
  • Альтернатива: BentoML для изоляции модели и сервера

Типичные ошибки при внедрении

  • False-positive патчи: LLM применяет изменения, которые ломают другие эндпоинты. Решение: тестируйте патчи в staging-окружении перед применением.
  • Циклические исправления: Агент постоянно применяет один и тот же патч. Добавьте версионирование кода (commit hash) и проверку, менялся ли файл после последнего патча.
  • Ресурсоёмкость LLM: Запросы к Llama 3.1 70B требуют 2xGPU. Используйте кэширование промптов и ограничьте частоту детекции (раз в 30 сек).
  • Ray Serve memory leaks: При частых редеплоях реплик может расти потребление памяти. Настройте max_replicas_per_node и мониторьте ray_memory_usage_bytes.

Итоги: что пошло не так в 2025 и как исправить в 2026

Основные проблемы самоисцеляющихся систем в 2025 году:

  1. LLM-галлюцинации в патчах — 15% автоматических исправлений приводили к регрессам. Решение: few-shot промпты с примерами успешных патчей и валидация через unit-тесты (генерируемые тем же LLM).
  2. Задержка детекции — классические метрики (error rate) реагируют с задержкой. Решение: streaming-анализ логов (например, через Apache Flink) и детекция аномалий в реальном времени.
  3. Отсутствие rollback-стратегии — агент часто применял "интересные" но опасные патчи. Решение: A/B тестирование стратегий на 5% трафика с автоматическим выбором по метрике MTTR.

К 2026 году стек Ray Serve + OpenAPI + Агентский LLM становится стандартом для критичных сервисов. Ключевой принцип: агент не должен быть автократом. Каждый патч проходит через pipeline (детекция → генерация → тестирование → применение) с человеческим approve для стратегических сервисов.

Полный рабочий пример (включая Dockerfile для BentoML, unit-тесты и скрипты развёртывания) доступен в репозитории: github.com/example/self-healing-2026