AI-агент в production не является prompt с большим контекстным окном. Это сервис, в котором модель выбирает действия, получает их результаты и продолжает работу, пока не достигнет ограниченного результата или не попросит человека. Модель является только одной частью сервиса. Остальной control plane решает, что модель может видеть, какие инструменты ей доступны, какие действия требуют approval, как состояние переживает перезапуск и какие доказательства нужны перед выпуском новой версии.
Ниже описана reference architecture для одного агента с небольшим набором инструментов. Те же границы подходят для workflow с фиксированными шагами и для будущей multi-agent системы. Начинайте с минимального цикла, который решает задачу. В руководстве Anthropic по эффективным агентам workflow с предсказуемым путем отделён от системы, где модель динамически управляет вызовами инструментов. Большая автономность является продуктовым выбором, а не архитектурной необходимостью.
Reference architecture
Разделите путь запроса, цикл принятия решений и эффекты, изменяющие внешний мир. Gateway аутентифицирует вызывающего, создаёт request ID и trace ID, применяет квоты и удаляет или классифицирует чувствительные поля. Orchestrator владеет циклом. Model adapter переводит нейтральное состояние в запрос провайдеру и ответ провайдера в небольшой тип decision. Tool gateway проверяет аргументы, авторизует действующего субъекта, применяет policy и вызывает изолированный connector. State store и журнал событий должны быть вне процесса, чтобы worker мог продолжить работу после сбоя.
[Пользователь или API-клиент]
|
[Gateway: identity, limits, request ID]
|
[Orchestrator: policy -> model -> tool loop]
/ | \
[State store] [Approval service] [Tool gateway]
| | |
[Memory/RAG] [Решение человека] [API, файлы, очереди]
|
[Events и traces]
Не давайте модели сырые credentials, неограниченный network access или прямую запись в базу. Tool gateway должен предоставлять узкие операции вроде "search_orders", "draft_refund" или "send_message", а не универсальный HTTP-клиент и не SQL-консоль. Для каждой операции нужны владелец, input schema, permission, risk class, timeout и описанный формат результата.
Используйте явный request envelope:
| Поле | Назначение |
|---|---|
| tenantId и actorId | Привязать каждое чтение и каждый эффект к авторизованному субъекту. |
| requestId и traceId | Связать retries, approvals, tool calls и логи. |
| goal | Хранить цель пользователя отдельно от сообщений модели. |
| policyVersion и modelVersion | Сделать запуск достаточно воспроизводимым для расследования. |
| deadline и stepBudget | Ограничить время и число ходов model-tool. |
NIST AI RMF Generative AI Profile помогает организовать работу с рисками по всему жизненному циклу. Переводите вопросы этого профиля в конкретные controls внутри envelope и tool gateway. Сам по себе framework не доказывает безопасность конкретного deployment.
Tool loop
У цикла должен быть один владелец и видимый переход состояния на каждом ходе:
- Загрузить run, policy, summary разговора и разрешённый каталог инструментов.
- Собрать ограниченный model input. Пометить user text, retrieved text, tool output и system instructions разными trust classes.
- Попросить модель вернуть final response или typed tool call. Проверить имя и аргументы до выполнения.
- Принять решение об authorization и risk policy вне модели. Фраза модели о безопасности действия не является решением об authorization.
- Если действие пересекает approval gate, сохранить pending action и остановиться. Продолжить после явного решения, загрузив сохранённое действие.
- Выполнить connector с deadline и idempotency key. Записать redacted result и добавить его в state.
- Проверить лимиты шагов, токенов, стоимости и времени. Продолжить только если policy разрешает ещё один ход.
- Вернуть ответ, в котором сказано, что произошло, что не произошло и какая approval или неопределённость осталась.
Не оборачивайте вызов модели в неограниченный цикл "while". Step budget не даёт prompt или инструменту превратить запрос в дорогую цепочку. Deadline защищает caller и пул workers. Detector повторных вызовов замечает, что модель снова просит одно и то же неудачное действие. Circuit breaker может временно отключить деградировавший connector, оставив read-only инструменты доступными.
Спецификация Model Context Protocol стандартизирует resources, prompts и tools, но не заменяет consent, authorization и isolation controls хост-приложения. Относитесь к MCP server как к внешней зависимости. Закрепляйте его identity, проверяйте рекламируемые tools, ограничивайте передаваемые данные и применяйте к нему ту же gateway policy, что и к внутреннему connector.
State, memory и context
State является устойчивым журналом одного run. Храните user goal, messages или ссылки на них, tool calls, tool results, approvals, policy decisions, версии model и tools, timestamps, status и classification ошибки. Сначала добавляйте events, затем стройте текущий run view. Так recovery и audit проще, чем при мутации одного непрозрачного JSON blob. Шифруйте чувствительные поля, изолируйте tenants, задайте retention и реализуйте удаление для snapshots, indexes, traces и caches.
Conversation history не равна memory. Для текущего run оставляйте краткоживущий working context. User или business memory сохраняйте только при понятной цели, retention rule, source и возможности исправления пользователем или администратором. Храните facts с provenance и полем confidence или freshness. Не записывайте догадки модели в долговременную memory только потому, что они звучат убедительно.
Retrieval является tool с trust boundary. До ranking применяйте tenant и authorization filters. Передавайте в context source identifiers и timestamps. Говорите модели, что retrieved text является данными, а не инструкциями. Ограничивайте chunks, удаляйте ненужные secrets и по умолчанию логируйте query и document identifiers без private content. В PostgreSQL можно сравнить lexical и vector candidates и делать rerank только после authorization. Внутреннее руководство по гибридному RAG с pgvector подробнее описывает эту границу.
Compaction context должна быть достаточно детерминированной для debugging. Суммируйте старые turns по фиксированной схеме, сохраняя decisions, нерешённые вопросы, tool effects, citations и user constraints. Original event log должен оставаться вне prompt. Если summary меняет смысл pending action, остановитесь и запросите review вместо тихого продолжения.
Tools и threat boundaries
Tool schema, security contract, а не только metadata для модели. Gateway должен проверять типы, длины, enum values, ownership ресурса и связи между полями. После authorization переводите имена в internal identifiers. Разделяйте read и write tools. Выдавайте connector короткоживущие credentials, ограниченные одним tenant и одной операцией. Опасный code или file work выполняйте в изолированном worker с allowlist filesystem и network.
Считайте любой внешний контент потенциально враждебным. Email, web page, issue, document, tool description или MCP resource могут содержать indirect prompt injection. Ограничивайте его в model input, удаляйте executable markup где уместно и оставляйте решение о permissions приложению. Output validation должна быть независимой от модели. Например, перед payment connector проверяйте refund amount по order, currency, actor permission и policy maximum.
Threat model должен включать:
| Граница | Что нужно не допустить |
|---|---|
| User → gateway | Account takeover, oversized requests и чужие tenant identifiers. |
| Model → tool gateway | Argument injection, путаницу privileges и excessive agency. |
| Retrieval или MCP → model | Indirect prompt injection и вредоносные инструкции в данных. |
| Connector → внешняя service | Утечку credentials, SSRF, replay и data exfiltration. |
| Worker → state store | Подмену events, устаревшую policy и неполный audit trail. |
OWASP GenAI LLM Top 10 выделяет prompt injection, excessive agency, insecure output handling и unbounded consumption как риски, требующие application-level mitigations. Внутреннее руководство по prompt injection и MCP security содержит threat-focused checklist. System prompt полезен как инструкция, но не является sandbox, authorization layer или secret store.
Approval gates и human control
Ставьте approvals вокруг effects, а не вокруг безвредного reasoning. Чтение собственного календаря пользователя может быть автоматическим. Отправка внешнего сообщения, изменение записи, выдача денег, удаление данных или deploy code обычно требуют policy decision с учётом actor, target, amount, reversibility и confidence. Gate должен находиться в application code, чтобы prompt не мог его обойти.
Сохраняйте approval request с proposed tool, normalized arguments, affected resources, reason, policy version, expiration и hash релевантного state. Reviewer должен видеть те же данные. Привяжите решение reviewer к action hash и actor. После approval повторно проверьте authorization, freshness и budget перед execution. При rejection или expiry запишите решение и сообщите модели, что действие не произошло. Старая approval не должна авторизовать изменённый payload.
Режимы human-in-the-loop:
| Режим | Подходящее применение |
|---|---|
| Observe | Записывать или выборочно проверять low-risk actions, оставляя agent automatic. |
| Confirm | Запрашивать approval непосредственно перед irreversible effect. |
| Review | Дать человеку проверить полный draft и выбранные evidence. |
| Take over | Передать run оператору вместе с текущими state и lock. |
Pause должен быть обычным state, а не exception. Queue доставляет pending approvals, notification может истечь, а worker продолжит run на другом host. Пользователь должен видеть, думает ли agent, ждёт ли данные, approval, retry или завершение.
Retries, idempotency и recovery
Перед retry классифицируйте error. Validation error требует исправленного call или вопроса пользователю. Authentication и authorization errors должны остановить run. Rate limits должны использовать retry signal провайдера. Timeout и connection reset неоднозначны для writes: удалённая service могла уже применить effect. Повторяйте только операцию, для которой semantics или idempotency key делает повтор безопасным. Раздел 9.2.2 RFC 9110 объясняет, почему non-idempotent methods нельзя автоматически повторять без способа установить безопасность эффекта.
Генерируйте стабильный key из run и logical action, а не из attempt number. Connector хранит key и final result достаточно долго для retry window. Если тот же key приходит с другими arguments, отклоняйте его. Используйте exponential backoff с jitter и небольшим максимумом attempts. Retry не является новым model decision. Сохраняйте original call, attempt number, response class и connector request ID.
Recovery относится к state machine. Используйте statuses "running", "waiting_for_approval", "retrying", "failed", "completed" и "cancelled". Lease не даёт двум workers выполнить один run одновременно. После потери lease остановитесь до следующего effect. Reconciler может сравнить pending actions с connector records после crash worker. Cancellation должна передаваться model requests, tool calls, queues и approval requests, если провайдер это поддерживает.
Observability
Создавайте один trace на user request и spans для model calls, retrieval, policy checks, approvals и tools. Записывайте duration, status, retry count, input и output token counts если они доступны, versions model и prompt, tool name, risk class и cost estimate. До export редактируйте secrets и sensitive content. Stable run ID связывает approval или support ticket с trace, но не помещайте personal data в baggage. OpenTelemetry context propagation описывает связь trace context между services и предупреждает о недоверенных headers и sensitive baggage.
Полезные measures: task completion по определённой rubric, успешность tool calls, validation failures, approval rate, retry и timeout rate, p50 и p95 latency, token и tool cost на completed task, cancellation rate и policy blocks. Разрезайте их по model version, tool, tenant class и release. Низкое число errors может скрывать много тихих wrong answers, поэтому связывайте traces с выборочными transcripts и evaluator results. Не логируйте chain-of-thought. Храните краткие decision metadata и user-visible reasoning или citations, разрешённые policy.
Внутреннее руководство по observability AI-агентов показывает, как сделать signals полезными, не превращая logs во вторую базу secrets.
Evals до и после release
Agent eval является scenario с определённым starting state, разрешёнными tools, ожидаемыми invariants и scoring rule. Одного final answer недостаточно. Проверяйте, что agent использовал authorized tool, сохранил tenant scope, запросил approval когда нужно, не повторил write, сослался на правильный source и остановился на budget. Добавляйте adversarial scenarios: вредоносный retrieved text, недоступный connector, timeout после write, устаревшая approval, неоднозначный request и malformed tool data.
Используйте layered suite:
- Deterministic unit tests для schemas, authorization, redaction, idempotency, state transitions и budget enforcement.
- Replay tests с recorded tool responses и fixed model decisions для проверки recovery.
- Scenario tests с моделью и rubric для task outcome, safety и communication.
- Red-team tests для direct и indirect injection, data leakage, excessive agency и denial of service.
- Production sampling с privacy controls, human review и превращением failures в regression cases.
Version everything: scenario, tools, policy, prompts, model и evaluator. Храните failures вместе с trace и минимальным reproducing input. Сравнивайте candidate release с baseline и не допускайте regression на hard safety invariants, даже если среднее качество выросло. Руководство Anthropic по evals агентов объясняет, почему multi-turn tool use требует оценки trajectory. Внутреннее руководство по evals AI-агентов даёт практическую test matrix.
Cost и latency
Каждый model turn, retrieved token, tool call, approval pause и retry добавляет time или money. Задавайте budgets для классов задач, а не один global number. Используйте небольшую model для routing, extraction и policy prechecks, если измеренная точность достаточна. Более сильную model оставляйте для неоднозначного planning или final synthesis. Кэшируйте стабильные tool catalogs и retrieval embeddings. Сжимайте context до роста, но измеряйте, не вызывает ли summary новые turns или потерю facts.
Параллелизуйте независимые read-only calls, затем объединяйте результаты с явным provenance. Writes оставляйте последовательными, если connector не предлагает transaction или спроектированную compensation. Показывайте пользователю progress, не раскрывая secrets. Долгую работу сохраняйте как job и продолжайте worker после завершения HTTP request. Более быстрая model не дешевле, если ошибки вызывают human review, compensating writes или повторные runs. Считайте total cost на correct и policy-compliant outcome.
Минимальный runnable loop
Ниже приведён protocol-independent TypeScript example с fake model и локальными tools. Он запускается без provider SDK и network access. Пример показывает typed decision loop, write approval, bounded retries, стабильный idempotency key и защиту от duplicate effect. Реальный adapter может заменить model, сохранив policy и tool boundary.
type ToolCall = { id: string; name: string; input: unknown };
type Message =
| { role: "user"; content: string }
| { role: "assistant"; content: string; toolCall?: ToolCall }
| { role: "tool"; callId: string; content: string };
type Decision =
| { kind: "answer"; text: string }
| { kind: "call"; call: ToolCall };
type Tool = {
sideEffect: "read" | "write";
run(input: unknown, idempotencyKey: string): Promise<string>;
};
const issued = new Set<string>();
const tools: Record<string, Tool> = {
getBalance: {
sideEffect: "read",
async run(): Promise<string> {
return JSON.stringify({ account: "demo", cents: 4200 });
},
},
sendInvoice: {
sideEffect: "write",
async run(input: unknown, idempotencyKey: string): Promise<string> {
if (issued.has(idempotencyKey)) return "already-sent";
if (typeof input !== "object" || input === null) throw new Error("invalid input");
issued.add(idempotencyKey);
return "invoice-sent";
},
},
};
const model = {
async decide(messages: readonly Message[]): Promise<Decision> {
const toolCount = messages.filter((message) => message.role === "tool").length;
if (toolCount === 0) {
return { kind: "call", call: { id: "balance-1", name: "getBalance", input: {} } };
}
if (toolCount === 1) {
return { kind: "call", call: { id: "invoice-1", name: "sendInvoice", input: { cents: 1200 } } };
}
return { kind: "answer", text: "The balance was checked and the invoice was sent." };
},
};
const wait = (milliseconds: number) => new Promise((resolve) => setTimeout(resolve, milliseconds));
async function execute(call: ToolCall, tool: Tool, key: string): Promise<string> {
for (let attempt = 0; attempt < 3; attempt += 1) {
try {
return await tool.run(call.input, key);
} catch (error) {
if (attempt === 2) throw error;
await wait(10 * 2 ** attempt);
}
}
throw new Error("unreachable");
}
async function run(): Promise<string> {
const state: { messages: Message[]; completed: Set<string> } = {
messages: [{ role: "user", content: "Check the balance and send the invoice." }],
completed: new Set<string>(),
};
const sessionId = "session-demo";
for (let step = 0; step < 6; step += 1) {
const decision = await model.decide(state.messages);
if (decision.kind === "answer") return decision.text;
const tool = tools[decision.call.name];
if (!tool) throw new Error("unknown tool");
const key = sessionId + ":" + decision.call.id;
if (tool.sideEffect === "write" && !process.argv.includes("--approve")) {
return "Paused for approval: " + decision.call.name;
}
if (state.completed.has(key)) continue;
const result = await execute(decision.call, tool, key);
state.completed.add(key);
state.messages.push(
{ role: "assistant", content: "", toolCall: decision.call },
{ role: "tool", callId: decision.call.id, content: result },
);
}
throw new Error("step budget exceeded");
}
run().then(console.log).catch((error: unknown) => {
console.error(error);
process.exitCode = 1;
});
Скомпилируйте код project TypeScript toolchain и запустите emitted JavaScript без flag, чтобы увидеть approval pause, затем с --approve, чтобы разрешить write. Пример намеренно хранит state в памяти. В production state должен быть durable, tenant-scoped, при необходимости encrypted и восстановлен lease-aware worker.
Порядок сборки
Определите task outcome и запрещённые effects до выбора model. Реализуйте один read tool и один reversible write tool за schemas и authorization. До добавления новых tools добавьте durable events, budgets, approval states и idempotency. Сразу instrument первый end-to-end trace. Создайте eval scenarios из реальных failure modes и запускайте их при каждом изменении prompt, policy, tool или model. Добавляйте MCP или multi-agent delegation только при измеренной необходимости, оправдывающей новую trust boundary.
Дальнейшее чтение
- Prompt injection и MCP security
- Evals AI-агентов
- Observability AI-агентов
- Гибридный RAG с pgvector
- Anthropic: Building effective agents
- Anthropic: Demystifying evals for AI agents
- Model Context Protocol specification
- NIST AI RMF Generative AI Profile
- OWASP GenAI LLM Top 10
- OpenTelemetry context propagation
- RFC 9110 HTTP Semantics