Современные AI-агенты строятся достаточно разнообразно. Где-то берут готовые приложения, которые выросли в полноценные фреймворки с преднастроенными агентами. Где-то провайдеры предлагают настройку из готовых блоков через UI на веб-портале. Где-то хочется написать своё: дешевле, в своём контуре или просто иначе. Один из популярных фреймворков для этого - LangChain / LangGraph: удобные обёртки, пайплайны, графы, интеграции под типичные куски агента.
В большинстве случаев агенты строятся вокруг базового цикла ReAct (Reasoning + Acting - рассуждение и действие): модель рассуждает, при необходимости вызывает инструмент (tool), смотрит результат и снова думает - пока не отдаст ответ. На короткой задаче этого хватает. На длинной - агент сам накапливает контекст: историю, результаты инструментов, планы, файлы. Вопрос смещается от «как написать промпт» к «как управлять всей этой средой». Этим занимается контекст-инжиниринг (context engineering). Глубокие агенты (Deep Agents) - готовая реализация этой идеи в экосистеме LangChain: не новый тип модели, а заранее собранная рабочая среда вокруг обычного агента (в API - create_deep_agent поверх create_agent).
В этой статье хочу разобрать, что стоит за словом глубокий (deep): какую проблему решает такая рабочая среда, из каких механизмов она собрана, какую цену за них приходится платить и когда этот «тяжёлый рюкзак» действительно стоит брать.
Немного истории как все развивалось.
Сначала появился LangChain: общий контракт Runnable и удобный способ собирать пайплайны, где выход одного шага становится входом следующего. От Runnable сделали много обёрток - компоненты можно менять, не переписывая всю цепочку. Такой подход хорошо подходил линейным задачам: прогнать последовательность вызовов и получить результат.
Например, шаблон сообщения, RAG (Retrieval-Augmented Generation), модель и разбор ответа (каждый оформляется как Runnable) склеиваются через | (пайплайн в LangChain):
from langchain.chat_models import init_chat_model
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
# FAQ-база поддержки: создать и заполнить статьями (наполнение не показываем)
retriever = ...
prompt = ChatPromptTemplate.from_template(
"Ты агент поддержки. Ответь кратко по FAQ.\n\nFAQ:\n{context}\n\nВопрос: {question}"
)
model = init_chat_model("qwen/qwen3.5-9b", model_provider="openai", base_url="http://localhost:1234/v1", api_key="x")
inputs = {"context": retriever, "question": RunnablePassthrough()}
chain = inputs | prompt | model | StrOutputParser()
print(chain.invoke("У меня не работает почта"))
Сменили модель, шаблон или способ доставать документы - в цепочке меняется один кусок, остальное можно не трогать. Для линейных сценариев («достал контекст → спросил модель → разобрал ответ») этого хватало, и большинство сценариев использования LLM в них прекрасно вписывались.
Линейных цепочек быстро стало мало: RAG всё чаще оформляют как опциональный инструмент (tool) поиска, инструментальные сценарии требуют цикла «модель ↔ инструменты (tools)», а множество ветвлений if/else сложно поддерживать. Появился LangGraph - граф состояний и переходов, на котором можно собрать почти любую конфигурацию. Но для множества задач нужен один и тот же ReAct-цикл, поэтому его сборку упростили до вызова готовой функции create_agent.

Простой агент с инструментом (tool) «сегодняшняя дата» - без него календарные вопросы просто не сработают:
from datetime import date
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from langchain_core.tools import tool
@tool
def today() -> str:
"""Вернуть сегодняшнюю дату в ISO-формате. Используй, когда нужен текущий день."""
return date.today().isoformat()
agent = create_agent(
model=init_chat_model("qwen/qwen3.5-9b", model_provider="openai", base_url="http://localhost:1234/v1", api_key="x"),
tools=[today],
system_prompt="Отвечай кратко. Используй tools, если это необходимо.",
)
# без функции today в tools модель физически не может знать какой день сегодня
# и посчитать количество дней до конца месяца
result = agent.invoke(
{
"messages": [
{
"role": "user",
"content": "Через сколько дней конец месяца?",
}
]
}
)
print(result["messages"][-1].content)
Простой ReAct-агент закрывает простые сценарии, но оказалось его можно серьезно улучшить, если вокруг нарастить обвязку, не меняя основную архитектуру: последовательность работы, общая память, общие файлы, субагенты, повторные попытки (retry) и проверки (guardrails). Чем сложнее агент, тем больше таких слоёв нужно стыковать - и часто кажется, что все решают одни и те же вопросы и что это должно быть проще.
В простых LLM-приложениях качество часто улучшали прежде всего через промпт: точнее формулировали роль, задачу, ограничения и формат ответа. Промпт по-прежнему важен - неясная инструкция останется неясной и внутри самого продуманного агента. Но перед очередным вызовом модель видит уже не только её: рядом лежат история разговора, описания инструментов, найденные документы, результаты предыдущих действий, рабочие заметки и память.
Если по-простому, промпт похож на записку сотруднику: что сделать и в каком виде отдать результат. Контекст - весь его рабочий стол. На нём могут лежать нужные документы и свежие цифры, а могут - устаревшая версия отчёта, вывод от другой задачи и двадцать похожих инструментов. Возникает резкий взлёт сложности написания записки, а чем она сложнее, тем сложнее ей пользоваться без ошибок; часть этой сложности можно убрать просто наведением порядка.
Для агента это особенно важно: ReAct-цикл после каждого хода сам производит будущий контекст. История сообщений пополняется вызовами инструментов (tool calls), их результатами, отчётами субагентов и неудачными попытками; отдельно накапливаются планы и рабочие файлы. На коротком запросе этот объём почти незаметен. На длинной задаче ранняя ошибка может закрепиться как факт, важная деталь - утонуть в истории, лишний инструмент - увести модель в сторону, а старая и новая версии данных - начать противоречить друг другу.
Работу со всей этой средой называют контекст-инжинирингом (context engineering). Инженерный вопрос теперь звучит шире, чем «как написать промпт»: что сохранить вне истории, что вернуть модели именно сейчас, что сократить и какую ветку работы вынести в отдельный контекст. В коротком агенте такая инфраструктура обычно избыточна. В длинном она помогает модели не захлебнуться информацией, которую агент накопил сам. Дальше - как Deep Agents собирает часть этих приёмов в готовую обвязку.
Технически это отдельная библиотека deepagents: репозиторий появился 2025-07-27, первая версия на PyPI 0.0.1 - 2025-07-29. Пакет по-прежнему pre-1.0 и живёт отдельно от LangGraph: в ядро (core) его не вливали.
По описанию авторов, deepagents - это готовая обвязка (harness) поверх create_agent: тот же цикл вызова инструментов (tool-calling loop) и среда выполнения LangGraph (runtime), а вокруг них заранее собраны промежуточные слои (middleware), инструменты (tools) и промпт-инструкции для длинных многошаговых задач - план, артефакты, разгрузка истории, отдельные ветки работы. Промежуточный слой (middleware) встраивается в этапы работы агента и добавляет поведение. Общие данные между шагами графа - состояние (state): сообщения (messages), пункты плана (todo), виртуальные файлы. Бэкенд (backend) определяет, где эти файлы лежат на самом деле - в состоянии текущего треда (thread), в постоянном хранилище (store) или на диске.
В базовой сборке create_deep_agent, даже без дополнительных параметров, уже добавлены конкретные механизмы для такой работы с контекстом:
планирование (write_todos);
файловые инструменты (tools), подключаемые бэкенды (pluggable backends) и выгрузка (offload) больших результатов;
субагент общего назначения по умолчанию и инструмент (tool) task;
суммаризация длинной истории и восстановление незакрытых вызовов инструментов (tool calls).
Остальные возможности включаются только при подходящей конфигурации:
навыки (skills) - через skills=, память (memory, AGENTS.md) - через memory=;
человек в цикле (human-in-the-loop, HITL) - через interrupt_on=;
execute - только с бэкендом-песочницей (sandbox backend);
промежуточный слой (middleware) для кэширования промпта (prompt caching) входит в стек, но реальные попадания в кэш (cache hits) даёт только на поддерживаемых моделях Anthropic и Bedrock.
Снаружи API намеренно похож на обычного агента - отсюда соблазн воспринять Deep Agents как волшебную кнопку «сделать умнее» и заменить один вызов другим. Минимальный пример действительно почти не показывает внутренней разницы:
from deepagents import create_deep_agent
from langchain.chat_models import init_chat_model
agent = create_deep_agent(
model=init_chat_model("qwen/qwen3.5-9b", model_provider="openai", base_url="http://localhost:1234/v1", api_key="x"),
tools=[...], # ваши доменные tools, если нужны
system_prompt="...",
)
Ниже разберём, что на самом деле меняется - тогда проще решить, нужно ли это. Выбор не бинарный: почти всё из Deep Agents - стандартные компоненты того же стека, и их можно подключать к обычному create_agent по частям.
Начнём с самого соблазнительного и неудачного сценария: возьмём короткого агента без собственных инструментов:
agent = create_agent(
model=init_chat_model("qwen/qwen3.5-9b", model_provider="openai", base_url="http://localhost:1234/v1", api_key="x"),
system_prompt="Ты полезный ассистент.",
)
state = agent.invoke({"messages": ["Объясни кратко ReAct-цикл в агентах"]})
print(state["messages"][-1].content)
В этом прогоне системный промпт (system prompt) занял 35 токенов. Теперь меняем только функцию создания агента на create_deep_agent:
agent = create_deep_agent(
model=init_chat_model("qwen/qwen3.5-9b", model_provider="openai", base_url="http://localhost:1234/v1", api_key="x"),
system_prompt="Ты полезный ассистент.",
)
state = agent.invoke({"messages": ["Объясни кратко ReAct-цикл в агентах"]})
print(state["messages"][-1].content)
Системный промпт вырос с 35 до 6215 токенов, а в схеме запроса появилось 8 инструментов: write_todos, шесть файловых инструментов и task для субагента.
Даже если модель не воспользуется ни одним инструментом, весь этот контекст уже отправлен ей на вход. В тарифицируемом API входные токены увеличивают стоимость, а небольшие модели ещё могут и запутаться в переданных инструкциях и инструментах (tools).
Посмотрим, откуда взялся такой рост - у него два источника. Сначала сам create_deep_agent добавляет базовые правила поведения. Затем подключённые промежуточные слои (middleware) дописывают свои инструкции и инструменты. Разберём их в этом же порядке.

Первый источник роста - константа BASE_AGENT_PROMPT из deepagents/graph.py. Она добавляется к вашему system_prompt независимо от того, какие возможности агента понадобятся в конкретном запросе.
Что она задаёт:
тон агента: коротко, без «Certainly!», меньше воды;
роль «сделай задачу до конца», а не «расскажи, как бы ты сделал»;
цикл понять → действовать → проверить (understand → act → verify), плюс «если застрял - остановись и спроси»;
как уточнять требования (минимум вопросов, разумные значения по умолчанию);
на длинных задачах - короткие отчёты о прогрессе (progress updates).
Этот блок задаёт общий характер глубокого агента: меньше разговоров о намерениях, больше самостоятельной работы до результата. На многошаговых задачах он помогает модели не остановиться после первого шага. На коротком вопросе или рядом с подробным пользовательским промптом те же правила дублируют уже заданное поведение и занимают контекст.
Оригинал:
BASE_AGENT_PROMPT - полный текст (43 строки)You are a deep agent, an AI assistant that helps users accomplish tasks using tools. You respond with text and tool calls. The user can see your responses and tool outputs in real time.
## Core Behavior
- Be concise and direct. Don't over-explain unless asked.
- NEVER add unnecessary preamble ("Sure!", "Great question!", "I'll now...").
- Don't say "I'll now do X" - just do it.
- If the request is underspecified, ask only the minimum followup needed to take the next useful action.
- If asked how to approach something, explain first, then act.
## Professional Objectivity
- Prioritize accuracy over validating the user's beliefs
- Disagree respectfully when the user is incorrect
- Avoid unnecessary superlatives, praise, or emotional validation
## Doing Tasks
When the user asks you to do something:
1. **Understand first** - read relevant files, check existing patterns. Quick but thorough - gather enough evidence to start, then iterate.
2. **Act** - implement the solution. Work quickly but accurately.
3. **Verify** - check your work against what was asked, not against your own output. Your first attempt is rarely correct - iterate.
Keep working until the task is fully complete. Don't stop partway and explain what you would do - just do it. Only yield back to the user when the task is done or you're genuinely blocked.
**When things go wrong:**
- If something fails repeatedly, stop and analyze *why* - don't keep retrying the same approach.
- If you're blocked, tell the user what's wrong and ask for guidance.
## Clarifying Requests
- Do not ask for details the user already supplied.
- Use reasonable defaults when the request clearly implies them.
- Prioritize missing semantics like content, delivery, detail level, or alert criteria.
- Avoid opening with a long explanation of tool, scheduling, or integration limitations when a concise blocking followup question would move the task forward.
- Ask domain-defining questions before implementation questions.
- For monitoring or alerting requests, ask what signals, thresholds, or conditions should trigger an alert.
## Progress Updates
For longer tasks, provide brief progress updates at reasonable intervals - a concise sentence recapping what you've done and what's next.
Базовый промпт объясняет только общие правила поведения, но не весь скачок до 6215 токенов и восьми инструментов. Остальное добавляет стек промежуточных слоёв (middleware). Чтобы дальше не воспринимать их как случайный список классов, сначала посмотрим, где именно они встраиваются в знакомый ReAct-цикл.
Deep Agents встраивает политики в уже знакомый цикл «модель ↔ инструменты», а не добавляет новый этап рассуждения. В терминологии LangChain такой код называется промежуточным слоем (middleware), а точки расширения делятся на два типа.
Точки расширения жизненного цикла (lifecycle hooks): before_* и after_*. Среда выполнения (runtime) вызывает их сама в определённый момент цикла:
before_agent - один раз перед началом запуска
before_model - перед каждым обращением к модели
after_model - после каждого ответа модели
after_agent - один раз перед завершением запуска
Эти методы получают текущие state и runtime. Они могут прочитать состояние и вернуть частичное обновление состояния (state) или None.
Обёртки (wrappers): wrap_model_call и wrap_tool_call. Они оборачивают саму операцию и получают request вместе с функцией handler. Вызов handler(request) запускает реальную модель или инструмент (tool), поэтому код до него выполняется до операции, а код после - после. Обёртка (wrapper) может изменить запрос (request) или результат (result), повторить вызов, обработать ошибку и даже вернуть собственный результат без запуска исходной операции.

У обоих типов есть асинхронные варианты (async) с тем же смыслом: среда выполнения (runtime) ожидает корутину (coroutine), а обёртка (wrapper) вызывает await handler(request).
Чтобы увидеть порядок на практике, берём простой промежуточный слой (middleware): точки расширения жизненного цикла (lifecycle hooks) печатают свои имена, а обёртки (wrappers) - сообщения до и после handler(request). Используем обычный create_agent: Deep Agents здесь ни при чём, так чище виден сам механизм промежуточных слоёв (middleware).
from langchain.agents import create_agent
from langchain.agents.middleware import AgentMiddleware
from langchain.chat_models import init_chat_model
from langchain_core.tools import tool
@tool
def count_letter(word: str, letter: str) -> int:
"""Сколько раз буква letter встречается в word (без учёта регистра)."""
return word.lower().count(letter.lower())
class PrintHooksMiddleware(AgentMiddleware):
def before_agent(self, state, runtime):
print("АГЕНТ: начало запуска (before_agent)")
return None
def before_model(self, state, runtime):
print("\nМОДЕЛЬ: начало вызова")
print(" hook before_model")
return None
def wrap_model_call(self, request, handler):
print(" wrapper: до handler(model)")
result = handler(request)
print(" wrapper: после handler(model)")
return result
def wrap_tool_call(self, request, handler):
tool_call = getattr(request, "tool_call", {})
print(f"\nИНСТРУМЕНТ: {tool_call.get('name')} {tool_call.get('args')}")
print(" wrapper: до handler(tool)")
result = handler(request)
content = getattr(result, "content", result)
print(f" wrapper: после handler(tool), result={content}")
return result
def after_model(self, state, runtime):
print(" hook after_model")
return None
def after_agent(self, state, runtime):
print("\nАГЕНТ: завершение запуска (after_agent)")
return None
agent = create_agent(
model=init_chat_model("qwen/qwen3.5-9b", model_provider="openai", base_url="http://localhost:1234/v1", api_key="x"),
tools=[count_letter],
system_prompt="Отвечай кратко.",
middleware=[PrintHooksMiddleware()],
)
state = agent.invoke(
{"messages": [{"role": "user", "content": "Сколько букв r в strawberry?"}]}
)
Пример прогона:
АГЕНТ: начало запуска (before_agent)
МОДЕЛЬ: начало вызова
hook before_model
wrapper: до handler(model)
wrapper: после handler(model)
hook after_model
ИНСТРУМЕНТ: count_letter {'word': 'strawberry', 'letter': 'r'}
wrapper: до handler(tool)
wrapper: после handler(tool), result=2
МОДЕЛЬ: начало вызова
hook before_model
wrapper: до handler(model)
wrapper: после handler(model)
hook after_model
АГЕНТ: завершение запуска (after_agent)
С промежуточными слоями (middleware) мы уходим от необходимости строить нестандартный граф выполнения: нужные действия можно выполнять вокруг уже существующих вызовов внутри ReAct-цикла - дополнить системный промпт (system prompt) или запрос к модели, добавить инструменты (tools), прочитать и обновить состояние (state), обернуть вызов модели или инструмента (tool).
Конкретные промежуточные слои превращают эти точки в политики работы: выгрузить большой результат из истории, остановиться перед опасным действием или разметить статическую часть запроса для кэша. В Deep Agents за этим стоят, например, FilesystemMiddleware, механизм HITL и слой кэширования промпта (prompt caching). Сама точка расширения (hook) ничего из этого не делает - она только даёт коду место встроиться в цикл.
Не каждый слой увеличивает системный промпт. TodoListMiddleware, FilesystemMiddleware и SubAgentMiddleware добавляют модели инструкции и инструменты. PatchToolCallsMiddleware работает с уже существующей историей, а SummarizationMiddleware вмешивается только после достижения порога контекста. Поэтому дальше для каждого компонента важно отдельно смотреть, что именно он приносит: постоянный текст, новый инструмент, поле состояния или обработку между вызовами.
Если вернуть общую картину Deep Agents, компоненты выполняют четыре вида работы с контекстом:
записывают снаружи истории сообщений (Write): todo, файлы и память фиксируют рабочие данные отдельно от траектории сообщений; способ вернуть их модели у каждого слоя свой;
выбирают нужное (Select): файловый поиск, чтение по диапазонам и навыки (skills) возвращают информацию по требованию;
сжимают накопившееся (Compress): выгрузка (offload) убирает тяжёлые результаты инструментов, а суммаризация сокращает длинную историю;
изолируют ветки (Isolate): субагенты получают отдельную историю и возвращают родителю итог без всей промежуточной траектории.
Это карта задач обвязки, а не новые этапы ReAct. Дальше - одна сквозная задача и несколько механизмов: для каждого - какую проблему он решает, что добавляет в LangChain и где заканчиваются гарантии.
На длинной задаче модель легко теряет нить: сделала два шага, забыла третий, перепрыгнула через проверку или «ответила» на середине. Человеку в такой ситуации помогает список задач со статусом выполнения. Именно такая функциональность и добавляется агенту.
TodoListMiddleware даёт агенту возможность оценить сложность задачи и при необходимости завести явный список шагов со статусами (pending / in_progress / completed) в состоянии (state): обновлять его по ходу, видеть что закрыто и что впереди, переписать план, если появились новые факты. На коротком «ответь в одно сообщение» слой обычно простаивает. Но без него сложные многоэтапные задачи могут провалиться, особенно если работу можно разбить между несколькими субагентами.
Слой добавляет:
кусок системного промпта (system prompt) - когда вести todo, когда обойтись без него, как закрывать шаги и как отдавать финальный ответ;
инструмент (tool) write_todos;
поле плана в состоянии (state) агента.
Встроенная инструкция советует заводить список только для сложной цели, обновлять статус сразу после каждого шага, не вызывать write_todos параллельно и не смешивать последнее обновление плана с финальным ответом. Решение всё равно остаётся за моделью: слой даёт инструмент и хранит состояние, а дисциплину его использования задаёт промпт. Для небольших локальных моделей встроенной подсказки часто мало - если сервису нужен обязательный рабочий процесс (workflow), его стоит явно закрепить в системном промпте (system prompt).
Полный исходный текст инструкции:
WRITE_TODOS_SYSTEM_PROMPT - полный текст (18 строк)## `write_todos`
You have access to the `write_todos` tool to help you manage and plan complex objectives.
Use this tool for complex objectives to ensure that you are tracking each necessary step.
This tool is very helpful for planning complex objectives, and for breaking down these larger complex objectives into smaller steps.
It is critical that you mark todos as completed as soon as you are done with a step. Do not batch up multiple steps before marking them as completed.
For simple objectives that only require a few steps, it is better to just complete the objective directly and NOT use this tool.
Writing todos takes time and tokens, use it when it is helpful for managing complex many-step problems! But not for simple few-step requests.
## Important To-Do List Usage Notes to Remember
- The `write_todos` tool should never be called multiple times in parallel.
- Don't be afraid to revise the To-Do list as you go. New information may reveal new tasks that need to be done, or old tasks that are irrelevant.
## Finishing a task
When you finish all work, write your final answer in the message AFTER your last `write_todos` call - not in the same turn as that call. Start the final message with the substantive content the user asked for - the data, computation, summary, or analysis. The user wants the result, not confirmation that the work is done.
В официальном примере nvidia_deep_agent эта дисциплина вынесена в блок Progress Tracking: после каждого шага агент должен обновить todo, а перед ответом - проверить, что план закрыт. Для нашего прогона достаточно коротких частей Workflow, Progress Tracking и Final Checklist.
Возьмём их и соберём обычный create_agent, добавив к нему только TodoListMiddleware:
from langchain.agents import create_agent
from langchain.agents.middleware import TodoListMiddleware
from langchain.chat_models import init_chat_model
TODO_SYSTEM_PROMPT = """
## Workflow
1. **Plan and Track**: Always break initial request into focused steps using `write_todos`.
2. Process each task one by one and update progress as you complete each step.
## Progress Tracking (REQUIRED)
You MUST invoke write_todos to update progress after completing each workflow step. Use status values: "pending", "in_progress", or "completed". Before returning, mark ALL tasks as "completed".
## Final Checklist
Before returning:
1. Invoke write_todos to mark ALL items as "completed"
2. Verify all aspects of the user's request are addressed
"""
model = init_chat_model(
"google/gemma-4-12b-qat",
model_provider="openai",
base_url="http://localhost:1234/v1",
api_key="x",
)
agent = create_agent(
model=model,
system_prompt=TODO_SYSTEM_PROMPT,
middleware=[TodoListMiddleware()],
)
result = agent.invoke(
{"messages": [{"role": "user", "content": "Напиши статью «Как найти работу в IT в 2026»"}]}
)
print(result.get("todos"))
print(result["messages"][-1].content)
На google/gemma-4-12b-qat агент вызвал write_todos пять раз и сам собрал план из пяти пунктов. Первые четыре закрыл; последний - «Написание структуры и текста статьи» - остался in_progress, а в чат уже ушла готовая статья. Типичная ловушка: финальный ответ и есть последний шаг, поэтому модель забывает ещё раз вызвать инструмент (tool) и поставить completed. Здесь видна граница промежуточного слоя (middleware): он даёт инструмент и хранит план в состоянии (state), но не блокирует ответ с незакрытым todo. Состояние плана после прогона:
[
{
"content": "Анализ трендов рынка IT на 2026 год (ИИ, автоматизация, дефицит кадров)",
"status": "completed"
},
{
"content": "Определение целевых ролей (разработчики, аналитики, менеджеры, AI-специалисты)",
"status": "completed"
},
{
"content": "Составление плана обучения и развития навыков (Hard & Soft Skills)",
"status": "completed"
},
{
"content": "Подготовка портфолио и стратегии поиска (Networking, GitHub, LinkedIn)",
"status": "completed"
},
{
"content": "Написание структуры и текста статьи",
"status": "in_progress"
}
]
Если завершённость списка задач (todos) важна для логики сервиса, проверяйте result["todos"] в коде и отдельно тестируйте дисциплину на выбранной модели. Более строгий промпт (prompt) может снизить число таких случаев, но сам по себе гарантии не даёт.
План отвечает на вопрос «что делать дальше», но длинной работе ещё нужно место для черновиков и крупных результатов. Следующий слой - рабочая поверхность и разгрузка истории сообщений.
У файлов здесь три роли:
Артефакты - черновики, отчёты, промежуточные JSON, которые не обязаны оставаться в сообщениях (messages).
Выгрузка (offload) - большой текстовый результат инструмента (tool result) не занимает целиком историю сообщений: полный ответ уходит в бэкенд (backend), а модель получает короткое превью и путь к нему.
Общая площадка с субагентами: дочерний запуск может получить подготовленные файлы и вернуть через них артефакты, не забирая полную историю родителя.
В промпте слой требует читать файл перед изменением, сохранять стиль окружения, использовать абсолютные пути и читать большие файлы частями. Там же объясняется механизм крупных результатов: полный ответ инструмента может уйти в /large_tool_results/, после чего агент должен дочитать его через read_file или найти фрагмент через grep.
Полный исходный текст инструкции:
FILESYSTEM_SYSTEM_PROMPT - полный текст (20 строк)## Following Conventions
- Read files before editing - understand existing content before making changes
- Mimic existing style, naming conventions, and patterns
## Filesystem Tools `ls`, `read_file`, `write_file`, `edit_file`, `glob`, `grep`
You have access to a filesystem which you can interact with using these tools.
All file paths must start with a /. Follow the tool docs for the available tools, and use pagination (offset/limit) when reading large files.
- ls: list files in a directory (requires absolute path)
- read_file: read a file from the filesystem
- write_file: write to a file in the filesystem
- edit_file: edit a file in the filesystem
- glob: find files matching a pattern (e.g., "**/*.py")
- grep: search for text within files
## Large Tool Results
When a tool result is too large, it may be offloaded into the filesystem instead of being returned inline. In those cases, use `read_file` to inspect the saved result in chunks, or use `grep` within `/large_tool_results/` if you need to search across offloaded tool results and do not know the exact file path. Offloaded tool results are stored under `/large_tool_results/<tool_call_id>`.
Выгрузка (offload) происходит между вызовом инструмента (tool) и добавлением результата в историю. В deepagents 0.6.12 порог по умолчанию - примерно 20 000 токенов: размер оценивается приближённо, из расчёта четыре символа на токен. Пока ответ меньше, в ToolMessage остаётся исходный текст. Если порог превышен, FilesystemMiddleware делает три шага:
записывает полный текст в бэкенд (backend) по пути /large_tool_results/<tool_call_id>;
заменяет содержимое ToolMessage на превью из начала и конца ответа;
добавляет туда же путь к полному результату.
Следующий вызов модели видит достаточно, чтобы понять характер ответа, но не получает весь текст в сообщениях (messages). Если нужна конкретная деталь, агент читает файл частями через read_file; если неизвестно, в каком выгруженном результате она лежит, ищет по /large_tool_results/ через grep. Сам результат не пропадает - меняется только способ, которым он попадает в рабочий контекст.
Порог настраивается параметром tool_token_limit_before_evict у FilesystemMiddleware. При стандартном StateBackend выгруженный файл остаётся виртуальным полем состояния (state), а не появляется на диске сервера: файловая система по умолчанию - не физический диск, а поверхность, которую реализует выбранный бэкенд (backend). Для модели интерфейс не меняется (read_file, write_file и остальные файловые инструменты). Для продукта бэкенд определяет, где данные лежат, сколько живут и кто ещё может их увидеть.
Перед прогоном соберём варианты в одну таблицу:
Бэкенд (backend) | Где и сколько живут файлы | Когда нужен |
|---|---|---|
| В состоянии (state) текущего треда (thread) | Временные артефакты и большинство учебных прогонов |
| На реальном диске | Файлы должны быть видны людям, CI или другим программам |
| Разные пути направляются в разные бэкенды (backends) | Нужно совместить временную рабочую область и узкие постоянные каталоги |
| В LangGraph Store между тредами (threads) | Долгая память и общие файлы между разговорами |
По умолчанию «файлы» лежат в состоянии (state) агента (state["files"]), а не на диске. Состояние - общий объект прогона LangGraph: сообщения (messages), пункты плана (todos), файлы и другие поля. В контекст модели файлы сами по себе не попадают - модель обращается к ним через инструменты (tools), а прикладной код может читать и менять эти поля рядом с чатом. Закончился тред (thread) - виртуальные файлы обычно ушли вместе с ним.
Зачем вообще лезть на диск: артефакты должны пережить процесс и быть видны людям и другим инструментам вне агента. Типичный пример - агент по ходу работы копит знания и оформляет их в навыки (skills) (SKILL.md и соседние файлы в каталоге навыков (skills)): завтра другой прогон или другой сервис подхватит те же файлы с диска, а не из временного state["files"]. То же про выгрузку отчётов в репозиторий, правку кода в рабочем дереве, CI-артефакты.
Подключить реальный диск можно через FilesystemBackend. Здесь нужна осторожность с root_dir и режимом путей: при неаккуратной настройке агент уходит за пределы выделенной папки (.., абсолютные пути вроде /etc/passwd). Документация прямо предупреждает: без virtual_mode=True даже заданный root_dir не даёт защиты. Одно неосторожное действие плюс галлюцинация или инъекция в промпт (prompt injection) - и модель читает или пишет то, чего в зоне агента быть не должно. Для веб-приложений и API рекомендуют StateBackend, StoreBackend или песочницу (sandbox), а прямой доступ к диску оставляют для CLI и CI.
Для сервисов часто безопаснее не делать FilesystemBackend единственным бэкендом, а добавить диск узким маршрутом через CompositeBackend.
CompositeBackend - это микс нескольких бэкендов (backends): по умолчанию обычно виртуальный StateBackend, а к нему префиксы на другие бэкенды (backends) - то есть они «живут» в своей виртуальной папке. Навыки (skills) на диске выглядят так: черновики, выгрузка (offload) и /large_tool_results/ живут в состоянии (state); /skills/ (или /workspace/) маршрутизируется на FilesystemBackend с узким root_dir и virtual_mode=True. Остальные пути остаются виртуальными - сбежать «куда угодно на сервере» через /etc/... некуда: запрос уйдёт в бэкенд по умолчанию, где секретов нет (если вы сами их туда не положили).
Почему это лучше, чем просто FilesystemBackend: если сделать диск единственным бэкендом, весь файловый интерфейс начинает опираться на реальный диск, и любой промах в путях превращается в широкий риск. CompositeBackend оставляет реальный диск только там, где он реально нужен (например, в /skills/), а остальное держит виртуальным через StateBackend.
Минимальный пример маршрута навыков (skills):
from deepagents.backends import CompositeBackend, FilesystemBackend, StateBackend
backend = CompositeBackend(
default=StateBackend(),
routes={
"/skills/": FilesystemBackend(
root_dir="/srv/agent-skills",
virtual_mode=True,
),
},
)
Безопасность даёт связка маршрутизация + virtual_mode на дисковом куске, а не магия одного класса.

StoreBackend кладёт ту же «файловую» абстракцию не в состоянии (state) прогона, а в LangGraph Store (BaseStore): пространства имён, сохранение между разговорами и тредами (threads). Для агента инструменты (tools) те же (ls / read_file / …), меняется срок жизни артефактов. Типичный паттерн - снова CompositeBackend: черновики и /large_tool_results/ живут временно в состоянии (state), а /memories/ (предпочтения, долгие заметки, выученные факты) - в хранилище (store). Слой памяти (memory) в Deep Agents часто опирается именно на такую связку: то, что должно помниться завтра, записывается в хранилище, а не остаётся в сообщениях (messages).
В прогоне ниже берём по умолчанию StateBackend: цель - показать контракт «задача и план лежат файлами», а не сохранение (persistence) или доступ к реальному диску. Пример остаётся безопасным и не требует дополнительной инфраструктуры.
Развиваем предыдущий пример с планом. write_todos ведёт статусы в состоянии (state); файловая система рядом фиксирует сжатый контракт для следующих шагов: исходный запрос → /task.md, читаемый план → /plan.md, результат → /article.md. Этот контракт пригодится дальше: субагент сможет взять готовые /task.md и /plan.md, не перенося в свой контекст весь диалог согласования.
Пойдём той же дорогой: собираем create_agent с нужными промежуточными слоями (middleware) вместо полного create_deep_agent. Ниже - прежняя модель и todo-инструкции, в коде только добавка файлового слоя:
from deepagents.middleware.filesystem import FilesystemMiddleware
# model, create_agent и TodoListMiddleware определены в предыдущем примере.
# К прежнему TODO_SYSTEM_PROMPT добавляем только правила работы с файлами.
FILESYSTEM_RULES = """
## Filesystem workflow
1. Persist context to the filesystem (StateBackend paths start with `/`):
- Write the original user request to `/task.md`
- After the plan exists, write a readable copy of the plan to `/plan.md`
2. Process each todo one by one and update progress via `write_todos`.
3. Write the final article to `/article.md` (Russian, at most 5000 characters).
4. Before returning, mark ALL todos as `completed`, then give a short final message
that only confirms paths `/task.md`, `/plan.md`, `/article.md` - do not paste the
full article into chat (it must live in the file).
"""
agent = create_agent(
model=model,
system_prompt=TODO_SYSTEM_PROMPT + FILESYSTEM_RULES,
middleware=[TodoListMiddleware(), FilesystemMiddleware()],
)
# Запрос тот же, что в первом прогоне; меняется только собранная обвязка.
result = agent.invoke(
{"messages": [{"role": "user", "content": "Напиши статью «Как найти работу в IT в 2026»"}]}
)
print(result.get("todos"))
print(sorted((result.get("files") or {}).keys()))
print(result["messages"][-1].content)
На google/gemma-4-12b-qat: 6× write_todos, 3× write_file, все пять пунктов плана completed, в состоянии (state) лежат нужные пути. Финал в чате короткий - статья в файле, не в сообщениях (messages). Ниже список путей и превью содержимого:
files keys: ['/article.md', '/plan.md', '/task.md']
/task.md:
Как найти работу в IT в 2026
/plan.md:
# План статьи «Как найти работу в IT в 2026»
1. **Введение**
- Текущее состояние рынка (перенасыщенность Junior-позициями, рост AI).
- Основная идея: от «просто кодить» к «решать задачи бизнеса с помощью технологий».
2. **Ключевые тренды рынка к 2026 году**
- **AI-Native разработка**: ИИ как стандартный инструмент (Copilot, Cursor, автоматизация тестов).
- **Смещение фокуса на Soft Skills**:
...
/article.md:
... большая статья лежит тут
Теперь план и итоговые артефакты отделены от истории чата. Следующий шаг - отделить и саму тяжёлую работу: оставить родителя координатором, а отдельный фрагмент задачи выполнить в новом контексте.
Официальная формулировка сильная и конкретная: субагенты (subagents) решают проблему раздувания контекста (context bloat). Тяжёлая многошаговая работа выполняется в изолированном контексте; родитель через инструмент (tool) task получает один итоговый отчёт, а не всю последовательность шагов. Слой добавляет инструкцию о делегировании в промпте (prompt), сам инструмент task и каталог доступных агентов.
Архитектурно это схема «оркестратор + исполнители»: дочерний ReAct живёт в отдельной истории. В базовой сборке в каталоге есть general-purpose. Инструкция предлагает передавать изолированные многошаговые задачи и работу с тяжёлым контекстом; не использовать task для тривиальных действий и случаев, где родителю нужны промежуточные шаги; независимые вызовы по возможности отправлять параллельно.
Полный исходный текст инструкции и каталога:
TASK_SYSTEM_PROMPT - полный текст (31 строка)## `task` (subagent spawner)
You have access to a `task` tool to launch short-lived subagents that handle isolated tasks. These agents are ephemeral - they live only for the duration of the task and return a single result.
When to use the task tool:
- When a task is complex and multi-step, and can be fully delegated in isolation
- When a task is independent of other tasks and can run in parallel
- When a task requires focused reasoning or heavy token/context usage that would bloat the orchestrator thread
- When sandboxing improves reliability (e.g. code execution, structured searches, data formatting)
- When you only care about the output of the subagent, and not the intermediate steps (ex. performing a lot of research and then returned a synthesized report, performing a series of computations or lookups to achieve a concise, relevant answer.)
Subagent lifecycle:
1. **Spawn** → Provide clear role, instructions, and expected output
2. **Run** → The subagent completes the task autonomously
3. **Return** → The subagent provides a single structured result
4. **Reconcile** → Incorporate or synthesize the result into the main thread
When NOT to use the task tool:
- If you need to see the intermediate reasoning or steps after the subagent has completed (the task tool hides them)
- If the task is trivial (a few tool calls or simple lookup)
- If delegating does not reduce token usage, complexity, or context switching
- If splitting would add latency without benefit
## Important Task Tool Usage Notes to Remember
- Whenever possible, parallelize the work that you do. This is true for both tool_calls, and for tasks. Whenever you have independent steps to complete - make tool_calls, or kick off tasks (subagents) in parallel to accomplish them faster. This saves time for the user, which is incredibly important.
- Remember to use the `task` tool to silo independent tasks within a multi-part objective.
- You should use the `task` tool whenever you have a complex task that will take multiple steps, and is independent from other tasks that the agent needs to complete. These agents are highly competent and efficient.
Available subagent types:
- general-purpose: General-purpose agent for researching complex questions, searching for files and content, and executing multi-step tasks. When you are searching for a keyword or file and are not confident that you will find the right match in the first few tries use this agent to perform the search for you. This agent has access to all tools as the main agent.
Здесь важно не перепутать изоляцию контекста с полностью пустым состоянием (state). Для дочернего запуска промежуточный слой заменяет историю одним сообщением - описанием задачи из вызова task - и не передаёт todo родителя. Остальные поля состояния, в том числе виртуальные файлы, доступны субагенту, а после завершения их изменения возвращаются родителю. Поэтому он может прочитать подготовленный /plan.md, записать артефакт и отдать наверх короткий отчёт без всего родительского диалога.
Остальные свойства механизма:
субагент работает до завершения и делает одну передачу результата (single handoff);
ему можно дать другую модель, инструменты (tools) и системный промпт (system prompt);
в полной сборке Deep Agents сам добавляет универсального субагента general-purpose, поэтому инструмент (tool) task доступен даже без ваших субагентов. Если делегирование не нужно, этот вариант по умолчанию можно отключить через профиль обвязки (harness profile).

Когда субагент заметно помогает
исследование/обход с десятками промежуточных результатов (поиск, чтение файлов, парсинг);
узкая роль («только сверка фактов», «только код-стиль») со своим промптом (prompt);
параллель независимых веток (два источника → два task → синтез у родителя);
родителю нужно остаться координатором и не утонуть в логе инструментов (tools).
Когда почти наверняка накладные расходы (overhead)
один-два простых вызова инструмента (tool call) без раздувания контекста;
вам критично, чтобы родитель видел промежуточные шаги как сообщения (messages);
модель слабая и уже путается в самом факте наличия task среди десятка инструментов (tools).
С параллельностью здесь два отдельных шага. Сначала модель и её API должны вернуть несколько вызовов task в одном AIMessage. Промпт (prompt) может попросить об этом, но не может гарантировать форму ответа. Если такая пачка tool_calls пришла, стандартный ToolNode запускает вызовы одновременно; если модель выдаёт по одному task в разных ходах, они выполняются последовательно. Сам task не превращает очередь из нескольких ходов в параллельную работу.
Продолжаем ту же статью «Как найти работу в IT в 2026». Раньше один агент и планировал, и писал текст (сначала в чат, потом в файлы). Теперь разводим роли так, чтобы на каждого агента была минимальная работа.
Как это должно работать.
Пользователь даёт одну фразу-задачу.
Родитель только оркестрирует: кладёт задачу в /task.md, короткий план (ровно 3 раздела) в write_todos и /plan.md, вызывает task → section-writer на каждый раздел, затем склеивает секции в /article.md и отвечает списком путей. Тела разделов он не сочиняет.
Каждый task поднимает нового section-writer: вместо сообщений (messages) родителя он получает только описание своей задачи. При этом файлы из состояния (state) доступны, поэтому исполнитель читает /task.md и /plan.md, пишет один /sections/NN.md и возвращает родителю одну строку с путём - не весь черновик. Разделы независимы, поэтому просим модель вернуть три вызова task в одном сообщении: только в этом случае среда выполнения (runtime) сможет запустить их параллельно.
При вызове task виртуальные файлы передаются в дочерний запуск (run), а после его завершения изменения мерджатся обратно в состояние родителя. Поэтому файлы становятся контрактом между изолированными контекстами сообщений (messages). Один экземпляр StateBackend ниже делает файловую конфигурацию явной; сам обмен происходит через состояние.
Ниже снова собираем обычный create_agent вручную, чтобы увидеть делегирование без остальной полной обвязки. Это тоньше, чем create_deep_agent(subagents=[...]): Deep Agents автоматически добавит дочернему стеку TodoListMiddleware, FilesystemMiddleware, SummarizationMiddleware и PatchToolCallsMiddleware. Наш section-writer намеренно проще - только файловый слой, без своих пользовательских инструментов: идея «одна роль → один файл», а не все возможности готовой сборки.
from deepagents.backends import StateBackend
from deepagents.middleware.subagents import SubAgentMiddleware
# model, create_agent, TodoListMiddleware и FilesystemMiddleware
# остаются из предыдущих двух примеров.
# Промпт родителя: только оркестрация, без текста разделов
PARENT_SYSTEM = """
You are an orchestrator only. Do not write article section bodies.
1. Write the user request to `/task.md` (short).
2. write_todos: exactly 3 section items + 1 assemble item; mirror the 3 sections in `/plan.md`.
3. After `/plan.md` exists, in ONE assistant turn call `task` three times in parallel
(three tool_calls in the same response), subagent_type=`section-writer`, for
`/sections/01.md`, `/sections/02.md`, `/sections/03.md`.
Each description: section title + output path + "read /task.md and /plan.md first".
Do not wait for one section before starting the others. Max 3 concurrent task calls.
4. Only after all three return, write `/article.md` by concatenating them (no new prose).
5. Mark all todos completed. Final user message: only the file paths.
"""
# Промпт субагента: один раздел → один файл
SECTION_WRITER_PROMPT = """
Write ONE Russian article section.
Read `/task.md` and `/plan.md`. Write only the requested section to the given
`/sections/NN.md` path (max 1500 characters). Do not write other sections.
Reply with one line confirming the path.
"""
# Одна конфигурация виртуальной ФС; сам handoff файлов идёт через state
backend = StateBackend()
# Описание субагента-исполнителя (его видит родитель в tool `task`)
section_writer = {
"name": "section-writer",
"description": (
"Writes a single article section to /sections/*.md. "
"Reads /task.md and /plan.md. Call once per section."
),
"system_prompt": SECTION_WRITER_PROMPT,
"model": model,
# свой FS middleware; файлы родителя придут в state дочернего запуска
"middleware": [FilesystemMiddleware(backend=backend)],
"tools": [],
}
# Собираем родителя: план + файлы + возможность звать субагентов
agent = create_agent(
model=model,
system_prompt=PARENT_SYSTEM,
middleware=[
TodoListMiddleware(), # план в state
FilesystemMiddleware(backend=backend), # /task.md, /plan.md, сборка
SubAgentMiddleware(backend=backend, subagents=[section_writer]), # tool task
],
)
# Запрос снова тот же; теперь меняются роли и маршрут выполнения.
result = agent.invoke(
{"messages": [{"role": "user", "content": "Напиши статью «Как найти работу в IT в 2026»"}]}
)
# Смотрим план, файлы и короткий финальный ответ
print(result.get("todos"))
print(sorted((result.get("files") or {}).keys()))
print(result["messages"][-1].content)
Что получилось на прогоне (google/gemma-4-12b-qat). Файлы и план на месте, список задач (todos) закрыт. В промпте (prompt) просили три task:
до делегирования: write_todos, write_file, write_todos, write_file, write_todos
ход делегирования 1: task # /sections/01.md
ход делегирования 2: task # /sections/02.md
ход делегирования 3: task # /sections/03.md
после делегирования: read_file, read_file, read_file, write_file, write_todos
todos: 4/4 completed
files: /task.md, /plan.md, /sections/01.md, /sections/02.md, /sections/03.md, /article.md
Вывод. Схема «родитель планирует и назначает → исполнитель пишет один артефакт → родитель собирает» работает.
Интересный момент. Субагенты сейчас - большой тренд, но реализация часто проста: родитель и дочерний исполнитель - обычные ReAct-агенты, а для родителя дочерний выглядит как инструмент (tool) task. Новизна Deep Agents - в готовом контракте делегирования и изоляции контекста, а не в отдельной среде выполнения.
С делегированием разобрались. Следующий слой полной сборки чинит историю, если шаг оборвался посередине.
У длинного агента запуск может оборваться между решением модели вызвать инструмент (tool) и появлением ответа инструмента (tool result): пришло новое сообщение, выполнение отменили или аргументы оказались повреждены. Тогда в истории остаётся незакрытый вызов, а следующий запрос к модели может быть отклонён из-за нарушенной последовательности сообщений.
Здесь недостаточно попросить модель быть аккуратнее: повреждён уже сам протокол сообщений. PatchToolCallsMiddleware перед новым запуском находит такие вызовы и добавляет для каждого служебный ToolMessage: вызов был отменён либо не выполнен из-за некорректных аргументов. Слой не добавляет модели новый инструмент и не раздувает постоянный системный промпт - он чинит историю на уровне среды выполнения (runtime), чтобы ReAct-цикл мог продолжиться. Deep Agents подключает эту страховку по умолчанию.
Даже после выгрузки артефактов в файлы и изоляции тяжёлых веток у агента продолжает расти сама лента сообщений. Рано или поздно она упирается в окно модели. Здесь важно отделить готовую возможность LangChain от того, что добавляет Deep Agents. В LangChain уже есть базовый SummarizationMiddleware: он сжимает старые сообщения. Deep Agents использует расширенную версию этого слоя для длительных агентов, работающих с файлами: сохраняет вытесненную историю в бэкенд (backend), оставляет исходный state["messages"] нетронутым, автоматически подбирает пороги под модель и умеет повторить запрос после переполнения контекста.
В отличие от планирования, файлов и субагентов, этот слой не держит в основном системном промпте постоянную инструкцию «как им пользоваться». У него есть отдельный summary_prompt, который отправляется модели-суммаризатору только при достижении порога. После сжатия основная модель видит уже готовую сводку и свежий хвост сообщений.
Как настроить. В create_deep_agent слой уже включён: отдельно добавлять его не нужно. Если нужны другие пороги, сначала исключите стандартный SummarizationMiddleware через профиль обвязки (harness profile), затем передайте настроенный экземпляр в middleware=. Два промежуточных слоя (middleware) с одинаковым .name не заменяют друг друга - такая сборка завершится ошибкой о дубликате.
Ниже только настройка, без длинного прогона:
from deepagents import (
HarnessProfile,
create_deep_agent,
register_harness_profile,
)
from deepagents.backends import StateBackend
from deepagents.middleware.summarization import SummarizationMiddleware
from langchain.chat_models import init_chat_model
model = init_chat_model(
"qwen/qwen3.5-9b",
model_provider="openai",
base_url="http://localhost:1234/v1",
api_key="x",
)
backend = StateBackend()
# Убираем стандартный слой только для этой модели.
register_harness_profile(
"openai:qwen/qwen3.5-9b",
HarnessProfile(
excluded_middleware=frozenset({"SummarizationMiddleware"}),
),
)
summarization = SummarizationMiddleware(
model=model,
backend=backend,
trigger=("tokens", 20_000), # когда начинать сжатие
keep=("messages", 6), # сколько последних сообщений оставить как есть
trim_tokens_to_summarize=12_000, # сколько старой истории отдать суммаризатору
)
agent = create_deep_agent(
model=model,
backend=backend,
middleware=[summarization],
)
Когда рабочая история вырастет примерно до 20 000 токенов, перед следующим вызовом основной модели промежуточный слой (middleware) соберёт старую часть разговора в структурированную сводку (summary), а последние шесть сообщений оставит без изменений. В контексте модели дальше будут сводка и свежий хвост, но канонический state["messages"] не переписывается. Вытесненная история дописывается в бэкенд (backend) по пути /conversation_history/{thread_id}.md: если позже понадобится исходная деталь, агент сможет снова прочитать файл через read_file или найти её через grep.

У стандартного слоя пороги выбираются автоматически. Если профиль модели содержит max_input_tokens, сжатие начинается примерно на 85% окна, а около 10% сохраняется свежим хвостом. Если размер окна неизвестен, в deepagents 0.6.12 используются фиксированные значения: trigger на 170 000 токенов и последние шесть сообщений без сжатия. Механика и остальные настройки - в Context engineering.
На коротких сценариях слой почти не заметен. В длинной сессии он определяет, сможет ли агент продолжить работу через несколько часов: старая история превращается в компактный рабочий контекст, но остаётся доступной в бэкенде (backend).
Основной стек на этом собран. Навыки (skills) и память (memory) - два опциональных промежуточных слоя (middleware), которые отвечают уже не за ход работы, а за то, что модель знает и помнит. Они появляются только при skills= / memory= и по-разному загружают инструкции:
навыки (skills) подгружаются по задаче - это знакомая многим концепция SKILL.md;
память (memory) всегда остаётся фоном в системном промпте (system prompt) - это знакомая многим концепция AGENTS.md.
Навыки - это постепенное раскрытие (progressive disclosure) для инструкций: в каждом запросе модель видит только короткий каталог, а полный рецепт читает через файловые инструменты (tools), когда задача похожа на соответствующий сценарий. Например, один и тот же агент пишет аналитические отчёты и пользовательские инструкции: для отчёта нужны источники и ограничения, для инструкции - шаги и проверка результата. Каждый рецепт - отдельный навык (skill), без складывания всех правил в общий системный промпт (system prompt).
Возьмём два навыка (skills) на диске.
./agent-data/skills/analytical-report/SKILL.md:
---
name: analytical-report
description: Используй для аналитических отчётов и сравнений.
---
Сначала собери факты и источники. Затем дай краткий вывод,
основные аргументы, ограничения данных и рекомендации.
./agent-data/skills/user-guide/SKILL.md:
---
name: user-guide
description: Используй для инструкций и руководств пользователя.
---
Укажи предпосылки, разбей процесс на проверяемые шаги,
добавь ожидаемый результат и частые ошибки.
Подключаем каталог навыков через CompositeBackend и передаём путь в skills=:
from deepagents import create_deep_agent
from deepagents.backends import CompositeBackend, FilesystemBackend, StateBackend
backend = CompositeBackend(
default=StateBackend(),
routes={
"/skills/": FilesystemBackend(
root_dir="./agent-data/skills",
virtual_mode=True,
),
},
)
agent = create_deep_agent(
model=model,
backend=backend,
skills=["/skills/"], # каталог внутри маршрута /skills/
)
result = agent.invoke(
{"messages": [{"role": "user", "content": "Подготовь инструкцию по настройке почты"}]}
)
На старте в системный промпт (system prompt) попадут только описания analytical-report и user-guide. По запросу про настройку почты модель может выбрать user-guide, прочитать полный файл с диска и применить его структуру. Спецификация формата - в Skills.
Память (memory) нужна для правил и предпочтений, которые важны в каждом разговоре: стиля ответов, договорённостей репозитория, запрета коммитить .env, тона общения с пользователем. Это не сценарий «как сделать X», а фон, без которого агент каждый раз начинает с нуля.
Для подключения готовят AGENTS.md - или несколько таких путей - и передают их в memory= при создании агента. Содержимое живёт в бэкенде (backend). Подробности - в Memory.
Главное отличие от навыков (skills): память (memory) всегда подмешивается в системный промпт (system prompt), а не загружается по необходимости. Слой добавляет и содержимое AGENTS.md, и постоянные правила о том, как проверять и обновлять память, - поэтому memory= само по себе утяжеляет обвязку, а файл стоит держать коротким. Длинные процедуры лучше оставить в навыках (skills).
MemoryMiddleware сам не делает файл вечным: он загружает AGENTS.md из выбранного бэкенда. Со стандартным StateBackend память живёт только в текущем треде (thread); чтобы новый тред увидел обновления, нужен постоянный StoreBackend, FilesystemBackend или маршрут CompositeBackend. Уже запущенный тред может продолжать снимок с начала запуска - запись в файл не обновляет промпт мгновенно.
По токенам содержимое AGENTS.md похоже на тот же текст в system_prompt, плюс фиксированная обвязка слоя. Практический смысл другой: файл можно менять без правки кода, переиспользовать между агентами и - с постоянным бэкендом - между разговорами. В system_prompt= остаются роль и задача текущего вызова; в памяти - правила, которые должны жить дольше. Для одноразового скрипта это избыточно; для продуктового или командного агента по репозиторию - подходящее место для того, что нельзя забывать.
Мы разобрали, как Deep Agents наращивает возможности вокруг того же ReAct-цикла. Теперь остаётся практический вопрос: какой объём контекстной инфраструктуры оправдан задачей и где дополнительная мощность превращается в лишнюю сложность.
1. Найдите баланс, насколько глубоким вам нужен агент.
На одном краю - базовый ReAct через create_agent: только ваши инструменты (tools), свой короткий промпт (prompt), предсказуемая отладка. На другом - create_deep_agent с большой обвязкой (harness): todo, файлы, субагенты, суммаризация и гигиена истории. Навыки и память - отдельно, когда нужны. Есть и путь посередине: к create_agent подключить только нужные промежуточные слои (middleware), а не «весь рюкзак на всякий случай».
Перед добавлением слоя полезно понять его работу: какой шаг должен пережить следующий вызов модели, какой артефакт вынести из сообщений (messages), какую часть задачи изолировать, где нужна гарантия среды выполнения (runtime). Если ответа нет - слой пока лишний. Один и тот же FilesystemMiddleware будет лишним в FAQ-боте и необходимым в агенте, который собирает отчёт из десятка источников.
Чем слабее модель, тем тяжелее ей длинный системный промпт (system prompt), меню из многих инструментов (tools) и сложная многошаговая логика. Путаница в инструментах или срыв последовательности - признак, что стек для неё слишком тяжёл.
2. Кэширование промпта (prompt caching) (Anthropic / Bedrock).
В глубоких агентах системный промпт (system prompt), как правило, получается большим. Для Anthropic и Amazon Bedrock create_deep_agent помечает его статический префикс как подходящий для кэша (cache-eligible) (prompt caching): провайдер может переиспользовать повторяющиеся инструкции, снижая задержку (latency) и стоимость длинной сессии. Промежуточный слой (middleware) уже входит в стек, но реальный кэш появляется только на поддерживаемых моделях. У каждого провайдера есть минимальная длина префикса - часто порядка 1-4 тыс. токенов. Более короткий запрос выполнится как обычно, просто без попадания в кэш. На остальных провайдерах промежуточный слой остаётся пустой операцией (no-op).
3. Человек в цикле (HITL) - пауза перед опасным действием.
Deep Agents не делает все действия безопасными автоматически, но позволяет встроить подтверждение человека прямо в цикл агента. В interrupt_on перечисляются инструменты (tools), которые нельзя выполнять сразу:
agent = create_deep_agent(
model=model,
interrupt_on={
"write_file": True,
"edit_file": True,
},
)
Если модель решит вызвать один из них, граф остановится до выполнения и покажет человеку имя инструмента (tool) и аргументы. Вызов можно подтвердить, отклонить или поправить, после чего агент продолжит тот же запуск (run). То есть это не ещё одна инструкция в промпте (prompt), которую модель может проигнорировать, а настоящая пауза на уровне среды выполнения (runtime).
На учебных прогонах с StateBackend такая защита обычно избыточна: виртуальные файлы живут только в состоянии (state). Она становится важной, когда агент пишет на реальный диск, меняет данные во внешней системе или запускает другое действие с последствиями. В Deep Agents для этого не нужно вручную перестраивать граф - достаточно указать границу подтверждения при создании агента. Остальные варианты настройки оставим за рамками обзора; они есть в официальной документации.
4. Песочница (sandbox) / execute.
Выше мы больше говорили про файловую систему и её защиту через виртуальный бэкенд (backend) (StateBackend, узкие маршруты, virtual_mode). Но агентам всё чаще дают возможность не только читать и писать файлы, но и создавать и запускать код - от построения графика в matplotlib до развёртывания приложения и прогона тестов.
Запуск произвольного кода опасен для хоста: агент может выполнить опасную shell-команду, установить пакет, получить доступ к секретам или уронить процесс. Поэтому в Deep Agents выполнение обычно выносят в изолированную бэкенд-песочницу (sandbox backend) - например, у внешнего провайдера. Если бэкенд (backend) поддерживает протокол песочницы (sandbox protocol), Deep Agents добавляет инструмент (tool) execute, а файловые операции и команды проходят через одну изолированную среду. Детали и список интеграций есть в разделе Sandboxes.
Похожая возможность теперь есть и в обычном LangChain create_agent: ShellToolMiddleware с DockerExecutionPolicy запускает оболочку агента в отдельном Docker-контейнере для каждого запуска (run). Задача та же - отделить выполнение от хоста. Но точка интеграции другая: LangChain задаёт политику выполнения для shell-инструмента, а Deep Agents направляет файловые операции и execute через бэкенд-песочницу (sandbox backend).