1. Навіщо агенту «продакшн-мислення»
Коли ви пишете звичайний бекенд, сама думка про «вихід у продакшн» автоматично вмикає режим параної: авторизація, логування, обробка помилок, ліміти, секрети в .env, а не в коді.
З агентом варто вмикати той самий режим — тільки ще жорсткіший. Причина проста: звичайний бекенд виконує рівно те, що ви написали, а агент — те, що модель сама вирішила зробити в межах наданих їй інструментів та інструкцій. Ілюзія контролю тут сильніша, ніж у класичному коді: здається, що промпт усе описує. Насправді ж ви контролюєте лише оточення й доступні дії, а не всі «думки» моделі.
Тож у цій лекції ми поступово «обкладемо» нашого агента шарами захисту:
- спочатку обмежимо, що саме він може робити (права інструментів і розділення агентів),
- потім ізолюємо середовище виконання (sandbox і ліміти),
- наведемо лад із секретами та PII,
- і насамкінець увімкнемо спостережуваність: логи, метрики та базовий трейсинг.
Щоб було наочніше, продовжимо історію про наш GiftGenius: агента, який допомагає підібрати подарунок і трохи занурюється у світ електронної комерції (через замовлення й checkout, але поки без подробиць ACP — це буде пізніше).
2. Права: агенту не потрібні «всі кнопки світу»
Принцип найменших привілеїв (Least Privilege)
Перше правило: агентові не потрібно вміти все. Що більше в нього інструментів, то вищий шанс, що він викличе «не ту» функцію «не в той» момент. Замість одного монструозного manageEverything(), який читає й записує будь-що, ми проєктуємо дрібні та чіткі функції. Як мінімум — розділяємо читання і запис.
Для GiftGenius це особливо очевидно: одне діло — читати список подарунків і вподобання користувача, інше — створювати чи підтверджувати замовлення (а там уже гроші). Тому зазвичай роблять так:
- набір безпечних інструментів лише для читання (пошук подарунків, перегляд деталей),
- окремі інструменти для запису (створення чернетки замовлення, скасування замовлення),
- і, за потреби, ще один рівень для особливо ризикових операцій (підтвердження платежу, масові зміни).
Різні агенти для різних задач
Ще один потужний прийом — розділяти агентів за зонами відповідальності. Один агент — «підбір подарунків», інший — «керування замовленнями». Тоді навіть якщо модель у gift-агенті раптом «зірветься з котушок», вона фізично не зможе викликати платіжний інструмент, бо його просто немає в її конфігурації.
Уявімо мінімалістичний тип конфігурації агента й інструментів:
// Спрощені типи для пояснення ідей
type ToolName = 'suggest_gifts' | 'get_gift_details' |
'create_order_draft' | 'confirm_order';
type AgentConfig = {
id: string;
allowedTools: ToolName[];
maxSteps: number;
};
Тепер опишемо двох агентів GiftGenius:
export const giftPlannerAgent: AgentConfig = {
id: 'gift-planner',
allowedTools: ['suggest_gifts', 'get_gift_details'],
maxSteps: 6,
};
export const orderAgent: AgentConfig = {
id: 'order-manager',
allowedTools: ['create_order_draft', 'confirm_order'],
maxSteps: 4,
};
Так, це поки що абстракція, але суть проста: навіть якщо в коді є всі чотири інструменти, конкретний агент отримує лише потрібну підмножину.
Привʼязка прав до користувача та ролей
Важливо памʼятати, що в нас є дві різні сутності:
- користувач і його права (чи може цей user_id взагалі щось купувати, скасовувати, бачити історію),
- агент і його дозволені інструменти.
В ідеалі кожен виклик інструмента має проходити обидві перевірки: «агентові це дозволено?» і «користувачеві це теж дозволено?».
Умовно:
type UserRole = 'guest' | 'customer' | 'admin';
function canUserCallTool(role: UserRole, tool: ToolName): boolean {
if (tool === 'confirm_order') {
return role === 'customer' || role === 'admin';
}
if (tool === 'create_order_draft') {
return role !== 'guest';
}
return true; // читання дозволяємо всім
}
На стороні MCP/бекенду під час обробки tool-виклику можна робити подвійну перевірку:
function assertToolAllowed(
agent: AgentConfig,
userRole: UserRole,
tool: ToolName,
) {
if (!agent.allowedTools.includes(tool)) {
throw new Error(`Інструмент ${tool} заборонено для агента ${agent.id}`);
}
if (!canUserCallTool(userRole, tool)) {
throw new Error(`Користувачу з роллю ${userRole} не можна викликати ${tool}`);
}
}
У результаті навіть якщо модель раптом вирішить викликати confirm_order з неправильного агента або від імені гостя, виклик упреться в цю перевірку й перетвориться на керовану помилку, а не на незапланований платіж.
Різні конфігурації для різних оточень
У dev і staging-оточеннях ви часто хочете дати агенту більше свободи: тестові інструменти, фейкові платіжні сервіси, експериментальні функції. У production, навпаки, конфігурація максимально жорстка: частину tools вимкнено, ендпоїнти — тільки бойові, токени — лише реальні.
Найпростіша схема:
type Env = 'dev' | 'staging' | 'production';
const env = (process.env.APP_ENV as Env) ?? 'dev';
const orderAgentByEnv: Record<Env, AgentConfig> = {
dev: {
id: 'order-manager-dev',
allowedTools: ['create_order_draft', 'confirm_order'],
maxSteps: 8,
},
staging: {
id: 'order-manager-staging',
allowedTools: ['create_order_draft', 'confirm_order'],
maxSteps: 6,
},
production: {
id: 'order-manager-prod',
allowedTools: ['create_order_draft'], // confirm лише через окремий шлях
maxSteps: 4,
},
};
export const currentOrderAgent = orderAgentByEnv[env];
У продакшні confirm_order може бути взагалі винесений в окремий «небезпечний» агент. Його викликають лише після явного кліку «Підтвердити замовлення» у віджеті та додаткових перевірок.
3. Sandbox: агенту не потрібен root-доступ до вашого всесвіту
Рівні ізоляції
Після того як ми задали права для агентів і користувачів, переходимо до наступного рівня захисту — sandbox та ізоляції середовища виконання.
Sandbox для агента та його інструментів умовно можна розбити на кілька рівнів:
- Рівень коду інструментів. Ми обмежуємо доступ до файлової системи, мережі та ресурсів процесу: не даємо записувати куди завгодно, ходити в довільні домени, безкінечно навантажувати CPU чи «зʼїдати» гігабайти памʼяті.
- Рівень Agents SDK. Ми задаємо ліміти за кроками циклу запуску, за кількістю викликів інструментів і за розміром контексту (token limit). Модель не може нескінченно «думати» й плодити виклики інструментів: у певний момент запуск завершиться з помилкою «ліміт кроків» або «ліміт часу».
Усе це складається в класичну «оборонну архітектуру», яку зручно подати схемою.
graph TD
A[Промпт / system‑інструкції] --> B[JSON Schema інструментів]
B --> C[Дозволи агента та користувача]
C --> D[Sandbox інфраструктури]
D --> E[Зовнішні сервіси / БД]
subgraph Агент
A
B
C
end
subgraph Інфраструктура
D
end
Промпт — найслабший захист. Справжня сила починається там, де ви фізично обмежуєте, що може зробити ваш код і які API доступні.
Ліміти на цикл запуску: кроки, час, виклики інструментів
Частину sandbox можна виразити прямо в конфігурації агента: максимальну кількість кроків, загальний час виконання, ліміт викликів інструментів. Це не лише захист від runaway-циклів, а й контроль витрат.
Приклад абстрактної конфігурації run-опцій:
type RunLimits = {
maxSteps: number;
maxToolCalls: number;
timeoutMs: number;
};
const defaultLimits: RunLimits = {
maxSteps: 8,
maxToolCalls: 10,
timeoutMs: 30_000,
};
Такі ліміти ви потім передаєте в обгортку, яка запускає агента. Якщо раптом модель вирішила 11-й раз викликати інструмент, ви перериваєте запуск і чесно кажете користувачеві, що завдання надто складне. Це краще, ніж дозволити агентові безконтрольно «спалювати» бюджет.
Ізоляція коду та мережі
На рівні контейнера/процесу зазвичай роблять так:
Код MCP-сервера та/або агентного сервісу запускається в контейнері з файловою системою лише для читання (крім спеціально відведеного робочого каталогу) та обмеженими ресурсами (CPU, RAM). Мережу налаштовують за allow-list: можна ходити лише до потрібних зовнішніх сервісів (ваш commerce-backend, платіжка, кілька зовнішніх API), а не в довільний інтернет.
Для агентних сценаріїв це особливо критично: модель може спробувати піти в якийсь небажаний API або прочитати неочікувані файли. Добре, якщо навіть за таких спроб вона фізично не матиме прав дістатися до зайвих ресурсів.
У коді це зазвичай виглядає не як «магічний рядок TypeScript», а як налаштування вашого оркестратора (Docker Compose, Kubernetes, Vercel, Fly.io тощо). Але в перспективі корисно думати про це ще на етапі проєктування:
- інструмент, який запускає сторонній код (наприклад, генерацію звіту із shell-командами), має працювати в окремому, жорстко ізольованому середовищі;
- інструменти не повинні мати можливості читати чужі файли, секрети, конфіги;
- мережевий доступ краще явно обмежувати доменами або IP.
4. Секрети та конфіденційні дані: що агентові знати не потрібно
Де секретам «жити», а де — ні
Базове правило: жодні секрети — API-ключі, паролі, access-токени — не повинні потрапляти в промпт моделі, у віджет, у логи та в репозиторій. Вони «живуть»:
- у змінних оточення (process.env.SOMETHING),
- у менеджері секретів (AWS Secrets Manager, GCP Secret Manager, Vault тощо),
- в окремих зашифрованих сторах, доступ до яких суворо контролюється.
У нашому GiftGenius, наприклад, є ключ до commerce-API магазину. Нам потрібно, щоб агент міг створювати чернетку замовлення через MCP-інструмент, але сам ключ модель бачити не повинна.
// mcp/tools/createOrderDraft.ts
const COMMERCE_API_KEY = process.env.COMMERCE_API_KEY!;
export async function createOrderDraft(args: {
userId: string;
giftId: string;
quantity: number;
}) {
// Модель ніколи не побачить COMMERCE_API_KEY — він тільки тут, на сервері
const res = await fetch(`${process.env.COMMERCE_API_URL}/orders/draft`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${COMMERCE_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(args),
});
if (!res.ok) {
throw new Error(`Commerce API returned ${res.status}`);
}
return res.json(); // У відповіді агенту віддамо вже безпечний об’єкт
}
Важливо: у відповіді інструмента ви не повинні «протягувати» ключі або інші чутливі деталі. Агентові достатньо знати draftOrderId, список позицій і, за потреби, статус.
PII і мінімізація даних у контексті
Окрім секретів, є ще категорія PII (персональні дані користувачів): імена, телефони, адреси доставки, email тощо. Агентові часто не потрібен увесь цей «сирий» текст. Достатньо структурованого профілю: «любить настільні ігри», «вік 30–35», «приблизний бюджет 50–70 $».
Замість того щоб «кидати» в промпт повну історію замовлень користувача, можна зробити tool get_user_profile_summary, який поверне вже агрегований і знеособлений профіль.
type ProfileSummary = {
ageRange: '18-25' | '26-35' | '36-50' | '50+';
interests: string[];
preferredBudget: { min: number; max: number };
};
export async function getUserProfileSummary(userId: string): Promise<ProfileSummary> {
// Тут ви лізете в БД, але назовні віддаєте лише агреговану інформацію
return {
ageRange: '26-35',
interests: ['настільні ігри', 'гаджети'],
preferredBudget: { min: 30, max: 80 },
};
}
Модель бачить рівно стільки, скільки потрібно для підбору подарунка, — і не більше.
Очищення логів (scrubbing)
Логи — природне місце, де випадково «спливають» секрети та PII. Особливо якщо написати «зручний» логер на кшталт console.log(...) і друкувати все підряд.
Хороший підхід — мати центральний логер, який перед записом проходить по корисному навантаженню та маскує чутливі поля.
type LogPayload = Record<string, unknown>;
const SENSITIVE_KEYS = ['email', 'phone', 'cardNumber', 'token'];
function scrub(payload: LogPayload): LogPayload {
const result: LogPayload = {};
for (const [key, value] of Object.entries(payload)) {
if (SENSITIVE_KEYS.includes(key)) {
result[key] = '***redacted***';
} else {
result[key] = value;
}
}
return result;
}
export function logEvent(event: string, payload: LogPayload) {
const safe = scrub(payload);
console.log(JSON.stringify({ event, ...safe }));
}
Тепер замість того, щоб колись ловити production-інцидент «ми вже пів року логували телефони й токени клієнтів», ви одразу будуєте систему так, щоб це було просто неможливо. Це не лише питання акуратності, а й майбутніх вимог комплаєнсу (GDPR і місцеві закони): що менше PII в логах, то простіше жити продукту.
5. Моніторинг і спостережуваність агента
Що саме потрібно бачити
Ми обмежили, що агент може робити, які дані він бачить і що потрапляє в логи. Наступне питання — як зрозуміти, що в усьому цьому «звіринці» агент у продакшні поводиться саме так, як ми задумали?
Звичайний моніторинг «сервіс живий / не живий» для агента майже не дає користі. Важливо не лише знати, що процес працює, а й розуміти його поведінку: які кроки він робить, які інструменти викликає, де помиляється та де зациклюється.
Мінімальний набір даних по кожному запуску:
- agent_run_id — унікальний ідентифікатор запуску;
- анонімний user_id або сесійний ID;
- імʼя агента та оточення;
- список викликаних tools: імʼя, кількість, сумарний час;
- кроки workflow і те, на якому кроці ми зупинилися;
- підсумковий статус: success, partial_success, failed, canceled, timeout, limits_exceeded.
Можна оформити це як структуру:
type RunStatus =
| 'success'
| 'partial_success'
| 'failed'
| 'canceled'
| 'timeout'
| 'limits_exceeded';
type ToolCallLog = {
name: ToolName;
durationMs: number;
success: boolean;
};
type AgentRunLog = {
runId: string;
agentId: string;
userId: string;
env: Env;
startedAt: string;
finishedAt: string;
status: RunStatus;
toolCalls: ToolCallLog[];
errorMessage?: string;
};
Приклад «обгортки» навколо запуску агента
Припустімо, у вас є функція runAgent, яка інкапсулює реальний виклик Agents SDK. Обгорнемо її моніторингом:
async function runAgentWithLogging(
agent: AgentConfig,
input: string,
userId: string,
): Promise<string> {
const runId = crypto.randomUUID();
const startedAt = new Date();
const toolCalls: ToolCallLog[] = [];
try {
const result = await runAgent(agent, input, {
userId,
limits: defaultLimits,
onToolCall: (name, durationMs, success) => {
toolCalls.push({ name, durationMs, success });
},
});
const finishedAt = new Date();
const log: AgentRunLog = {
runId,
agentId: agent.id,
userId,
env,
startedAt: startedAt.toISOString(),
finishedAt: finishedAt.toISOString(),
status: 'success',
toolCalls,
};
logEvent('agent_run', log);
return result;
} catch (err) {
const finishedAt = new Date();
const log: AgentRunLog = {
runId,
agentId: agent.id,
userId,
env,
startedAt: startedAt.toISOString(),
finishedAt: finishedAt.toISOString(),
status: 'failed',
toolCalls,
errorMessage: (err as Error).message,
};
logEvent('agent_run', log);
throw err;
}
}
Тут runAgent — це «чорна скринька», яка може бути реалізована через реальний Agents SDK. Ми ж показуємо, як додати спостережуваність, не привʼязуючись до конкретного API.
Логи vs метрики vs трейсинг
Зручно розрізняти три рівні спостережуваності:
| Рівень | Що це | Приклад для агента GiftGenius |
|---|---|---|
| Логи | «Історії» про конкретні запуски | Детальний AgentRunLog з кроками й інструментами |
| Метрики | Агреговані числові показники | p95 тривалість запуску, середня кількість викликів інструментів, частка помилок |
| Трейсинг | Дерево / граф запитів і підзапитів | Запуск → кроки → виклики інструментів → виклики зовнішніх API (commerce, БД тощо) |
Метрики потрібні, щоб зрозуміти «чи загалом усе добре?» (наприклад, частка помилок за останню годину). Логи й трейсинг — щоб розібратися, «чому погано саме тут?» і відтворити конкретний проблемний запуск.
Приклад «зачатку» метрик можна реалізувати поверх логів: періодичне завдання агрегує події agent_run і рахує p95 тривалості, кількість помилок тощо.
6. Як це виглядає в GiftGenius цілком
Щоб усе не здавалося набором абстракцій, зберемо цілісну картинку для нашого навчального застосунку.
Агент gift-planner у production-оточенні має лише безпечні інструменти: підбір подарунків і отримання деталей. Він не бачить ані платежів, ані керування замовленнями. Його system-інструкції кажуть, що він не повинен обіцяти користувачеві «я все оплачу за вас». Максимум — підготувати рекомендації й, можливо, чернетку списку подарунків.
Агент order-manager існує окремо й уміє лише працювати із замовленнями. У продакшні він може створювати тільки чернетку замовлення (create_order_draft), а підтвердження замовлення (confirm_order) або виконується людиною через явний UI-триґер у віджеті, або доступне лише в dev/staging. Його інструменти використовують секрети (API-ключі магазину) виключно на backend-стороні, а у відповіді «проксіюють» тільки потрібні поля.
Обидва агенти запускаються через обгортку runAgentWithLogging, яка навішує ліміти й записує логи з agent_run_id, userId, оточенням і списком інструментів. У логах немає email і телефонів: ці поля заздалегідь вичищаються скрубером. Профіль користувача використовується у знеособленому вигляді: віковий діапазон, інтереси, бюджет — але не повний текст історії покупок.
Інфраструктура, у якій живуть MCP-сервер і агентний сервіс, ізольована: контейнери з файловою системою лише для читання (крім /tmp або спеціально виділеного каталогу), обмеження CPU/RAM, мережа за allow-list доменів. Якщо агент раптом спробує викликати «щось зайве», він просто не зможе дістатися до цього фізично.
Якщо в якийсь момент ви бачите сплеск метрики «частка запусків зі статусом limits_exceeded» або «середня кількість викликів інструментів > 10», це сигнал: або промпт став надмірно балакучим, або один з інструментів глючить і змушує агента перезапускати кроки.
Це вже поведінка дорослого сервісу, а не експериментального агента «якось воно буде».
7. Типові помилки під час виведення агентів у продакшн
Усе, що ми обговорювали вище, — це «правильна» картинка продакшн-агента. На практиці найчастіше трапляються типові граблі. Зберемо їх в один список: якщо уникнути хоча б цих помилок, запуск у продакшн пройде значно спокійніше.
Помилка № 1: агенту «дозволили все».
Поширений сценарій: ви описали купу MCP-інструментів (пошук, зміна, видалення, платежі), а під час створення агента просто «згодували» йому весь список. У результаті модель може випадково викликати видалення або платіж там, де ви хотіли лише читання. Це лікується розділенням інструментів за ролями та створенням кількох вужчих агентів — у кожного з яких свій allowedTools.
Помилка № 2: перевірка прав лише в промпті.
Іноді розробники пишуть у system-інструкціях: «ніколи нічого не купуй без підтвердження користувача» — і на цьому заспокоюються. Але промпт — слабкий захист, а jailbreak-и та звичайні помилки ніхто не скасовував. Потрібні реальні перевірки на backend-рівні: «агентові цей tool дозволено» і «користувачеві цей tool дозволено». Інакше одна неакуратна генерація може призвести до дій, на які ніхто не розраховував.
Помилка № 3: секрети в промптах і логах.
Іноді хочеться «прискорити інтеграцію» й просто покласти API-ключ у system-prompt або передати його в аргументах інструмента, щоб агент сам ходив на зовнішній API. У підсумку ключ опиняється і в логах моделі, і потенційно в сторонніх системах. Це прямий шлях до витоків і блокування в Store. Секрети мають жити тільки на серверній стороні — у змінних оточення або менеджері секретів — і ніколи не потрапляти в контекст моделі.
Помилка № 4: «сирі» логи без очищення.
Під час налагодження зручно писати console.log(...) і забути про це. За кілька місяців виявляється, що в логах лежать адреси користувачів, телефони, номери замовлень із PII. Особливо неприємно у світі GDPR та інших регуляцій. Краще одразу завести центральний логер і впровадити автоматичне маскування чутливих полів — навіть якщо здається, що «ми логували лише на dev».
Помилка № 5: відсутність лімітів на поведінку агента.
Без обмежень за кроками, часом і кількістю викликів інструментів агент може зациклитися: багато разів викликати один і той самий інструмент, намагатися нескінченно виправляти одну й ту саму помилку, витрачати купу токенів і навантажувати зовнішні API. У кращому випадку ви отримаєте гігантські рахунки за моделі, у гіршому — «покладете» бекенд і розсердите всіх користувачів. Ліміти на цикл запуску й sane defaults за тайм-аутами — обовʼязкова частина конфігурації.
Помилка № 6: змішування read- і write-операцій в одному інструменті.
Іноді створюють «зручні» методи на кшталт getOrCreateOrder, які за відсутності замовлення створюють нове. Для класичного бекенду це допустимий патерн, але у світі агентів він може призвести до неочікуваних побічних ефектів: модель хотіла просто дізнатися стан, а інструмент щось створив. Значно безпечніше розділяти get_order_details і create_order_draft — тоді навіть за повторних викликів наслідки більш керовані.
Помилка № 7: ігнорування спостережуваності.
Багато хто починає з «потім прикрутимо логи та метрики, зараз головне — щоб працювало». Агенти без моніторингу — це чорна скринька: ви не знаєте, які інструменти вони викликають, скільки кроків роблять, де помиляються. Будь-яка скарга користувача перетворюється на розслідування в темній кімнаті. Набагато простіше одразу закласти структуру логів (agent_run_id, tools, статус) і базові метрики, ніж потім намагатися добудувати це поверх хаотичного коду.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ