1. Ce qu'est un agent en production — définissons honnêtement
Dans la presse, « agent » désigne tout, donc rien. En production, un agent IA est une boucle déterministe : un prompt système qui définit le rôle et les outils, une étape de raisonnement où le modèle décide quoi faire, une exécution d'outil (HTTP, code, SQL, navigateur, script maison), puis retour au modèle avec le résultat, en boucle jusqu'à complétion ou épuisement du budget. Tout le reste — mémoire, planification multi-étapes, sous-agents — est de la composition de cette boucle. Le démon n'est pas magique ; c'est un processus qui bloque sur un appel de modèle, l'exécute, et recommence.
Cette constatation change tout pour l'infrastructure : si un agent est un processus qui consomme des appels LLM et du temps CPU, alors ses besoins se déduisent. Il lui faut une machine toujours allumée, un état persistant entre les redémarrages, un moyen de relancer le processus quand il tombe, et un compte qui borne ses dépenses. Ce sont exactement les quatre points des sections suivantes.
2. L'architecture de référence : file + worker + modèle
Le montage que nous voyons tenir la charge réelle, du prototype solo au parc d'une dizaine de workers, tient en trois briques reliées :
- La file — un Redis ou une table Postgres qui retient les tâches à traiter. C'est elle qui absorbe les pics : si votre déclencheur produit 2 000 tâches à 3 h du matin, la file les garde et les distribue à votre rythme. Sans file, chaque tâche lance un processus — et la machine meurt de ses propres enfants.
- Le worker — un processus unique par machine (ou par quelques cœurs) qui pioche dans la file, exécute la boucle agent et rend le résultat. Écrivez-le à l'état pur : s'il crashe au milieu d'une tâche, la file la rend disponible et le worker suivant la reprend. L'idempotence n'est pas un luxe, c'est ce qui vous évite les traitements en double après chaque redémarrage.
- Le modèle — soit une API externe (OpenAI, Anthropic, Mistral…) appelée par HTTPS, soit votre propre serveur local vLLM/Ollama sur une GPU-1 ou GPU-2. Le worker n'a pas à savoir lequel : il parle le protocole OpenAI dans les deux cas, et vous pouvez basculer d'un fournisseur à l'autre en changeant deux variables d'environnement.
La règle d'or : le worker ne doit jamais être un cron qui lance des scripts, mais un démon qui consomme une file. Le cron lance ; le démon vit. C'est la différence entre une démo qui marche une semaine et une infrastructure qui marche un an.
3. Pourquoi une machine dédiée — et pourquoi offshore
Commençons par la partie qui n'a rien d'idéologique : votre PC n'est pas fait pour ça. Un agent 24/7 suppose une IP stable (les fournisseurs d'API bannissent les plages résidentielles dynamiques à la première vague d'erreurs), une veille qui ne coupe pas les processus, un disque qui tient les journaux, et une consommation électrique qui ne s'additionne pas à votre facture de salon. Un VPS-2 Reef à 11,99 $/mois remplit tout cela avec de la marge ; un laptop ouvert en permanence ne coche aucune de ces cases.
L'argument offshore est plus subtil et mérite d'être dit sans langue de bois. Quand vos agents consomment des API tierces avec des clés liées à un compte nominatif, chaque requête porte votre identité fournisseur — et les rate limits, suspensions et revues de modération s'appliquent à votre compte. Faire transiter vos appels depuis une infrastructure que vous louez sans lien d'identité ne rend pas anonyme la clé API elle-même (le fournisseur la voit toujours), mais découple votre infrastructure d'exécution — IPs, journaux d'accès, données de contexte — de votre identité civile. Pour les charges sensibles (données clients, veille concurrentielle, automatismes internes), c'est une séparation réelle, pas cosmétique.
Le cas limpide est celui du modèle local : déployez vLLM sur une GPU-1 (voir notre guide d'inférence self-hosted) et vos agents n'appellent plus aucun tiers — ni pour le raisonnement, ni pour les embeddings. La boucle complète vit dans votre serveur, sous votre UFW, avec vos règles de rétention. C'est la configuration maximale de contrôle que nous fournissons.
4. Choisir la machine selon la charge
La question « quel serveur pour mes agents » a une réponse honnête : cela dépend de où vit le modèle.
| Cas d'usage | Plan conseillé | Pourquoi |
|---|---|---|
| Orchestrateur + API externes (≤ 5 workers) | VPS-2 Reef (2 vCPU / 4 Go) | La charge est I/O ; 4 Go suffisent pour Redis, le worker et la supervision |
| Orchestrateur lourd + Postgres + monitoring (10–30 workers) | VPS-3 Horizon (4 vCPU / 8 Go) | Marges pour la base, Prometheus, les journaux et les pics de file |
| Modèle local 8–32 B pour la boucle agent | GPU-1 RTX 4090 (24 Go) | Un 8B en Q8 tient 60+ tok/s par flux — confort pour des boucles courtes |
| Modèle local 70 B ou plusieurs modèles servis | GPU-2 / GPU-3 | 48–80 Go de VRAM, parallélisme tensoriel vLLM sur la GPU-2 |
Le montage fréquent est hybride : un VPS-2 ou VPS-3 fait tourner la file, les workers et la supervision, et appelle un GPU-1 soit sur le même serveur (si vous êtes sur la ligne GPU), soit sur une seconde instance dédiée au modèle. Séparer l'orchestration de l'inférence vous permet de redémarrer, mettre à jour ou remplacer le modèle sans toucher aux tâches en cours — et l'inverse.
5. Le worker : code minimal et unit systemd
Le worker est délibérément court : boucle infinie, brpop bloquant sur la file, exécution bornée, accusé de réception. Python avec redis-py et le client OpenAI — mais n'importe quel langage fait l'affaire si le schéma est respecté.
#!/usr/bin/env python3 — le squelette à faire vôtre, pas à copier aveuglément
import os, redis
from openai import OpenAI
r = redis.Redis(host="127.0.0.1", port=6379, decode_responses=True)
llm = OpenAI(base_url=os.environ["LLM_BASE_URL"],
api_key=os.environ["LLM_API_KEY"])
MAX_STEPS = int(os.environ.get("AGENT_MAX_STEPS", "12"))
while True:
# BRPOP bloque jusqu'à une tâche — aucun polling, aucune charge à vide
_, payload = r.brpop(["tasks"], timeout=0)
task = Task.from_json(payload)
history = [{"role": "system", "content": SYSTEM_PROMPT}]
try:
for step in range(MAX_STEPS):
resp = llm.chat.completions.create(
model=os.environ["LLM_MODEL"],
messages=history + task.messages(),
max_tokens=2048,
)
msg = resp.choices[0].message.content
history.append({"role": "assistant", "content": msg})
call = parse_tool_call(msg) # votre grammaire d'outils
if call is None:
break
history.append({"role": "user", "content": run_tool(call)})
task.complete(history[-1]["content"])
except Exception as exc:
task.fail(repr(exc)) # la file gère la reprise
Ce qui rend ce squelette industrialisable tient à quatre choix : la tâche est prise depuis la file puis acquittée (pas l'inverse — sinon une panne entre les deux la perd), les étapes sont bornées (AGENT_MAX_STEPS), le budget est borné (max_tokens à chaque appel), et toute exception rend la tâche à la file plutôt que de la détruire. Ajoutez un timeout sur run_tool — un outil HTTP sans timeout est la source n°1 de workers gelés.
Le service systemd qui ne lâche jamais
Le démon lui-même est trivial à écrire, mais trois directives en font un citoyen de production : Restart=always pour survivre aux OOM et aux plantages de dépendance, MemoryMax pour qu'un worker fuyard ne prenne pas la machine en otage, et EnvironmentFile pour sortir les secrets du code.
[Unit]
Description=Agent IA — worker de file de tâches
After=network-online.target redis-server.service
Wants=network-online.target
[Service]
Type=simple
User=agent
Group=agent
WorkingDirectory=/opt/agent
EnvironmentFile=/opt/agent/.env
ExecStart=/opt/agent/venv/bin/python worker.py
Restart=always
RestartSec=5
MemoryMax=3G
NoNewPrivileges=true
ProtectSystem=strict
ReadWritePaths=/opt/agent/data
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
useradd -r -d /opt/agent agent
chown -R agent:agent /opt/agent
systemctl daemon-reload
systemctl enable --now agent-worker
# Journal en direct : ce que l'agent décide, tape et casse
journalctl -u agent-worker -f
6. Borner le budget : le garde-fou qui compte
L'accident classique de l'agent 24/7 n'est pas un plantage, c'est une boucle. Un prompt qui échoue, un outil qui répond une erreur, le modèle qui réessaie — et à 3 h du matin votre worker a consommé la facture mensuelle d'une API en quatre heures. Trois garde-fous, dans cet ordre d'efficacité :
- Bornes dans la boucle — étapes maximum, tokens maximum par appel, tokens maximum par tâche, calculés et journalisés. La borne qui n'est pas comptée n'existe pas.
- Plafond côté fournisseur d'API — configurez un spending limit sur le compte de l'API externe quand l'option existe, et une clé dédiée à l'agent (jamais votre clé personnelle) : une révocation ne coûte alors rien.
- Circuit-breaker — si le taux d'échec d'outils dépasse un seuil sur N minutes, le worker se met en pause et vous alerte. Un agent qui échoue à 90 % ne répare pas ses outils en tapant plus fort ; il vous attend.
Version locale, le calcul s'inverse agréablement : un GPU-1 à 119 $/mois coûte 3,9 $/jour quel que soit le nombre d'appels. Vos boucles infinies deviennent un problème de latence, pas de facture — mais surveillez quand même la file : une file qui grandit indéfiniment signifie un modèle devenu trop lent pour la charge, et c'est un problème de dimensionnement, pas de garde-fou.
7. Supervision : voir l'invisible pendant que vous dormez
Un agent ne vous parle pas quand ça va mal ; il se tait. La supervision doit donc mesurer trois signaux : le worker vit-il (heartbeat), la file avance-t-elle (longueur, âge de la tête de file), le modèle répond-il (latence, taux d'erreur). Trois sondes, trois alertes, et vous avez couvert 95 % des pannes réelles.
#!/usr/bin/env bash
# Échoue (exit 1) si le worker est mort, la file bloquée ou le modèle muet
systemctl is-active --quiet agent-worker || exit 1
LLEN=$(redis-cli llen tasks)
[ "$LLEN" -gt 500 ] && exit 1 # file en explosion : consommateur en panne
curl -sf -m 10 "$LLM_BASE_URL/models" -H "Authorization: Bearer $LLM_API_KEY" || exit 1
Poussez le résultat vers un service d'uptime externe (un simple curl vers une URL healthchecks.io fonctionne bien et reste gratuit à petit volume) ou vers votre Prometheus si la stack existe déjà. Sur la ligne GPU, ajoutez nvidia-smi --query-gpu=utilization.gpu,memory.used,temperature.gpu --format=csv,noheader à la sonde : un GPU à 0 % à 14 h signifie que vos agents dorment ou que le worker est mort, et seul le journal tranchera. Enfin, archivez le journal : journalctl -u agent-worker est votre boîte noire quand une décision d'agent devra être expliquée — rétention par défaut d'un mois, ajustez selon vos obligations.