Mon homelab tourne sur Docker Swarm avec un pipeline de sécurité continu. Quand Prometheus déclenche une alerte — swap élevé, service crashé, disk plein — un composant que j’appelle l’alert-agent reçoit le webhook, enrichit le contexte avec des infos Docker, consulte un LLM pour décider quoi faire, et envoie le résultat sur Telegram.
Jusqu’ici, ce LLM c’était Ollama avec Mistral:latest, tournant en local sur meta-68.
Le problème : ça ne fonctionnait plus vraiment.
Le Problème avec Ollama
Ollama était là depuis le début, mais au fil des mois, meta-68 est devenu de plus en plus chargé : MSF teamserver (6 GB RAM), ZAP qui scanne en continu, 15 replicas bm12, 3 replicas uzi… Le modèle Mistral avait du mal à répondre dans les temps.
En mesurant directement :
43 secondes. Et même là, Mistral ignorait le format JSON demandé et répondait en texte libre.
Avec un timeout à 30s dans le code, chaque alerte finissait en notify_only par défaut, sans décision LLM réelle. Le fallback prenait le relais — ce qui était mieux que rien, mais l’agent ne servait plus à grand chose.
L’autre Problème : la Sévérité Figée
Toutes mes alertes arrivaient comme ⚠️ Severity: warning. Pourquoi ? Parce que la sévérité affichée dans Telegram venait directement du label Prometheus, qui est défini statiquement dans les règles d’alerting.
HighSeverityFindingsFound → warning. NodeHighSwapUsage → warning. DiskAlmostFull → warning.
Tout est warning. C’est inutile — un swap à 85% sur le manager n’a pas le même impact qu’un disk à 99% sur le worker qui héberge la base MSF.
La Solution : Claude API avec Évaluation Dynamique
J’ai intégré Claude Haiku (Anthropic) comme backend LLM, avec deux changements clés.
1. Architecture multi-backend
Le backend LLM est maintenant configurable via une variable d’environnement LLM_BACKEND :
async def _call_claude(messages: list) -> dict:
client = anthropic.AsyncAnthropic(api_key=settings.claude_api_key)
system = next((m["content"] for m in messages if m["role"] == "system"), "")
user_messages = [m for m in messages if m["role"] != "system"]
response = await client.messages.create(
model=settings.claude_model,
max_tokens=256,
system=system,
messages=user_messages,
)
return _extract_json(response.content[0].text)
LLM_BACKEND=claude active Claude. LLM_BACKEND=ollama revient à Mistral local. LLM_BACKEND=kimi appellerait Kimi K3 via Moonshot AI. Pas de rebuild nécessaire pour switcher.
2. Sévérité évaluée par le LLM
J’ai modifié le system prompt pour demander au LLM d’évaluer la sévérité réelle de l’alerte, indépendamment du label Prometheus :
Le JSON de réponse inclut maintenant un champ severity que le LLM choisit lui-même. Ce champ remplace le label Prometheus dans le message Telegram.
3. Emojis adaptatifs
severity_emoji = {
"critical": "🔴",
"high": "🟠",
"medium": "🟡",
"low": "🔵",
"info": "⚪",
}.get(severity or "", "⚠️")
Résultat
Avant :
Après :
Latence : 2.4 secondes au lieu de 40+. La sévérité est maintenant contextuelle — le même NodeHighSwapUsage peut être medium à 60% et high à 85%.
Ce que j’ai Gardé d’Ollama
Le stack 51-service-ollama.yml est conservé avec replicas: 0. Si un jour Claude API est indisponible ou trop coûteux, un docker service scale ollama_ollama=1 + LLM_BACKEND=ollama suffit à revenir en arrière.
C’est l’avantage de l’architecture multi-backend : pas de couplage fort avec un provider.
Leçon
Un LLM local gratuit peut sembler idéal pour ce genre d’usage. Mais quand le node est déjà à 90% de sa capacité RAM/CPU, l’inference locale devient le goulot d’étranglement. À 0.25$ / million de tokens d’input pour Haiku, et avec des alertes qui se déclenchent quelques dizaines de fois par jour au max, le coût mensuel sera probablement inférieur à 1$.
Parfois, l’API cloud est la solution pragmatique.