Проблема (агент працював… доки ти його не задеплоїв)
Notebook‑агент: ок.
Деплой‑агент: тут живе біль:
- немає мережі, яку ти “припускав”
- OOMKill, бо хтось увімкнув “full trace logging”
- ретраї множаться і стають rate‑limit storm
- секрети запечені в image (не треба)
Контейнеризація — це не Dockerfile‑театр. Це момент, коли ти змушуєш агента бути сервісом.
Чому це ламається в проді
Агенти — незручні ворклоади:
- bursty трафік (spikes = spikes токенів)
- I/O (tools) і зависання на timeouts
- довгі хвости (p95 норм, p99 хаос)
Якщо контейнер не enforce’ить бюджети і timeouts, це зробить прод. Просто через 504, OOMKills і рахунок.
Діаграма: що ти реально деплоїш
Реальний код: entrypoint, який не соромно деплоїти (Python + JS)
Нудно, але правильно:
- конфіг з env
- бюджети/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, без секретів у image)
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"]
Реальний інцидент (з цифрами)
Ми задеплоїли сервіс з “debug logging” увімкненим за замовчуванням. Він логував повні tool results.
Імпакт за один день:
- памʼять росла → OOMKill
- ретраї множили навантаження
- ~12% failure rate
- онкол: ~3 години (бо логи величезні і все одно не допомагають)
Фікс:
- sampled logging + редакція (
/uk/observability-monitoring/agent-logging) - enforce’ити бюджети в runtime
- kill switch, щоб вимикати дорогі tools під час інциденту
Компроміси
- Жорсткі timeouts зменшують хвости і можуть знижувати якість.
- Більше логів = легше дебажити, важче по privacy/вартості.
- “Один контейнер на агента” — просто і дорого. “Спільний сервіс” — дешевше і важче.
Коли НЕ контейнеризувати
Якщо ти не оперуєш це як сервіс — не overbuild. Але якщо реальний юзер може це тригернути, ти вже оперуєш сервіс.
Copy‑paste чекліст
- [ ] Budgets з env + enforced у runtime
- [ ] Tool gateway: timeouts/retries/allowlists
- [ ] Health + readiness checks
- [ ] Secrets через платформу (не baked)
- [ ] Kill switch config
- [ ] Структуровані логи + sampling; PII‑редакція за замовчуванням
Safe default config (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
Implement in OnceOnly (опційно)
# 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)
Використовується в патернах
Пов’язані відмови
Пов’язані сторінки (3–6 лінків)
- Основи: Що робить агента production-ready
- Фейли: Cascading failures · Partial outage handling
- Governance: Kill switch · Budget controls
- Спостережуваність: Логи AI‑агентів
- Тести: Юніт‑тести для AI‑агентів