Problème (ça marchait… jusqu’au déploiement)
Agent dans un notebook : OK.
Agent déployé : là où ça fait mal :
- pas le réseau que tu pensais avoir
- OOMKill parce que quelqu’un a activé “full trace logging”
- storms de retries
- secrets dans l’image (non)
Containeriser, ce n’est pas du théâtre Dockerfile. C’est forcer l’agent à se comporter comme un vrai service.
Pourquoi ça casse en prod
Les agents sont des workloads bizarres :
- bursty (spikes de trafic = spikes de tokens)
- I/O (tools) + blocages sur timeouts
- longues queues (p95 ok, p99 chaos)
Si ton container n’enforce pas budgets/timeout, la prod le fera. Mais via 504s, OOMKills et factures.
Diagramme : ce que tu déploies vraiment
Code réel : entrypoint friendly container (Python + JS)
On garde ça boring :
- config via env
- budgets/timeout enforced
- health endpoint
import os
import time
from dataclasses import dataclass
from typing import Any, Dict
@dataclass(frozen=True)
class Budgets:
max_steps: int
max_tool_calls: int
max_seconds: int
def load_budgets() -> Budgets:
return Budgets(
max_steps=int(os.getenv("AGENT_MAX_STEPS", "25")),
max_tool_calls=int(os.getenv("AGENT_MAX_TOOL_CALLS", "12")),
max_seconds=int(os.getenv("AGENT_MAX_SECONDS", "60")),
)
def run_request(task: str, *, budgets: Budgets) -> Dict[str, Any]:
t0 = time.time()
steps = 0
tool_calls = 0
while True:
steps += 1
if steps > budgets.max_steps:
return {"output": "", "stop_reason": "max_steps"}
if tool_calls > budgets.max_tool_calls:
return {"output": "", "stop_reason": "max_tool_calls"}
if time.time() - t0 > budgets.max_seconds:
return {"output": "", "stop_reason": "max_seconds"}
return {"output": "ok", "stop_reason": "finish"}
def health() -> Dict[str, str]:
return {"ok": "true"}export function loadBudgets() {
return {
maxSteps: Number(process.env.AGENT_MAX_STEPS ?? 25),
maxToolCalls: Number(process.env.AGENT_MAX_TOOL_CALLS ?? 12),
maxSeconds: Number(process.env.AGENT_MAX_SECONDS ?? 60),
};
}
export function runRequest(task, { budgets }) {
const t0 = Date.now();
let steps = 0;
let toolCalls = 0;
while (true) {
steps += 1;
if (steps > budgets.maxSteps) return { output: "", stop_reason: "max_steps" };
if (toolCalls > budgets.maxToolCalls) return { output: "", stop_reason: "max_tool_calls" };
if ((Date.now() - t0) / 1000 > budgets.maxSeconds) return { output: "", stop_reason: "max_seconds" };
return { output: "ok", stop_reason: "finish" };
}
}
export function health() {
return { ok: true };
}Dockerfile (multi-stage, sans secrets)
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=deps /app/node_modules ./node_modules
COPY . .
EXPOSE 3000
CMD ["npm","run","start"]
Panne réelle (avec chiffres)
On a déployé avec “debug logging” activé par défaut. Résultats tools complets loggés à chaque appel.
Impact :
- mémoire monte → OOMKill
- retries amplifient la charge
- ~12% de failure rate
- astreinte : ~3 heures (logs énormes et pourtant pas utiles)
Fix :
- logging échantillonné + redaction (
/fr/observability-monitoring/agent-logging) - budgets enforced au runtime
- kill switch pour désactiver des tools coûteux en incident
Compromis
- Timeouts serrés réduisent la queue et peuvent réduire la qualité.
- Plus de logs aide le debug et fait mal en coût/privacy.
- Un container par agent = simple et cher. Mutualiser = moins cher et plus dur.
Quand NE PAS containeriser
Si tu n’opères pas ça comme un service, n’overbuild pas. Mais dès qu’un user peut déclencher le run, tu opères un service. Voilà.
Checklist copy-paste
- [ ] Budgets via env + enforced au runtime
- [ ] Tool gateway : timeouts/retries/allowlists
- [ ] Health + readiness checks
- [ ] Secrets via plateforme (pas baked)
- [ ] Kill switch config
- [ ] Logs structurés + sampling; redaction PII par défaut
Config safe par défaut (YAML)
runtime:
env:
AGENT_MAX_STEPS: 25
AGENT_MAX_TOOL_CALLS: 12
AGENT_MAX_SECONDS: 60
tools:
allowlist: ["search.read", "http.get"]
timeouts_ms: { default: 8000 }
retries: { max: 2, backoff_ms: [200, 800] }
observability:
sampled_tool_results: true
result_sample_rate: 0.01
rollout:
canary_percent: 10
rollback_on_error_rate: 0.05
Implémenter dans OnceOnly (optionnel)
# onceonly-python: tool allowlist + governed tool call
import os
from onceonly import OnceOnly
client = OnceOnly(
api_key=os.environ["ONCEONLY_API_KEY"],
timeout=5.0,
max_retries_429=2,
)
agent_id = "billing-agent"
client.gov.upsert_policy({
"agent_id": agent_id,
"allowed_tools": ["search.read", "http.get"],
"max_actions_per_hour": 200,
"max_spend_usd_per_day": 10.0,
})
res = client.ai.run_tool(
agent_id=agent_id,
tool="http.get",
args={"url": "https://example.com/health"},
spend_usd=0.001,
)
if not res.allowed:
raise RuntimeError(res.policy_reason)
FAQ (3–5)
Utilisé par les patterns
Pannes associées
Gouvernance requise
Pages liées (3–6 liens)
- Fondations: Un agent prêt pour la prod
- Pannes: Cascading failures · Partial outage handling
- Gouvernance: Kill switch · Budget controls
- Observabilité: Logging d’agents IA
- Tests: Unit tests pour agents IA