Привет, на связи команда GigaChain! Мы занимаемся агентными системами и развиваем набор open source-решений для подключения агентов к GigaChat API.

LangChain развивается с 2022 года и выросла в одну из самых популярных экосистем для агентов: у основной библиотеки более 140 тысяч звёзд на GitHub. Благодаря LangGraph — движку, который исполняет агент как граф с состоянием, — с её помощью собирают и простые цепочки вызовов, и сложные автономные агенты, ведущие задачу на сотни шагов. Поверх этого стека команда LangChain выпустила Deep Agents — готовую агентную обвязку (agent harness): рабочую среду с файловой системой, оболочкой, планировщиком, субагентами и памятью, в которую остаётся лишь поместить модель.

Сегодня мы подробно разберём эту обвязку: из чего она состоит и как собрать на ней агента, работающего на GigaChat.

Из чего состоит обвязка

Год назад Claude Code задумывали как агент для программирования, а потом случилось неожиданное: люди начали натравливать его на всё подряд — исследование, разбор инцидентов, аналитику, черновики документов. Оказалось, ценна не только способность писать код, но и среда, в которую эта способность помещена: рабочее место с файлами и оболочкой, планировщик задач, субагенты, память. Именно этот слой инженерной обвязки вокруг одной модели и называют агентной обвязкой. Он превращает модель из собеседника в агента: тот ведёт задачу на десятки и сотни шагов, работает в реальной среде и оставляет после себя реальные артефакты — кодовую базу, отчёт с таблицами, презентацию, да что угодно, что можно потом открыть и проверить.

В основе любой обвязки лежит всё тот же агентный цикл: модель получает историю сообщений и на каждом шаге решает, что делать дальше — вызвать инструмент или дать финальный ответ. Запрос на вызов инструмента (tool calling) модель возвращает в структурированном виде, обвязка исполняет инструмент и кладёт результат обратно в историю — и так до решения задачи. Как собрать такого ReAct-агента, мы подробно разбирали: на LangGraph — в отдельной статье, а на LangChain v1, через новый create_agent, — в докладе на AI Journey. Этот цикл никуда не делся, он внутри каждой обвязки.

Проблема в том, что в чистом виде цикл плохо переносит длинные задачи. Всё состояние агента живёт в одном месте — в списке сообщений, — и при длинной задаче это оборачивается сразу несколькими проблемами:

  • Контекст переполняется. Каждый вызов инструмента добавляет в историю результат, иногда на десятки тысяч токенов. Довольно быстро окно контекста забивается промежуточным мусором, и модель «забывает» начало задачи. У этого есть имя — context rot: чем длиннее контекст, тем хуже модель достаёт из него нужное.

  • План теряется. План действий существует только в тексте рассуждений модели — нет структуры, которая напоминала бы, что сделано, а что осталось. Поэтому в обвязках план выносят из рассуждений в отдельный артефакт — структурированный список задач со статусами.

  • Инструкции не масштабируются. Все процедуры и исключения под узкие сценарии приходится складывать в системный промпт; он раздувается, качество следования падает. Отсюда возникли навыки (skills) с прогрессивным раскрытием: в промпте постоянно висит только оглавление — имя и описание навыка, а полная инструкция подгружается, лишь когда навык понадобится.

  • Нет среды. Конечно, можно прикрутить отдельный инструмент — сохранить файл, дёрнуть API, — но целостного рабочего места у агента нет: ни файловой системы, куда складывать результаты, ни оболочки для выполнения кода; промежуточный результат кочует от шага к шагу только через контекст. А готовая файловая система снимает разом и это, и переполнение контекста.

Обвязка — это системный ответ на все четыре проблемы сразу. Если разобрать её на компоненты — примерно так их группируют и сами Deep Agents, — то в типичной обвязке их пять:

  1. Среда исполнения: файловая система, оболочка, выполнение кода. Агент перестаёт быть «говорящей головой с API-вызовами» и получает рабочее место, где результаты труда складываются в артефакты.

  2. Управление контекстом: агрегирование истории, выгрузка больших результатов в файлы, подгрузка знаний по требованию. Окно контекста перестаёт быть узким местом.

  3. Планирование: структурированный список задач как артефакт, а не абзац в рассуждениях.

  4. Делегирование: субагенты с изолированным контекстом для тяжёлых подзадач.

  5. Контроль: точки, где человек одобряет или отклоняет опасные действия.

Всё это — инженерия вокруг модели, и у неё уже есть имя: harness engineering. Пять компонентов покрывают ядро обвязки, но не исчерпывают его: в более полных разборах компонентов ещё больше. Databricks, например, насчитывает восемь и добавляет к списку наблюдаемость и автоматическую проверку результата. Обе темы ещё встретятся: наблюдаемости посвящена отдельная глава, а к проверке результата мы вернёмся в последующих статьях.

Deep Agents: открытая обвязка

Deep Agents (пакет deepagents) — реализация обвязки от команды LangChain. Её место в стеке проще всего показать таблицей:

Слой

Что даёт

Deep Agents

Готовая обвязка: планирование, файловая система, субагенты, память, навыки.

LangChain

Агенты, инструменты, middleware API.

LangGraph

Среда исполнения графа: состояние, checkpointing, streaming, прерывания.

langchain-core

Сообщения, модели, инструменты, Runnable.

Читать стек удобно снизу вверх. В основании — langchain-core: общий словарь всей экосистемы (сообщения, модели, инструменты) и единый интерфейс Runnable, на котором всё держится. Над ним — LangGraph, среда исполнения, которая запускает агент как граф с состоянием. Это самый низкий уровень, где узлы, рёбра и сам цикл ты собираешь руками:

LangGraph за пять минут (если не сталкивались)

LangGraph описывает агент как граф состояний (StateGraph) из трёх сущностей: state — общая структура данных, которая течёт через весь граф и хранит всё накопленное (история сообщений, файлы, todos); узлы — функции-шаги (вызвать модель, выполнить инструмент); рёбра — переходы, причём условное ребро ветвит маршрут по состоянию.

from langgraph.graph import StateGraph, START, END

builder = StateGraph(State)
builder.add_node("model", call_model)
builder.add_node("tools", run_tools)
builder.add_edge(START, "model")
builder.add_conditional_edges("model", route)  # инструмент или выход
builder.add_edge("tools", "model")
graph = builder.compile(checkpointer=checkpointer)

LangGraph умеет сам нарисовать скомпилированный граф (graph.get_graph().draw_mermaid()):

Тот самый ReAct-цикл: модель либо зовёт инструмент и возвращается, либо выходит с ответом. Подробнее — в наших руководствах: ReAct-агент на LangGraph и агент на GigaChat + LangGraph от архитектуры до валидации.

Ещё выше — LangChain. Тут агент уже не нужно собирать из узлов: функция create_agent() возвращает готовый граф с ReAct-циклом — тем самым, что под спойлером, — в неё лишь передаёшь модель и инструменты. Чтобы вмешиваться в этот цикл, LangChain v1 даёт middleware — перехватчики, которые встраиваются между шагами агента и умеют проверить или подменить вызов инструмента, поправить промпт, изолировать контекст. Разбор механизма — в докладе «LangChain v1: укрощаем логику агентов через middleware». К самому middleware ещё вернёмся в конце, на нём построено почти всё, что делает Deep Agents.

На самом верху — Deep Agents. Сам по себе create_agent() — это только цикл; файловую систему, планировщик, субагенты и агрегирование пришлось бы городить поверх него вручную. Внутри себя create_deep_agent() вызывает тот же create_agent() и надстраивает над его циклом все компоненты обвязки из предыдущей главы, подключая их как middleware. Тебе остаётся только специализировать готовую обвязку содержимым: инструкциями, навыками, памятью. А LangGraph уже есть под капотом, так что при необходимости всегда можно уйти на уровень ниже.

У проекта две формы:

  • deepagents — библиотека (SDK) на Python: встраиваете агент в свой код, настраиваете модель, инструменты, бэкенд, память;

  • deepagents-code — готовый терминальный агент для программирования на той же обвязке, сделан по образцу Claude Code, но имеет открытый исходный код и работает с любой моделью.

В статье работаем с SDK — на нём виднее, как обвязка устроена изнутри.

Пройдёмся по этим компонентам, по тем же пяти, что и в главе «Из чего состоит обвязка»: среда исполнения, управление контекстом, планирование, делегирование, контроль. Теперь объясним на конкретных инструментах Deep Agents.

Среда исполнения: файловая система, бэкенды, shell

«Из коробки» deep-агент получает файловые инструменты: ls, read_file, write_file, edit_file, delete, glob, grep. Всё уже заточено под агент: read_file читает большие файлы порциями (offset и limit), edit_file правит точечно через замену строк, а grep и glob ищут по содержимому и по маске имён — писать это руками не нужно.

Важное уточнение: инструменты работают не напрямую с диском, а через бэкенд — подключаемую реализацию файловой системы. И бэкенд, и сами файловые инструменты даёт один и тот же FilesystemMiddleware. Подключать бэкенд вручную необязательно: не передашь ничего — встанет StateBackend из списка ниже, и инструменты продолжат работать как ни в чём не бывало, просто файлы лягут в состояние графа:

  • StateBackend (по умолчанию): виртуальные файлы, лежащие словарём (files) прямо в состоянии графа; на диск ничего не пишется, файлы живут в пределах одного треда. Знакомым с LangGraph: этот словарь чекпоинтится вместе со всем состоянием — с MemorySaver он, как и обычные чекпоинты, держится в памяти процесса.

  • FilesystemBackend: реальные файлы под заданным root_dir. С флагом virtual_mode=True пути нормализуются, и агент не может выйти за пределы корневой папки через .. или абсолютные пути.

  • LocalShellBackend расширяет FilesystemBackend инструментом execute: агент получает оболочку. Команды выполняются на вашей машине с рабочей директорией root_dir; в инструкции к execute агенту велено адресоваться абсолютными путями и не использовать cd, чтобы cwd не «плыла». При этом virtual_mode оболочку не ограничивает — команды видят реальную ФС целиком, так что подход годится только для доверенной локальной среды.

  • StoreBackend кладёт файлы в LangGraph Store: отдельное key-value-хранилище, общее для всех тредов агента (у StateBackend файлы заперты в одном треде, а тут доступны отовсюду). Раскладываются они по namespace — ключу-кортежу вроде (user_id, "filesystem"), что даёт каждому пользователю свою изолированную «папку». Долговечность зависит от подключённой реализации BaseStore: InMemoryStore держит данные в памяти, а реализация поверх Postgres — на диске, так что файлы переживают перезапуски. Отсюда и роль основы долговременной памяти.

  • CompositeBackend — маршрутизатор: разные пути виртуальной ФС ведут в разные бэкенды.

Представим агент, который готовит вам еженедельный дайджест по нужным темам. Ему нужны три разных хранилища с разным сроком жизни. Пока идёт один прогон, он тянет кучу источников и складывает сырые выдержки — после сборки дайджеста этот ворох не нужен. Между запусками агент должен помнить контекст: какие темы вам интересны, в каком тоне вы любите выжимку, что уже присылал на прошлой неделе. Сам дайджест — это файл, который вы откроете и прочитаете. CompositeBackend разводит все три по одной виртуальной файловой системе:

from deepagents import create_deep_agent
from deepagents.backends import (
    CompositeBackend, StateBackend, StoreBackend, FilesystemBackend,
)

agent = create_deep_agent(
    model=llm,
    backend=CompositeBackend(
        default=StateBackend(),                      # черновики прогона -- временно
        routes={
            "/memories/": StoreBackend(),            # память между запусками
            "/output/": FilesystemBackend(           # готовый дайджест -- на диск
                root_dir="./output", virtual_mode=True,
            ),
        },
    ),
)

В коде это три адреса: default — временный StateBackend под черновики прогона; /memories/ ведёт в StoreBackend, переживающий перезапуск, — та самая память между запусками; /output/ ведёт в FilesystemBackend, где дайджест становится настоящим файлом в папке ./output.

Маршрутизация для агента невидима: агент пишет по обычным путям, а CompositeBackend в начале пути раскладывает файлы по хранилищам. Какой путь для чего — агент знает из инструкций: их задают в системном промпте или в AGENTS.md (память разберём в следующей главе).

Список бэкендов этим не исчерпывается — свой можно написать поверх чего угодно, хоть S3. Для продакшена особенно важны sandbox-бэкенды: безопасная замена LocalShellBackend, где execute выполняется в изоляции. В Deep Agents своя песочница — это подкласс BaseSandbox с реализованным execute, а изолированное окружение под ним может быть любым. Готовые партнёрские пакеты (Modal, Daytona, Vercel) зарубежные, но в наших реалиях подойдёт и self-hosted Docker с усиленной изоляцией (gVisor, Firecracker-microVM), и одноразовые контейнеры в любом российском облаке с serverless-контейнерами или управляемым Kubernetes.

Управление контекстом: агрегирование, выгрузка, память, навыки

Окно контекста ограничено, и на длинной задаче возникает вопрос: «Что держать в нём сейчас, а что подгружать по требованию?» Управление контекстом — то, что в индустрии зовут context engineering, — отвечает на первую из проблем, с которых мы начинали: переполнение контекста. В Deep Agents оно складывается из четырёх механизмов:

  • Агрегирование. Когда история разрастается, старые сообщения автоматически сжимаются в резюме. Порог настраивается параметрами SummarizationMiddleware: trigger (когда сработать — по количеству токенов, сообщений или доле окна) и keep (сколько свежих сообщений не трогать). Доля берётся от окна конкретной модели: LangChain читает его размер из профиля модели (max_input_tokens), поэтому 0,85 для окна в 200 тыс. — это около 170 тыс. токенов, а для 32 тыс. —  около 27 тыс. Если профиля с размером окна у модели нет, то порог откатывается на фиксированные 170 тыс. токенов — для модели с меньшим окном его стоит задать вручную. Токены считаются приблизительно (count_tokens_approximately; можно подменить своим token_counter), точный токенизатор модели не нужен.

  • Выгрузка (offloading). Делает FilesystemMiddleware — тот, что раздаёт файловые инструменты: когда результат инструмента превышает порог (tool_token_limit_before_evict, по умолчанию 20 тыс. токенов; оценивается приблизительно — по длине текста, около 4 символов на токен), его тело уходит файлом в /large_tool_results/<tool_call_id>, а в контексте остаётся только превью (первые и последние 5 строк с пометкой, сколько пропущено в середине) и путь к файлу. Если понадобятся подробности, то агент дочитает файл через read_file по частям или поищет нужное через grep.

  • Память. У памяти два уровня. Небольшой необходимый контекст — инструкции и соглашения — задают параметром memory у create_deep_agent: MemoryMiddleware грузит перечисленные файлы (по стандарту agents.md, обычно AGENTS.md) в системный промпт при запуске, обёртывая их в блок <agent_memory>. Всё остальное — накопленные факты, заметки — живёт файлами в файловой системе (например, под /memories/ в StoreBackend) и подтягивается агентом по требованию через read_file; в промпт целиком оно не грузится. Пополняет память сам агент через edit_file; со StoreBackend она переживает перезапуски.

  • Навыки. Согласно стандарту agentskills.io это папки со SKILL.md-инструкциями и вспомогательными файлами. Работают по принципу прогрессивного раскрытия: в контексте постоянно живёт только короткий индекс — название, описание и путь к SKILL.md, — а полная инструкция подтягивается, только когда навык нужен. Это и есть тот механизм «специализации содержимым папки», о котором шла речь во введении. В deepagents этот индекс кладётся прямо в системный промпт (в других обвязках бывает иначе — в пользовательском промпте или в описании отдельного инструмента), а отдельного инструмента для чтения нет — агент берёт путь из индекса и открывает SKILL.md обычным read_file.

Планирование: write_todos

Инструмент write_todos (middleware TodoListMiddleware) позволяет агенту вести структурированный список задач со статусами pending, in_progress, completed — он живёт в состоянии графа. План становится артефактом: агент отмечает пункты по мере выполнения и после каждого шага видит, что сделано.

Несколько подробностей реализации. Во-первых, план пересоздаётся целиком: каждый вызов write_todos перезаписывает весь список, поэтому за один ход инструмент вызывается максимум раз (параллельные вызовы создавали бы неоднозначность): агент шлёт свежую версию списка с обновлёнными статусами. Во-вторых, прогресс отражается статусами: выполненную задачу помечают completed и оставляют в списке (готовые пункты менять нельзя), а удаляют из плана только то, что стало неактуальным. В-третьих, для небольших задач план не нужен: в описании инструмента прямо сказано не использовать write_todos, если задача укладывается в три простых шага, чтобы не разводить бюрократию на ровном месте.

Делегирование: субагенты

Некоторые подзадачи тяжёлые сами по себе: чтобы решить одну, агент выполняет десятки шагов, перебирает источники, читает большие файлы, заходит в тупики. Если крутить всё это в основном контексте, то он забивается промежуточным шумом и теряет главную нить задачи. Субагент — способ вынести такую подзадачу в сторону: дочерний агент перемалывает всю возню в своём отдельном контексте и отдаёт наверх только чистый результат. Это называют карантином контекста (context quarantine): тяжёлую работу изолируют, а в контекст родителя попадает лишь выжимка.

Технически главный агент получает инструмент task (middleware SubAgentMiddleware) и запускает через него субагент нужного типа. «Из коробки» есть один — general-purpose: те же инструменты и возможности, что у главного, только в чистом контексте; его берут даже ради одной тяжёлой подзадачи, чтобы не топить основную ветку в подробностях. Свои субагенты добавляют декларативно: имя, описание (по нему главный решает, кого звать), системный промпт, набор инструментов, при желании — своя модель (provider:model, например подешевле для рутины), свой middleware и собственный interrupt_on. Можно и наоборот — подключить как субагент готовый скомпилированный граф.

Пара важных деталей. Субагент живёт только на время задачи и общается с родителем одним финальным сообщением — ни переспросить, ни дописать нельзя, поэтому задание должно быть самодостаточным, с явным указанием, что вернуть. Его результат не виден пользователю, родитель сам оформляет из него ответ. Несколько субагентов можно запустить параллельно. И тонкость изоляции: у субагента изолирован контекст сообщений, а файловая система общая — он работает с тем же бэкендом, что и родитель, так что тяжёлые данные удобно передавать через файлы, а не тащить в отчёт.

Контроль: human-in-the-loop

Контроль обеспечивает HumanInTheLoopMiddleware поверх механизма прерываний LangGraph. Параметр interrupt_on указывает, перед какими инструментами агент обязан остановиться и дождаться человека:

agent = create_deep_agent(
    model=llm,
    backend=backend,
    interrupt_on={"execute": True},   # каждая shell-команда -- только с одобрения
    checkpointer=checkpointer,
)

Агент дойдёт до вызова execute, сохранит состояние в чекпоинт и остановится. Можно одобрить вызов, изменить аргументы или отклонить с комментарием — граф продолжит с того же места. Для агента с shell это не опция, а необходимость.

Под капотом: middleware

Прочитав про пять компонентов, вы наверняка заметили, что почти у каждого мы называли свой middleware — FilesystemMiddleware, TodoListMiddleware, SubAgentMiddleware и другие. Отойдём на шаг назад: всё это — один механизм, причём самого LangChain (мы отмечали его в ступени LangChain): deepagents им пользуется как есть. Middleware встраивают в граф агента через хуки двух типов: before_agent/before_model и after_model/after_agent — отдельные узлы вокруг вызова модели, и wrap_model_call/wrap_tool_call — обёртки самого вызова (узлов не создают, а перехватывают вызов модели или инструмента). create_deep_agent просто собирает готовый стек таких middleware поверх create_agent.

Один middleware не привязан к одному хуку, а может занимать сразу несколько (например, и вставлять узел before_model, и обёртывать вызов через wrap_model_call). Порядок в стеке важен: before_* выполняются сверху вниз по списку middleware, after_* — в обратном порядке, а wrap_* вкладываются луковицей: первый middleware обёртывает все остальные. Поэтому самые «внешние» проверки ставят в начало списка.

И в LangChain, и в Deep Agents уже есть целый набор готовых middleware — ограничение количества вызовов модели, повторы, фильтрация PII, выбор инструментов и другое; их каталог — в документации LangChain. Если нужного нет, то можно написать свой и поставить в стек рядом со встроенными.

Практика: агент действует в реальной среде

Компоненты разобрали, теперь соберём из них живого агента. На GigaChat поднимем агента-аналитика с доступом к файлам и оболочке: он читает данные с диска, пишет код, запускает его в bash, проверяет результат и собирает отчёт — тот самый воспроизводимый артефакт, о котором шла речь во вступлении.

Двигаться будем по нарастающей. Сначала поставим пакеты и запустим трассировку в Arize Phoenix, чтобы дальше видеть каждый шаг агента. Потом — минимальный агент, которому мы не даём ни одного своего инструмента (встроенные, из коробки, у него есть). Затем дадим ему файловую систему и оболочку и запустим на реальных данных. И под конец подключим навык.

Весь код этого раздела собран в воспроизводимый Jupyter-блокнот — он лежит в репозитории deepagents-gigachat, в папке examples/sales-analyst. В статье разберём его по частям.

Установка

Начнём с окружения. Понадобятся Python 3.12+ и три пакета: библиотека обвязки deepagents, интеграция GigaChat с LangChain langchain-gigachat и профиль deepagents-gigachat, о котором чуть ниже.

pip install "deepagents>=0.6.7" langchain-gigachat deepagents-gigachat

Профиль deepagents-gigachat подстраивает Deep Agents под особенности GigaChat: системный промпт, описания инструментов, дополнительный middleware. Он подключается автоматически, достаточно, чтобы пакет стоял в окружении, вызывать в коде ничего не нужно. Как он устроен и зачем — тема следующей статьи; здесь мы просто им пользуемся, чтобы примеры работали надёжно.

Ключ GigaChat API (как получить) и настройки кладём в файл .env рядом с кодом:

# .env
GIGACHAT_CREDENTIALS=ваш_авторизационный_ключ
GIGACHAT_SCOPE=GIGACHAT_API_PERS          # физлицам; компаниям -- GIGACHAT_API_B2B / GIGACHAT_API_CORP
GIGACHAT_MODEL=GigaChat-3-Ultra
GIGACHAT_VERIFY_SSL_CERTS=False       

Мы запускали примеры на модели GigaChat-3-Ultra. Вы можете подставить любую доступную вам модель с поддержкой функций — например, GigaChat-2-Max; актуальный список моделей и их идентификаторы — в документации GigaChat API.

В коде подхватим их через python-dotenv (pip install python-dotenv) — вызовом load_dotenv() в начале примера. langchain-gigachat сам возьмёт нужные переменные окружения (те, что с префиксом GIGACHAT_).

В примерах — deepagents 0.6.x (профиль GigaChat требует Python 3.12+ и deepagents ≥ 0.6.7). Обвязку быстро развивают: при воспроизведении сверяйтесь с changelog.

Наблюдаемость

Прежде чем запускать агент, стоит подготовить наблюдаемость (observability) и включить её до первого прогона, чтобы все шаги сразу записывались. У обвязки за одну задачу могут набегать десятки вызовов модели и инструментов; понять по «простыне» stdout, где агент свернул не туда, почти нереально, а в трассировке это видно сразу.

Под капотом у Deep Agents — обычный граф LangGraph, и LangChain предлагает смотреть трассы в своём облачном LangSmith. Но он проприетарный, с платными тарифами (бесплатный план ограничен), а мы обойдёмся open source: локальный Arize Phoenix с OpenInference-инструментированием LangChain собирает и хранит трассы целиком на вашей стороне. В них записи всего прогона, виден каждый шаг агента: вызовы модели и инструментов (каждый такой шаг называют спаном), содержимое контекста, расход токенов, ветки субагентов.

Трассы — не просто картинка в интерфейсе. Phoenix складывает их в базу (по умолчанию SQLite), а значит, они доступны как данные. С такой базой удобно работать кодинг-агенту: просишь его разобраться, почему прогон пошёл не так, — он сам запрашивает базу и собирает полную картину, вместо того чтобы вы вручную кликали по спанам.

Ставим пакеты и поднимаем Phoenix отдельным процессом — так он не зависит от нашего скрипта: переживает его перезапуски и продолжает копить трассы:

pip install arize-phoenix openinference-instrumentation-langchain
phoenix serve      # поднимет UI и коллектор на http://localhost:6006

В самом скрипте — только подключение трассировки (сервер уже поднят):

from phoenix.otel import register

register(
    project_name="deepagents-gigachat",
    endpoint="http://localhost:6006/v1/traces",
    auto_instrument=True,   # сам подключит OpenInference-обёртку для LangChain
)

Теперь каждый запуск агента автоматически попадает в Phoenix, его видно в интерфейсе по адресу http://localhost:6006.

Минимальный агент: файловая система в памяти

Начнём с агента, которому не даём ни одного своего инструмента:

from dotenv import load_dotenv
from deepagents import create_deep_agent
from langchain_gigachat import GigaChat

load_dotenv()          # подхватит GIGACHAT_* из .env
llm = GigaChat()       # параметры модели берутся из окружения

agent = create_deep_agent(model=llm, system_prompt="Ты полезный ассистент.")

result = agent.invoke(
    {"messages": [{"role": "user", "content":
        "Составь план изучения LangGraph на неделю и сохрани его в файл plan.md"}]}
)
print(result["files"].keys())   # dict_keys(['/plan.md'])

Мы не передавали инструменты, но агент справился — единственным вызовом встроенного write_file он сохранил план:

Трейс прогона
================================ Human Message =================================
Составь план изучения LangGraph на неделю и сохрани его в файл plan.md
================================== Ai Message ==================================
Tool Calls: write_file (file_path: plan.md)
================================= Tool Message =================================
Updated file /plan.md
================================== Ai Message ==================================
План изучения LangGraph на неделю успешно сохранен в файл plan.md.

Записан он не на диск, а в StateBackend — состояние графа. В result["files"] он под ключом /plan.md: агент просил сохранить plan.md, а виртуальная файловая система deepagents отсчитывает пути от своего корня  /. Для агента это полноценная файловая система, для нас — просто запись в объекте состояния; на диске при этом ничего не создаётся.

Основной пример: файлы и оболочка

Теперь у агента есть файлы и оболочка, и мы дадим ему задачу посерьёзнее. В рабочей папке лежит CSV с продажами; агент должен сам разобраться в данных, написать скрипт анализа, выполнить его и оформить отчёт.

Сначала подготовим песочницу и данные:

import csv, random
from pathlib import Path

workspace = Path("workspace")
workspace.mkdir(exist_ok=True)

random.seed(42)
with open(workspace / "sales.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.writer(f)
    writer.writerow(["date", "region", "product", "qty", "price"])
    for month in range(1, 7):
        for _ in range(50):
            writer.writerow([
                f"2026-{month:02d}-{random.randint(1, 28):02d}",
                random.choice(["Москва", "СПб", "Казань", "Новосибирск"]),
                random.choice(["A", "B", "C"]),
                random.randint(1, 20),
                random.choice([990, 1490, 2490]),
            ])

Получилось полгода продаж (январь–июнь 2026) — по 50 строк на месяц, с регионом, товаром, количеством и ценой. Обычный CSV, каких много; никакой разметки «специально для агента» в нём нет.

Теперь создаём агент с LocalShellBackend — это файловые инструменты плюс оболочка:

from deepagents import create_deep_agent
from deepagents.backends import LocalShellBackend
from langgraph.checkpoint.memory import MemorySaver

backend = LocalShellBackend(root_dir=str(workspace.resolve()), virtual_mode=True)

agent = create_deep_agent(
    model=llm,
    backend=backend,
    checkpointer=MemorySaver(),
    system_prompt=(
        "Ты -- аналитик данных с доступом к файлам и shell. "
        "Работай итеративно: изучи данные, напиши код, запусти его, "
        "проверь результат. Используй относительные пути. "
        "Не выдумывай цифры -- бери их только из вывода своих скриптов."
    ),
)

config = {"configurable": {"thread_id": "sales-analysis"}}

result = agent.invoke(
    {"messages": [{"role": "user", "content":
        "В рабочей папке лежит sales.csv. Разберись в структуре данных, "
        "напиши Python-скрипт analyze.py, который считает выручку по регионам "
        "и по месяцам, запусти его и оформи отчёт report.md с таблицами и выводами."}]},
    config=config,
)

for msg in result["messages"]:
    msg.pretty_print()

Обратите внимание на «Используй относительные пути» в системном промпте — это сознательное отступление от стоковой инструкции execute, которая, наоборот, просит абсолютные пути и запрещает cd. Дело в том, что в virtual_mode=True абсолютные пути файловых инструментов и execute отличаются, а вот относительные — совпадают. Файловые инструменты (read_file, ls и другие) живут в виртуальной файловой системе, где корень / — это рабочая папка агента: для них /sales.csv — файл в этой папке. А execute передаёт команду настоящей оболочке, которая про виртуальную систему ничего не знает: для неё /sales.csv — файл в корне диска, и его там нет. Относительный же путь sales.csv в обоих случаях указывает на один и тот же файл, потому что оболочка выполняет команды прямо в рабочей папке. 

Запускаем, и агент проходит всю задачу сам. Полная трасса с вызовами инструментов спрятана в спойлере (там интересно: скрипт с первого раза не завёлся, и видно, как агент чинит его по ошибкам). Ниже разберём ключевые шаги, так что заглядывать под спойлер необязательно.

Трейс прогона (сокращён — оставлены вызовы инструментов)
================================ Human Message =================================
В рабочей папке лежит sales.csv. Разберись в структуре данных, напиши Python-скрипт
analyze.py, ... оформи отчёт report.md с таблицами и выводами.
================================== Ai Message ==================================
Tool Calls: read_file (file_path: sales.csv)
================================== Ai Message ==================================
Tool Calls: write_file (file_path: analyze.py)
================================== Ai Message ==================================
Tool Calls: execute (command: python analyze.py)
================================= Tool Message =================================
[stderr] /bin/sh: python: command not found
Exit code: 127
================================== Ai Message ==================================
Tool Calls: execute (command: python3 analyze.py)
================================= Tool Message =================================
[stderr]     "| Регион | Выручка |
SyntaxError: EOL while scanning string literal
Exit code: 1
================================== Ai Message ==================================
Tool Calls: edit_file (file_path: analyze.py)
... ещё несколько edit_file ...
================================== Ai Message ==================================
Tool Calls: execute (command: python3 run.py)
================================= Tool Message =================================
[Command succeeded with exit code 0]
================================== Ai Message ==================================
Tool Calls: read_file (file_path: report.md)

Текстовый вывод удобен для чтения, но настоящую картину даёт Phoenix: тот же прогон там разворачивается в дерево спанов, где у каждого шага видны аргументы, результат, тайминг и токены.

Разберём, что здесь произошло:

  1. Изучение данных. Агент читает sales.csv через read_file — с пагинацией, чтобы понять структуру, не затягивая весь файл в контекст.

  2. Код. Через write_file создаёт analyze.py — обычный Python-скрипт с агрегацией выручки по регионам и месяцам.

  3. Первый запуск, поправка команды. execute с python analyze.py падает: python: command not found. Агент сам пробует python3 — уже рабочую команду.

  4. Второй запуск, исправление кода. Теперь падает сам скрипт: SyntaxError в незакрытой строке. Агент видит traceback, несколькими edit_file правит код (и в какой-то момент выносит его в отдельный run.py) и запускает снова — пока не получит exit code 0. Тот самый цикл «написал → запустил → исправил».

  5. Артефакт. Убедившись, что скрипт отработал, агент записывает его результаты в report.md в виде таблиц и выводов.

Значения в отчёте агент посчитал реально выполненным скриптом и прочитал из его вывода. После прогона в workspace/ лежат настоящие файлы: sales.csv, analyze.py, report.md (плюс пара вспомогательных, которые агент создал по ходу работы). Скрипт можно перезапустить руками, отчёт можно открыть и проверить. Так работает агент в среде: после него остаются воспроизводимые артефакты, которые живут отдельно от диалога.

С флагом virtual_mode=True файловые инструменты агента работают внутри workspace/ как внутри корня: пути с .. и ~ отклоняются, а каждый итоговый путь проверяется на выход за пределы папки — наружу файловым инструментам не выбраться. execute так не ограничить: команды оболочки выполняются на вашей машине по-настоящему, со всеми правами вашего пользователя. Для локальных экспериментов это приемлемо, но в эксплуатации нужен sandbox-бэкенд с изоляцией.

Навыки (Skills)

Покажем механизм навыков. Допустим, у команды есть стандарт оформления отчётов — держать его в системном промпте не хочется: он большой, а нужен не в каждом запросе. Вынесем его в навык. И добавим второй, не относящийся к задаче, — проверку качества данных: с одним навыком выбирать не из чего, а на двух видно, как агент по короткому описанию берёт нужный и не трогает лишний.

Навык — это папка со SKILL.md (имя и описание во Front Matter, дальше инструкция) и, при желании, вспомогательными файлами рядом; наш sales-report ссылается на лежащий рядом шаблон template.md. Обе папки-навыка уже лежат в workspace/skills/ — агенту достаточно указать путь к этой директории, найти и раскрыть нужный он сумеет сам. Собираем агент с навыками:

from dotenv import load_dotenv
from deepagents import create_deep_agent
from deepagents.backends import LocalShellBackend
from langgraph.checkpoint.memory import MemorySaver
from langchain_gigachat import GigaChat

load_dotenv()
llm = GigaChat()

# В workspace/skills/ заранее подготовлены две папки-навыка:
#   sales-report/       -- SKILL.md + template.md (стандарт оформления отчёта)
#   data-quality-check/ -- SKILL.md (проверка данных)

backend = LocalShellBackend(root_dir="workspace", virtual_mode=True)
agent = create_deep_agent(
    model=llm,
    backend=backend,
    skills=["/skills"],   # виртуальный путь: корень бэкенда = workspace/
    checkpointer=MemorySaver(),
    system_prompt=(
        "Ты -- аналитик данных с доступом к файлам и shell. "
        "Работай итеративно, не выдумывай цифры -- бери их из вывода своих скриптов."
    ),
)

result = agent.invoke(
    {"messages": [{"role": "user", "content":
        "Подготовь отчёт по продажам из sales.csv по нашим правилам оформления."}]},
    config={"configurable": {"thread_id": "sales-report-skill"}},
)

Запускаем — и в трассе видно, как агент выбирает нужный навык, раскрывает его и выполняет задачу:

Трейс прогона (сокращён)
Tool Calls: read_file (file_path: /skills/sales-report/SKILL.md)     # выбрал нужный навык
Tool Calls: read_file (file_path: /skills/sales-report/template.md)  # подтянул шаблон по ссылке
Tool Calls: read_file (file_path: sales.csv)
Tool Calls: write_file (file_path: run.py)
Tool Calls: execute (command: python run.py)
[stderr] /bin/sh: python: command not found   →  Exit code: 127
Tool Calls: execute (command: python3 run.py) →  exit code 0
Tool Calls: read_file (file_path: report.md)

Получив задачу «подготовь отчёт по продажам», агент из двух навыков выбрал релевантный — sales-report, — прочитал его SKILL.md, а следом и template.md, на который тот ссылается. Во второй навык (data-quality-check) он не полез: задача не про это. Полная инструкция заняла место в контексте только тогда, когда понадобилась, — в этом и есть прогрессивное раскрытие. Дальше — тот же цикл, что и в примере с аналитиком: агент пишет скрипт, сам чинит команду python→python3, получает report.md. Только теперь отчёт оформлен по стандарту из навыка.

На этом практика закончена. Из одного create_deep_agent() мы собрали агент, дали ему файловую систему, оболочку и навыки — и он прошёл реальную задачу от сырого CSV до готового отчёта, оставив после себя проверяемые артефакты. Ни одного специализированного агента писать не пришлось: вся настройка — содержимым среды. Перейдем к выводам.

Выводы

  • Обвязки стали де-факто стандартом разработки агентов в любой сфере: кодинг-агенты, персональные ассистенты, исследовательские и аналитические агенты. За разными задачами один узнаваемый набор: файловая система, планировщик, память, субагенты, навыки.

  • Deep Agents — открытая реализация этого стандарта на надёжном стеке: внутри тот же LangGraph с чекпоинтами и стримингом, снаружи — create_deep_agent() с готовой обвязкой. Специализируешь его содержимым среды: инструкциями, навыками, памятью.

  • Все примеры выше работают на GigaChat через langchain-gigachat: обвязка не привязана к конкретной модели — в этом её главная сила. Под Deep Agents GigaChat работает и без профиля, но с профилем deepagents-gigachat — заметно лучше: он подстраивает обвязку под особенности GigaChat и повышает её результаты на агентных задачах.

За этим профилем — отдельная работа. Как мы собирали deepagents-gigachat и как создали и регулярно обновляем открытый бенчмарк harness-bench-fast, на котором измеряем его эффект, расскажем в следующей статье.

Заходите в репозитории нашей команды на GitHub и GitVerse — там библиотеки интеграции GigaChat с экосистемой LangChain/LangGraph, утилиты и кукбуки с примерами агентов.

Полезные ссылки

Комментарии (2)


  1. gybson_63
    17.08.2026 12:01

    А ACP вам чем не угодил? Уже как-то поздно велосипедить агентов на питоне, не?


    1. trashchenkov Автор
      17.08.2026 12:01

      Ничего против ACP не имею :) Но ACP сам по себе же не заменяет харнесс. Он про то, как агента подключить к IDE или еще какому-либо клиенту. А саму внутреннюю механику агента — цикл, работу с контекстом, планирование, субагентов и т. д. — все равно надо либо реализовать самому, либо взять готовую в виде Claude Code, OpenCode, Deep Agents и прочих харнессов.

      Для этого у LangChain для этого уже есть отдельная интеграция deepagents-acp: она позволяет агенту на их стеке работать как ACP-сервер.