1. Зачем агенту «продакшен‑мышление»
Когда вы пишете обычный backend, сама идея «выйти в прод» автоматически включает режим паранойи: авторизация, логирование, обработка ошибок, лимиты, секреты в .env, а не в коде.
С агентом нужно включить тот же режим, только ещё более жёсткий. Причина простая: обычный backend выполняет ровно то, что вы написали, а агент — то, что модель сама решила сделать в рамках заданных ей инструментов и инструкций. Иллюзия контроля здесь сильнее, чем в классическом коде: кажется, что промпт всё описывает, но на самом деле вы контролируете только окружение и доступные действия, а не все мысли модели.
Поэтому в этой лекции мы будем постепенно обкладывать нашего агента «слоями защиты»:
- сначала ограничим, что именно он может делать (права инструментов и разделение агентов),
- потом изолируем среду выполнения (sandbox и лимиты),
- наведём порядок с секретами и PII,
- и наконец, включим наблюдаемость: логи, метрики и базовый трейсинг.
Чтобы было предметно, будем продолжать историю с нашим GiftGenius: агент, который помогает подобрать подарок и чуть-чуть залезает в commerce‑мир (через заказ и checkout, но пока без подробностей ACP — это будет позже).
2. Права: агенту не нужны «все кнопки мира»
Принцип наименьших привилегий (Least Privilege)
Первое правило: агенту не нужно уметь всё. Чем больше у него инструментов, тем выше шанс, что он вызовет «не ту» функцию в «не тот» момент. Вместо одного монструозного manageEverything(), который читает и пишет что угодно, мы проектируем мелкие, чёткие функции, разделённые хотя бы на чтение и запись.
Для GiftGenius это особенно ясно: одно дело — читать список подарков и предпочтения пользователя, другое — создавать или подтверждать заказ (там уже деньги). Поэтому мы обычно делаем:
- набор безопасных «read‑only» инструментов (поиск подарков, просмотр деталей),
- отдельные «write» инструменты (создание черновика заказа, отмена заказа),
- и, если нужно, ещё один уровень для особо опасных операций (подтверждение платежа, массовые изменения).
Разные агенты под разные задачи
Ещё один мощный приём — разделять агентов по зонам ответственности. Один агент — «подбор подарков», другой — «управление заказами». Тогда даже если модель в 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 ${tool} запрещён для агента ${agent.id}`);
}
if (!canUserCallTool(userRole, tool)) {
throw new Error(`Пользователь с ролью ${userRole} не может вызывать ${tool}`);
}
}
В итоге даже если модель вдруг решит вызвать confirm_order из неправильного агента или от имени гостя — вызов упрётся в эту проверку и превратится в управляемую ошибку, а не в незапланированный платёж.
Разные конфигурации по окружениям
В dev и staging окружениях вы часто хотите дать агенту больше свободы: тестовые инструменты, фейковые платёжные сервисы, экспериментальные функции. В production, наоборот, конфигурация максимально жёсткая: часть tools отключена, endpoints — только боевые, токены — только реальные.
Простейшая схема:
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. Мы задаём лимиты по шагам run‑цикла, по количеству tool‑вызовов и по размеру контекста (token limit). Модель не может бесконечно «думать» и плодить tool‑calls — в какой-то момент run завершится с ошибкой «лимит шагов» или «лимит времени».
Всё это складывается в классическую «оборонительную архитектуру», которую удобно представить схемой.
graph TD
A[Промпт / system‑инструкции] --> B[JSON Schema инструментов]
B --> C[Пермиссии агента и пользователя]
C --> D[Sandbox инфраструктуры]
D --> E[Внешние сервисы / БД]
subgraph Агент
A
B
C
end
subgraph Инфраструктура
D
end
Промпт — самая слабая защита; настоящая сила начинается там, где вы физически ограничиваете, что может сделать ваш код и какие API доступны.
Лимиты на run‑цикл: шаги, время, tool‑calls
Часть sandbox можно выразить прямо в конфигурации агента: максимальное количество шагов, общее время выполнения, лимит tool‑вызовов. Это не только защита от runaway‑циклов, но и контроль затрат.
Пример абстрактной конфигурации run‑опций:
type RunLimits = {
maxSteps: number;
maxToolCalls: number;
timeoutMs: number;
};
const defaultLimits: RunLimits = {
maxSteps: 8,
maxToolCalls: 10,
timeoutMs: 30_000,
};
Такие лимиты вы потом передаёте в обёртку, которая запускает агента. Если вдруг модель решила 11‑й раз дернуть инструмент — вы прерываете запуск агента и честно говорите пользователю, что задача слишком сложная, а не даёте агенту бесконтрольно жечь бюджет.
Изоляция кода и сети
На уровне контейнера/процесса обычные практики такие:
Код MCP‑сервера и/или агентного сервиса запускается в контейнере с read‑only файловой системой (кроме специально отведённого рабочего каталога) и ограниченными ресурсами (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$».
Вместо того чтобы кидать в prompt полную историю заказов пользователя, можно сделать 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. Мониторинг и наблюдаемость агента
Что именно нужно видеть
Мы ограничили, что агент может делать, какие данные он видит и что попадает в логи. Следующий вопрос — как понять, что во всём этом «зоопарке» агент в проде ведёт себя так, как мы задумали?
Обычный мониторинг «сервис жив / не жив» для агента почти бесполезен. Нам важно не только знать, что процесс жив, но и понимать его поведение: какие шаги он делает, какие инструменты вызывает, где ошибается, где зацикливается.
Минимальный набор данных по каждому run:
- 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 |
|---|---|---|
| Логи | «Истории» про конкретные run‑ы | Детальный AgentRunLog c шагами и инструментами |
| Метрики | Агрегированные числовые показатели | p95 длительность run, среднее число tool‑calls, error‑rate |
| Трейсинг | Дерево / граф запросов и подзапросов | Run → шаги → tool‑calls → вызовы внешних API (commerce, БД и т.п.) |
Метрики нужны, чтобы понять «а вообще всё хорошо?» (например, error‑rate за последний час). Логи и трейсинг — чтобы разобраться «почему плохо именно здесь?» и воспроизвести конкретный проблемный run.
Пример зачатка метрик можно реализовать поверх логов: периодическая задача агрегирует 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‑сервер и агентный сервис, изолирована: контейнеры с read‑only файловой системой (кроме /tmp или специально выделенного каталога), ограничением CPU/RAM, сетью по allow‑list доменов. Если агент вдруг попытается дернуть «что‑то левое», он просто не сможет до этого добраться физически.
Если в какой‑то момент вы видите всплеск метрики «доля run‑ов со статусом limits_exceeded» или «среднее число tool‑calls > 10», вы понимаете, что либо промпт стал излишне болтливым, либо один из инструментов глючит и заставляет агента перезапускать шаги.
Это уже поведение взрослого сервиса, а не экспериментального агента «как-нибудь сойдёт».
7. Типичные ошибки при выводе агентов в продакшен
Всё, что мы обсуждали выше, — это «правильная» картинка продакшен‑агента. На практике же чаще всего встречаются типовые грабли. Соберём их в один список: если избежать хотя бы этих ошибок, запуск в прод пройдёт гораздо спокойнее.
Ошибка №1: агенту «разрешили всё».
Распространённый сценарий: вы описали кучу MCP‑инструментов (поиск, изменение, удаление, платежи), а при создании агента просто скормили ему весь список. В результате модель может случайно вызвать удаление или платеж там, где хотели только чтение. Лечится разделением инструментов по ролям и созданием нескольких более узких агентов, у каждого из которых свой allowedTools.
Ошибка №2: проверка прав только в промпте.
Иногда разработчики пишут в system‑инструкциях: «никогда не покупай ничего без подтверждения пользователя» и на этом успокаиваются. Но промпт — слабая защита, а jail‑breakи и просто ошибки никто не отменял. Нужны реальные проверки на backend‑уровне: «агенту этот tool разрешён» и «пользователю этот tool разрешён», иначе одна неаккуратная генерация может привести к действиям, на которые никто не рассчитывал.
Ошибка №3: секреты в промптах и логах.
Иногда хочется «ускорить интеграцию» и просто положить API‑ключ в system‑prompt или передать его в tool‑аргументах, чтобы агент сам ходил на внешний API. В итоге ключ оказывается и в логах модели, и потенциально в сторонних системах. Это прямой путь к утечкам и бану в Store. Секреты должны жить только на серверной стороне, в переменных окружения или менеджере секретов, и никогда не попадать в контекст модели.
Ошибка №4: «сырые» логи без scrub‑инга.
При отладке удобно писать console.log(...) и забыть про это. Через пару месяцев оказывается, что в логах лежат адреса пользователей, телефоны, номера заказов с PII. Особенно неприятно в мире GDPR и других регуляций. Лучше сразу завести центральный логгер и внедрить автоматическое маскирование чувствительных полей, даже если кажется, что «мы логируем только на dev».
Ошибка №5: отсутствие лимитов на поведение агента.
Без ограничений по шагам, времени и количеству tool‑вызовов агент может зациклиться: многократно вызывать один и тот же инструмент, пытаться бесконечно исправлять одну и ту же ошибку, тратить кучу токенов и грузить внешние API. В лучшем случае вы получите гигантские счета за модели, в худшем — положите backend и разозлите всех пользователей. Лимиты на run‑цикл и sane defaults по таймаутам — обязательная часть конфигурации.
Ошибка №6: смешивание read и write‑операций в одном инструменте.
Иногда создают «удобные» методы вроде getOrCreateOrder, которые при отсутствии заказа создают новый. Для классического backend это допустимый паттерн, но в мире агентов это может привести к неожиданным побочным эффектам: модель хотела просто узнать состояние, а инструмент что‑то создал. Гораздо безопаснее разделять get_order_details и create_order_draft, тогда даже при повторных вызовах последствия более контролируемы.
Ошибка №7: игнорирование наблюдаемости.
Многие начинают с «потом прикрутим логи и метрики, сейчас главное — чтобы работало». Агенты без мониторинга — это чёрный ящик: вы не знаете, какие инструменты они вызывают, сколько шагов делают, где ошибаются. Любая жалоба пользователя превращается в расследование в тёмной комнате. Гораздо проще сразу заложить структуру логов (agent_run_id, tools, статус) и базовые метрики, чем потом пытаться достроить это поверх хаотичного кода.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ