Как строить AI-агентов: практические
паттерны из production-кода Anthropic
Этот гайд написан для тех, кто прямо сейчас проектирует собственных AI-
агентов или инструменты автоматизации. Мы не будем заниматься абстрактной
теорией. Вместо этого мы посмотрим, как решают реальные инженерные
проблемы разработчики из Anthropic в кодовой базе Claude Code.
В основе этого материала лежит анализ более 20 тысяч строк production-кода.
Цель — вытащить оттуда конкретные архитектурные решения, хитрые трюки и
неочевидные tradeoff-ы, которые вы можете применить в своих пайплайнах
(например, для глубокого ресёрча или автоматизации браузера).
Оглавление
1. Как проектировать инструменты (Tools), чтобы агент не ломался
2. Как управлять контекстом в длинных сессиях (Compaction)
3. Архитектура подагентов: как распараллелить глубокий ресёрч
4. Безопасность: как не дать агенту удалить вашу базу данных
5. Как расширять агента: хуки и MCP
6. Как ускорить запуск и работу: трюки из production
1. Как проектировать инструменты (Tools), чтобы
агент не ломался
Когда вы строите агента-оркестратора, его возможности определяются набором
доступных инструментов. В простых туториалах инструмент — это просто
функция, которая принимает строку и возвращает строку. В реальности всё
сложнее.
Влад Куклев | [Link]/prod1337
Проблема раздувания контекста
Если у вашего агента 50+ инструментов (особенно если вы используете MCP-
серверы), системный промпт мгновенно забивается описаниями этих
инструментов. Описание одного сложного MCP-сервера может весить 15-60
килобайт.
Как это решено: Паттерн “Deferred Tools” (отложенная загрузка). Модель
изначально видит только имена доступных инструментов и короткие подсказки
(search hints). Когда агенту нужен конкретный инструмент, он отправляет
специальный блок tool_reference . Система перехватывает его, подгружает
полную JSON-схему инструмента и возвращает её в контекст.
Это снижает потребление токенов на старте сессии на порядки и позволяет
подключать к агенту сотни инструментов одновременно.
Принцип Fail-Closed и фабрика buildTool()
При проектировании интерфейса инструмента критически важно правильно
задать значения по умолчанию. В кодовой базе Claude Code фабрика
buildTool() использует принцип fail-closed:
const TOOL_DEFAULTS = {
isEnabled: () => true,
isConcurrencySafe: (_input?) => false, // НЕ безопасен для параллелизма
isReadOnly: (_input?) => false, // Считается деструктивным
isDestructive: (_input?) => false,
checkPermissions: (input, _ctx?) =>
[Link]({ behavior: 'allow', updatedInput: input }),
toAutoClassifierInput: (_input?) => '', // Пропустить в классификаторе
}
export function buildTool<D extends AnyToolDef>(def: D): BuiltTool<D> {
return { ...TOOL_DEFAULTS, userFacingName: () => [Link], ...def }
}
Обратите внимание: isConcurrencySafe по умолчанию false . Если вы явно не
указали, что инструмент можно запускать параллельно с другими, система будет
выполнять его строго последовательно. isReadOnly тоже false — инструмент
Влад Куклев | [Link]/prod1337
считается потенциально деструктивным, пока не доказано обратное. Этот
подход защищает от случайных race conditions, когда агент решает запустить
чтение файла и его удаление в одном параллельном батче.
Минимальное определение нового инструмента выглядит так:
const MyTool = buildTool({
name: 'MyTool',
maxResultSizeChars: 100_000,
get inputSchema() { return [Link]({ query: [Link]() }) },
async call(input, context) {
const result = await doWork([Link])
return { data: result }
},
async description() { return 'Поиск по базе знаний' },
async prompt() { return 'Используй этот инструмент для...' },
mapToolResultToToolResultBlockParam(content, id) { /* ... */ },
renderToolUseMessage(input, opts) { return null },
})
Все входные данные проходят через Zod-валидацию до любых других проверок.
При ошибке модель получает форматированное сообщение с описанием
ожидаемой схемы — и может исправить свой вызов на следующей итерации.
Параллельное выполнение: StreamingToolExecutor
Когда модель возвращает несколько tool_use блоков в одном ответе, система
решает, что можно запустить параллельно, а что — строго последовательно:
class StreamingToolExecutor {
private canExecuteTool(isConcurrencySafe: boolean): boolean {
const executingTools = [Link](t => [Link] === 'executing')
return (
[Link] === 0 ||
(isConcurrencySafe && [Link](t => [Link]))
)
}
}
Влад Куклев | [Link]/prod1337
Правило простое: concurrent-safe инструменты (Read, Glob, Grep) могут
выполняться параллельно друг с другом. Non-concurrent инструменты (Edit,
Write, Bash) выполняются эксклюзивно. Если один инструмент из параллельной
группы падает с ошибкой, [Link]() отменяет остальные
— но не убивает родительский query loop.
2. Как управлять контекстом в длинных сессиях
(Compaction)
Ограничение контекстного окна — ключевая проблема при разработке агентов.
Когда агент-исследователь анализирует десяток GitHub-репозиториев или
читает длинные PDF-документы, контекст забивается за пару итераций.
Anthropic использует многоуровневый конвейер компактификации (сжатия)
контекста. Каждый следующий уровень применяется только если предыдущий
не помог.
Уровень 1: Tool Result Budget
Никогда не отдавайте агенту сырой результат выполнения команды, если он
слишком большой. Если скрипт парсинга вернул 10 мегабайт текста, модель
превысит лимит токенов или потеряет фокус.
Паттерн: обрезать результат до разумного лимита (например, 10 000 символов).
Полный результат сохраняется на диск в скрытую папку, а в контекст агенту
передаётся усечённая версия плюс абсолютный путь к полному файлу. Если
агенту нужны детали, он использует инструмент чтения файлов, чтобы
прочитать конкретные строки.
Уровень 2: Snip Compact
По мере продвижения диалога старые результаты выполнения инструментов
теряют актуальность. Система проходит по истории сообщений и заменяет
длинные ответы инструментов на плейсхолдер [snipped] . Это
инкрементальная операция, которая не требует вызова LLM.
Влад Куклев | [Link]/prod1337
Уровень 3: Microcompact — три стратегии
В коде реализованы три стратегии microcompact:
Стратегия 1 — Time-based. Если пауза между сообщениями превышает порог
(prompt cache истёк), система очищает старые tool results, сохраняя только
последние N:
function maybeTimeBasedMicrocompact(messages, querySource) {
const trigger = evaluateTimeBasedTrigger(messages, querySource)
if (!trigger) return null
const keepRecent = [Link](1, [Link])
const keepSet = new Set([Link](-keepRecent))
const clearSet = new Set(
[Link](id => )
)
// Заменяем содержимое на stub:
return { ...block, content: '[Old tool result content cleared]' }
}
Стратегия 2 — Cached Microcompact. Если кэш ещё жив, используется API
cache_edits , которая позволяет удалять старые результаты из истории, не
инвалидируя при этом дорогой prompt cache. Это снижает стоимость длинных
сессий за счет сохранения кэша промпта. Фича доступна только через feature
gate:
if (feature('CACHED_MICROCOMPACT')) {
const mod = await getCachedMCModule()
if ([Link]()
&& [Link](model)) {
return await cachedMicrocompactPath(messages, querySource)
}
}
Стратегия 3 — Server-side API Microcompact. Самая новая: вместо модификации
сообщений на клиенте, система передаёт серверу инструкцию
context_management с типом clear_tool_uses_20250919 или
clear_thinking_20251015 , и сервер сам решает, что удалить.
Влад Куклев | [Link]/prod1337
Уровень 4: Auto Compact (LLM-суммаризация)
Когда простые методы исчерпаны, в дело вступает LLM. Конкретные пороги из
кода:
const AUTOCOMPACT_BUFFER_TOKENS = 13_000
const WARNING_THRESHOLD_BUFFER_TOKENS = 20_000
const MANUAL_COMPACT_BUFFER_TOKENS = 3_000
function getAutoCompactThreshold(model: string): number {
const effectiveContextWindow = getEffectiveContextWindowSize(model)
return effectiveContextWindow - AUTOCOMPACT_BUFFER_TOKENS
}
Оценка токенов делается на клиенте с коэффициентом запаса 4⁄3 (conservative
padding). Точный подсчёт через API CountTokens слишком дорог для каждого
решения о компактификации.
Промпт для суммаризации требует структурированного ответа из 9 секций:
Primary Request, Key Technical Concepts, Files and Code Sections, Errors and Fixes,
Problem Solving, All User Messages (вербатим-цитаты — это критично), Pending
Tasks, Current Work, Optional Next Step.
Уровень 5: Session Memory Compact (Zero-API-call)
Самый экономичный вариант. Вместо вызова модели для создания summary,
используется уже существующий Session Memory файл (который модель
обновляет после каждого turn):
Влад Куклев | [Link]/prod1337
const DEFAULT_SM_COMPACT_CONFIG = {
minTokens: 10_000,
minTextBlockMessages: 5,
maxTokens: 40_000,
}
async function trySessionMemoryCompaction(messages, agentId, threshold) {
await waitForSessionMemoryExtraction()
const sessionMemory = await getSessionMemoryContent()
if (!sessionMemory || await isSessionMemoryEmpty(sessionMemory)) {
return null // Fallback на обычный autocompact
}
// Собираем summary из session memory — без API-вызова
}
Инцидент из production: Одно время Auto Compact работал без ограничений.
При сбоях он уходил в бесконечный цикл, тратя до 250 000 API-вызовов в день.
Решение — жёсткий circuit breaker:
const MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES = 3
async function autoCompactIfNeeded(...) {
if (tracking?.consecutiveFailures >= MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES)
{
return { wasCompacted: false } // Сдаёмся
}
}
Комментарий из кода: “BQ 2026-03-10: 1,279 sessions had 50+ consecutive failures
(up to 3,272) in a single session, wasting ~250K API calls/day globally.” Обязательно
реализуйте circuit breaker для любых фоновых LLM-вызовов в ваших пайплайнах.
3. Архитектура подагентов: как распараллелить
глубокий ресёрч
Когда вы строите пайплайн для глубокого исследования (Deep Research), вам
нужен агент-оркестратор, который раздаёт задачи агентам-исследователям. В
Влад Куклев | [Link]/prod1337
коде это реализовано через модуль AgentTool .
Существует два принципиально разных подхода к созданию подагентов.
Fresh Subagent (С чистого листа)
Подагент запускается с пустым контекстом. Вы передаёте ему только системный
промпт и конкретную задачу. Плюсы: Идеальная изоляция, минимальное
потребление токенов. Можно использовать дешёвую модель (например, GPT-4o-
mini или Claude Haiku) для простых задач парсинга. Минусы: Подагент не знает
контекста общей задачи и может задавать глупые вопросы или дублировать
работу.
Fork Subagent (Форк с наследованием)
Это более продвинутый паттерн. Подагент получает точную копию всей истории
сообщений родительского агента на момент создания. Плюсы: Подагент
полностью в контексте. Благодаря механизму prompt caching (когда префикс
сообщений совпадает байт-в-байт), вы экономите от 50% до 80% стоимости
входных токенов, так как история родителя уже закэширована на серверах
провайдера. Минусы: Требует аккуратной работы с состоянием, чтобы подагент
не начал модифицировать те же файлы, что и родитель.
Вот как это выглядит в коде. Ядро подагента — это обычный рекурсивный вызов
того же query() , но с изолированным контекстом:
async function* runAgent({
agentDefinition, promptMessages, toolUseContext,
canUseTool, isAsync, querySource, model, availableTools,
}): AsyncGenerator<Message> {
// 1. Инициализация MCP-серверов агента
// 2. Фильтрация инструментов (через agent restrictions)
// 3. Сборка system prompt
// 4. Регистрация frontmatter hooks
// 5. Рекурсивный вызов query()
// 6. Yield сообщений родителю
// 7. Cleanup (MCP, shell tasks, hooks)
}
Влад Куклев | [Link]/prod1337
Для fork-агентов используется специальная структура CacheSafeParams , которая
гарантирует попадание в prompt cache:
type CacheSafeParams = {
systemPrompt: SystemPrompt
userContext: { [k: string]: string }
systemContext: { [k: string]: string }
toolUseContext: ToolUseContext
forkContextMessages: Message[] // Копия истории родителя
}
// Каждый fork клонирует file state и создаёт независимый denial tracking:
fileStateCache: cloneFileStateCache([Link]),
denialTracking: createDenialTrackingState(),
Изоляция рабочих директорий (Worktrees)
Если ваши исследователи пишут код или скачивают файлы, они могут
конфликтовать друг с другом. Паттерн решения: для каждого агента создаётся
собственный изолированный git worktree в скрытой директории (например,
.claude/worktrees/{slug}/ ). Тяжёлые зависимости (вроде node_modules )
подключаются через симлинки, чтобы не тратить место на диске.
ID задач генерируются с type-prefix и криптографическим рандомом — 36^8
комбинаций (около 2.8 триллиона), что защищает от brute-force symlink-атак:
function generateTaskId(type: TaskType): string {
const prefix = getTaskIdPrefix(type) // 'b'=bash, 'a'=agent, 'r'=remote...
const bytes = randomBytes(8)
// ...
}
Инцидент из production: Отсутствие лимитов на создание агентов привело к
тому, что система породила 292 агента за 2 минуты. Каждый агент потреблял
около 20 мегабайт оперативной памяти (а при активной работе — до 125
мегабайт). Результат: 36.8 гигабайт RSS и падение системы. Урок: Всегда
устанавливайте жесткий hard cap на количество параллельных агентов и
Влад Куклев | [Link]/prod1337
реализуйте механизм graceful rejection (отказ в создании с понятной ошибкой), а
также сборку мусора для завершённых задач.
4. Безопасность: как не дать агенту удалить вашу
базу данных
Даже если ваш агент работает в песочнице, вопросы безопасности критичны —
особенно если он имеет доступ к браузеру или терминалу. В production-системах
безопасность строится по принципу Defense in Depth (глубокоэшелонированная
защита).
YOLO Classifier (ML-классификатор разрешений)
Вместо того чтобы спрашивать пользователя при каждом чихе (что убивает идею
автономности), используется двухэтапный классификатор на базе LLM:
1. Fast decision: Быстрый прогон через легковесную модель.
2. Thinking analysis: Если есть сомнения, модель пишет цепочку рассуждений
(chain of thought), оценивая риски команды.
Важный нюанс защиты от prompt injection: при анализе безопасности текст,
сгенерированный самим агентом-ассистентом, исключается из проверки.
Анализируется только сама команда и её аргументы. Каждый инструмент
реализует метод toAutoClassifierInput() , который возвращает компактное
представление для классификатора. Пустая строка '' означает “пропустить
этот инструмент в классификаторе” — это default.
Многоуровневая валидация (на примере FileEditTool)
Вот реальный pipeline валидации из кода. FileEditTool проходит 11 проверок до
того, как разрешить редактирование:
Влад Куклев | [Link]/prod1337
async validateInput(input, toolUseContext) {
// 1. Team memory secrets check
// 2. old_string === new_string → reject (бессмысленная правка)
// 3. Deny rule check (путь к файлу)
// 4. UNC path security (Windows NTLM credential theft)
// 5. File size check (1 GiB max)
// 6. File existence check
// 7. Previous read check (readFileState)
// 8. Modification timestamp check (файл изменён с момента чтения?)
// 9. old_string existence in file
// 10. Multiple matches without replace_all
// 11. Settings file validation
}
Проверка номер 7 — обязательное требование: модель не может редактировать
файл, не прочитав его. Это предотвращает слепые правки. Проверка номер 8
ловит ситуацию, когда файл был изменён внешним процессом между чтением и
записью.
Ещё одна неочевидная деталь: findActualString() обрабатывает
нормализацию кавычек — модель может отправить " вместо " или ' вместо
' , и система это корректно обработает.
Защита от TOCTOU (Time-of-Check to Time-of-Use)
Если агент просит выполнить команду cat [Link] , а вы её разрешаете, файл
[Link] может быть подменен симлинком на /etc/shadow в момент
выполнения.
Правильная валидация путей включает:
Разрешение всех симлинков до проверки.
Блокировку shell-экспансий (запрет конструкций вроде $() , , ${} ).
Проверку на case-insensitive файловых системах (macOS/Windows).
Запрет NTFS Alternate Data Streams ( [Link]:evil ).
Блокировку device-файлов ( /dev/zero , /dev/random , /dev/urandom ,
/dev/stdin и ещё 9 путей).
Влад Куклев | [Link]/prod1337
5. Как расширять агента: хуки и MCP
Чтобы ваш агент не превратился в монолитный неподдерживаемый кусок кода,
логику взаимодействия с внешним миром нужно выносить наружу.
Model Context Protocol (MCP)
MCP — это стандарт, который позволяет агентам подключаться к внешним
источникам данных (базам, API, локальным файлам) без написания кастомных
интеграций под каждый инструмент. В Claude Code поддерживается 8 типов
транспорта: stdio, SSE, HTTP (Streamable HTTP), WebSocket, SDK, In-Process, IDE-
specific, и [Link] proxy.
Подключение к серверу мемоизируется, чтобы не создавать дубликаты:
export const connectToServer = memoize(
async (name, serverRef, serverStats) => { ... },
(name, serverRef) => `${name}-${[Link](serverRef)}`
)
Каждое MCP-соединение проходит через 5 возможных состояний (discriminated
union):
type MCPServerConnection =
| ConnectedMCPServer // Рабочее соединение
| FailedMCPServer // Ошибка подключения
| NeedsAuthMCPServer // Требуется OAuth
| PendingMCPServer // В процессе подключения
| DisabledMCPServer // Отключен пользователем
Для stdio-серверов реализована трёхступенчатая эскалация завершения —
SIGINT (100ms ожидание) → SIGTERM (400ms) → SIGKILL . Общее время cleanup
ограничено 600ms, чтобы не блокировать CLI. Stderr буферизируется с cap в
64MB, чтобы verbose MCP-серверы не съели всю память.
Ещё одна деталь: при закрытии соединения очищаются все мемоизированные
кеши — tools, resources, commands, и само соединение:
Влад Куклев | [Link]/prod1337
[Link] = () => {
[Link](name)
[Link](name)
[Link](name)
[Link](key)
}
Система хуков
Вместо хардкода логики в основном цикле агента, используются хуки
(PreToolUse, PostToolUse, UserPromptSubmit, PermissionRequest, PreCompact,
PostCompact, SessionStart, SessionEnd, Stop, AgentSpawn).
Система поддерживает шесть типов хуков: command (shell), HTTP, prompt, agent,
callback и function. Самый универсальный — command hook. Его Zod-схема:
const BashCommandHookSchema = [Link]({
type: [Link]('command'),
command: [Link](),
if: IfConditionSchema(),
shell: [Link](['bash', 'powershell']).optional(),
timeout: [Link]().positive().optional(),
once: [Link]().optional(), // Одноразовый хук
async: [Link]().optional(), // Фоновое выполнение
asyncRewake: [Link]().optional(), // Фон + разбудить модель при exit 2
})
Протокол взаимодействия: программа получает JSON на stdin и возвращает
exit code:
0 — success (продолжить выполнение).
2 — blocking error (немедленно остановить операцию, передать stderr
модели).
Любой другой — non-blocking error (показать stderr пользователю,
продолжить).
На Windows bash-хуки выполняются через Git Bash, а не через [Link].
PowerShell-хуки используют pwsh с флагами -NoProfile -NonInteractive .
Влад Куклев | [Link]/prod1337
Хук получает набор переменных окружения: CLAUDE_PROJECT_DIR (корень
проекта), CLAUDE_PLUGIN_ROOT (корень плагина), CLAUDE_PLUGIN_DATA
(директория данных). Это позволяет подключать внешние скрипты (линтеры,
сканеры секретов, валидаторы) к пайплайну агента без изменения его исходного
кода.
Интересная оптимизация: когда все хуки — внутренние (callback/function, без
shell-вызовов), pipeline работает в fast path с задержкой около 1.8 микросекунд
вместо 6 микросекунд для shell-хуков.
6. Как ускорить запуск и работу: трюки из
production
Параллельная инициализация
При запуске агента критически важно минимизировать время до первого ответа.
В Claude Code используется паттерн “fire-and-forget void” — некритичные
операции запускаются параллельно без await :
// Параллельно, без ожидания:
void populateOAuthAccountInfoIfNeeded()
void initJetBrainsDetection()
void detectCurrentRepository()
// Синхронно, строго по порядку (критический путь):
configureGlobalMTLS() // MUST be before first TLS
configureGlobalAgents() // Depends on mTLS
preconnectAnthropicApi() // Fire-and-forget AFTER the above
Функция init() обёрнута в lodash-es/memoize — повторный вызов мгновенно
возвращает кешированный Promise.
Dead Code Elimination через Feature Gates
Для разных сборок (internal vs external) используется система compile-time
feature gates через виртуальный модуль bun:bundle :
Влад Куклев | [Link]/prod1337
import { feature } from 'bun:bundle'
// При сборке для external: feature('VOICE_MODE') → false
// Bun tree-shakes весь код внутри if (false) {...}
const VoiceProvider = feature('VOICE_MODE')
? require('../context/[Link]').VoiceProvider
: ({ children }) => children
Паттерн require(...) as typeof import(...) даёт полную типизацию при
compile-time elimination. Bun не только убирает код, но и строковые литералы
внутри мёртвых блоков не попадают в финальный бинарь — это критично для
security (скрытие внутренних кодовых названий экспериментов).
Zero-copy Stream
Для передачи данных от API к UI используется кастомный Stream<T> — single-
consumer async iterator с zero-copy оптимизацией:
class Stream<T> implements AsyncIterator<T> {
private readonly queue: T[] = []
private readResolve?: (value: IteratorResult<T>) => void
enqueue(value: T): void {
if ([Link]) {
// Consumer уже ждёт — передаём напрямую, без буфера
const resolve = [Link]
[Link] = undefined
resolve({ done: false, value })
} else {
[Link](value)
}
}
}
Если consumer уже ждёт (есть readResolve ), данные передаются напрямую без
промежуточного буфера. Ограничение: stream можно итерировать только один
раз — single-consumer design для предотвращения race conditions.
Влад Куклев | [Link]/prod1337
Deferred Flush для I/O
При записи логов и транскриптов используется createBufferedWriter() с
хитрой оптимизацией: при overflow буфера вместо синхронного writeFn
(который может быть appendFileSync ) вызывается setImmediate() . Это
предотвращает jank mid-render:
function flushDeferred(): void {
const detached = buffer
buffer = []
setImmediate(() => {
writeFn([Link]('')) // Async, не блокирует render
})
}
Заключение
Создание надёжных AI-агентов сильно отличается от написания скриптов-
обёрток вокруг OpenAI API. Главные вызовы кроются не в качестве промптов, а в
инженерии: управлении растущим контекстом, изоляции параллельных задач,
безопасном выполнении сгенерированного кода и обработке неизбежных
ошибок (rate limits, context window exceeded, hallucinations).
Используя паттерны вроде отложенной загрузки инструментов, многоуровневой
компактификации контекста и форкирования подагентов с общим кэшем, вы
сможете строить пайплайны, которые работают стабильно и потребляют
меньше токенов.
Влад Куклев | [Link]/prod1337