LLM Sandbox: пример реализации агента с песочницей [часть 2, практика]

LLM Sandbox: пример реализации агента с песочницей [часть 2, практика]

Статья посвящена практической реализации агента с изолированной средой исполнения кода. Рассказываю как устроен агент, который пишет и исполняет код в Docker песочнице.

Это вторая часть серии про LLM Sandbox. В первой части мы разобрали риски исполнения кода от LLM, ограничения песочницы, способы изоляции (Docker, Wasm, gVisor, microVM) и минимальную архитектуру агент+песочница.

Код реализации агента, skills, полные логи и артефакты примера — в открытом GitHub-репозитории .

Меня зовут Евгений. Я разработчик и лид ML-команды. На работе и в свободное время занимаюсь проектами, связанными с агентами, LLM и обработкой естественного языка в целом. В своём тг-канале делюсь практическим опытом и рассказываю про техническую часть AI и ML. Ссылка на пост-навигатор канала .

Реализация агента с песочницей

В этом разделе посмотрим как агент реализован в репозитории : оркестратор, субагенты, skills и Docker Sandbox.

Если кратко, агент работает так:

Получает задачу от пользователя.

Получает задачу от пользователя.

С помощью инструментов выполняет планирование и составляет набор подзадач.

С помощью инструментов выполняет планирование и составляет набор подзадач.

Назначает их субагентам.

Назначает их субагентам.

Субагент пишет код для решения подзадачи и исполняет его в песочнице.

Субагент пишет код для решения подзадачи и исполняет его в песочнице.

После выполнения подзадачи обновляется список задач.

После выполнения подзадачи обновляется список задач.

Артефакты, полученные в песочнице, сохраняются, а выходные файлы показываются в интерфейсе и доступны для скачивания.

Артефакты, полученные в песочнице, сохраняются, а выходные файлы показываются в интерфейсе и доступны для скачивания.

На финальном этапе оркестратор агрегирует решение всех подзадач и артефакты.

На финальном этапе оркестратор агрегирует решение всех подзадач и артефакты.

Выдаёт пользователю ответ.

Выдаёт пользователю ответ.

Основной паттерн работы агента - гибрид ReAct + Plan-Execute. Основной фреймворк - LangGraph.

Посмотрим подробнее на ключевые блоки.

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

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

Субагент — сессия с LLM и чистым контекстным окном. Оркестратор передаёт субагенту конкретную задачу и необходимый контекст. Так можно не перегружать основного агента, а выполнять подзадачи независимо.

Субагент — сессия с LLM и чистым контекстным окном. Оркестратор передаёт субагенту конкретную задачу и необходимый контекст. Так можно не перегружать основного агента, а выполнять подзадачи независимо.

Внутри оркестратора и субагента находится LLM с отдельными системными промптами.

Внутри оркестратора и субагента находится LLM с отдельными системными промптами.

Каталог навыков (skills) — набор подробных инструкций под конкретные задачи. Например, в skills/presentation в репозитории полностью описан процесс написания презентаций. Что такое skills, я писал в канале ( ссылка на пост ). Оркестратор и субагенты всегда видят название и описание навыка, а при необходимости подгружают полную инструкцию через инструмент read_skill .

Каталог навыков (skills) — набор подробных инструкций под конкретные задачи. Например, в skills/presentation в репозитории полностью описан процесс написания презентаций.

Что такое skills, я писал в канале ( ссылка на пост ). Оркестратор и субагенты всегда видят название и описание навыка, а при необходимости подгружают полную инструкцию через инструмент read_skill .

Что такое skills, я писал в канале ( ссылка на пост ). Оркестратор и субагенты всегда видят название и описание навыка, а при необходимости подгружают полную инструкцию через инструмент read_skill .

Инструменты для управления подзадачами. create_todo_list — создаёт набор подзадач. complete_todo — закрывает подзадачу при успешном решении. start_next_todo — начинает работу над подзадачей. revise_todo_list — пересобирает список подзадач, если текущий план перестал подходить. block_todo — блокирует подзадачу, если решение пошло не по плану.

Инструменты для управления подзадачами.

create_todo_list — создаёт набор подзадач.

create_todo_list — создаёт набор подзадач.

complete_todo — закрывает подзадачу при успешном решении.

complete_todo — закрывает подзадачу при успешном решении.

start_next_todo — начинает работу над подзадачей.

start_next_todo — начинает работу над подзадачей.

revise_todo_list — пересобирает список подзадач, если текущий план перестал подходить.

revise_todo_list — пересобирает список подзадач, если текущий план перестал подходить.

block_todo — блокирует подзадачу, если решение пошло не по плану.

block_todo — блокирует подзадачу, если решение пошло не по плану.

Инструменты управления файлами. read_skill — читает навык из файла и добавляет его в контекст. list_input_files — список файлов, отправленных пользователем. list_output_files — список файлов, созданных в ходе задачи. read_file_content — читает файл.

Инструменты управления файлами.

read_skill — читает навык из файла и добавляет его в контекст.

read_skill — читает навык из файла и добавляет его в контекст.

list_input_files — список файлов, отправленных пользователем.

list_input_files — список файлов, отправленных пользователем.

list_output_files — список файлов, созданных в ходе задачи.

list_output_files — список файлов, созданных в ходе задачи.

read_file_content — читает файл.

read_file_content — читает файл.

Инструменты исполнения кода. write_python_file — подготавливает полный Python-скрипт и сохраняет в /workspace/code . run_python_file — исполняет ранее подготовленный скрипт в песочнице.

Инструменты исполнения кода.

write_python_file — подготавливает полный Python-скрипт и сохраняет в /workspace/code .

write_python_file — подготавливает полный Python-скрипт и сохраняет в /workspace/code .

run_python_file — исполняет ранее подготовленный скрипт в песочнице.

run_python_file — исполняет ранее подготовленный скрипт в песочнице.

Docker Sandbox — песочница для изолированного исполнения кода. В режиме субагента создаётся отдельный Docker контейнер на время выполнения подзадачи и серии sandbox-вызовов. Команда запуска описана в теоретической части , флаги: --network=none , --read-only , --cap-drop=ALL , --security-opt=no-new-privileges , non-root user, лимиты CPU/RAM/PIDs, tmpfs для /tmp , read-only mount для /workspace/input и /workspace/code .

Docker Sandbox — песочница для изолированного исполнения кода. В режиме субагента создаётся отдельный Docker контейнер на время выполнения подзадачи и серии sandbox-вызовов. Команда запуска описана в теоретической части , флаги: --network=none , --read-only , --cap-drop=ALL , --security-opt=no-new-privileges , non-root user, лимиты CPU/RAM/PIDs, tmpfs для /tmp , read-only mount для /workspace/input и /workspace/code .

Важно: Docker с такими флагами — практичная изоляция для локального MVP, для продуктивной среды может понадобиться более серьезная изоляция в несколько уровней. См. теоретическую часть.

Важно: Docker с такими флагами — практичная изоляция для локального MVP, для продуктивной среды может понадобиться более серьезная изоляция в несколько уровней. См. теоретическую часть.

Инструменты работы с Bash умышленно не предоставляются агенту для прозрачной и контролируемой работы с заданными инструментами.

В интерфейсе показывается весь путь агента: текущий план, закрытые подзадачи, вызовы инструментов и их результаты. Полные логи — в подключённом Langfuse , а локально — в agent/runs/<run_id>/events.jsonl , agent/traces/<run_id>.jsonl и agent/web-artifacts/<run_id>/ .

Пример работы агента

Для наглядности посмотрим на конкретный запуск.

Задача (промпт)

Проанализируй набор данных и подготовь презентацию с ключевыми выводами EDA.

Набор данных

Датасет соревнования «Титаник» .

P.S. Модели видели этот датасет множество раз при обучении, и написать код аналитики несложно. Цель примера — показать взаимодействие агента с песочницей, а не способность LLM писать код.

Qwen3.5-27B — модель, которую можно развернуть на доступном железе. В репозитории это значение используется как дефолт ( Qwen/Qwen3.5-27B ), но его можно заменить через LLM_MODEL на любую OpenAI-совместимую модель.

Шаги выполнения задачи

Первым шагом агент создаёт список подзадач:

eda-analysis — первичный анализ данных.

eda-analysis — первичный анализ данных.

create-presentation — создание презентации.

create-presentation — создание презентации.

Для каждой подзадачи заданы требуемые навыки, входные/выходные файлы и критерии успеха.

Второй шаг — start_next_todo . Агент просматривает доступные входные файлы через list_input_files :

Читает фрагмент CSV через read_file_content , затем пишет код через write_python_file :

Результат:

Срабатывает валидация: файл не может быть больше 120 строк — агент делит его на два скрипта.

Исполняет в песочнице через run_python_file :

Аналогично создаётся /workspace/out/eda_summary.csv . Подзадача закрывается через complete_todo :

Следующая подзадача — презентация. Агент подгружает skills presentation и python-scripts . В процессе возможны ошибки, которые агент исправляет в следующих итерациях работы с кодом:

Итоговый ответ агента (сокращённо):

Полные логи и артефакты — в репозитории .

Пример выше — не только демонстрация песочницы, но и типичный агентский запуск с заметной «стоимостью» в токенах. Все LLM-вызовы, тайминги и usage отслеживались через Langfuse. Ниже — разбор run feec9b8b1e754c018d6de3f2f09d4f8c (Titanic + презентация, Qwen3.5-27B, OpenRouter/OpenAI-совместимый API).

Что считаем. На каждом шаге цикла модель получает промпт ( input_tokens ), генерирует ответ ( output_tokens ). Часть промпта может быть взята из KV-cache ( cached_input_tokens ). Песочница и файловые инструменты сами по себе токены не тратят, но результаты их работы возвращаются в контекст — и именно это раздувает входной запрос в модель на следующих шагах. В таблице ниже суммируются все вложенные LLM-вызовы оркестратора и субагентов из Langfuse.

Время работы

182,6 с (~3 мин)

29 (4 оркестратор + 8 субагент EDA + 17 субагент presentation)

Input tokens

359 913

Output tokens

16 551

Всего (input + output)

376 464

Запусков кода в песочнице

7 (~5 с суммарно)

Sandbox занял ~3% общего времени работы. Основная задержка — ожидание LLM и накопление контекста, а не исполнение Python.

Разбивка по фазам (данные Langfuse-трейса):

Всего токенов

create_todo_list — планирование

3 429

409

3 838

5,5 с

start_next_todo — делегирование EDA

3 895

3 168

3 972

1,6 с

файлы → код → sandbox → eda_summary.csv

68 810

3 527

36 960

72 337

39,7 с

start_next_todo — делегирование презентации

4 280

3 168

4 333

1,1 с

Субагент presentation

skills → код → sandbox → .pptx

274 700

11 832

204 864

286 532

127,6 с

финальный ответ пользователю

4 799

653

4 224

5 452

7,0 с

359 913

16 551

252 384

376 464

182,5 с

Что видно из цифр:

Оркестратор дешёвый — 17,6 тыс. токенов (4,7% от запуска). Он планирует, делегирует и синтезирует ответ, не таская в контекст логи.

Оркестратор дешёвый — 17,6 тыс. токенов (4,7% от запуска). Он планирует, делегирует и синтезирует ответ, не таская в контекст логи.

Субагент presentation — главный потребитель — 286,5 тыс. токенов (76%) и 128 с (70% времени). Причина: много итераций write_python_file / run_python_file , ошибки в коде ( slide_width ), повторные прогоны и растущий контекст.

Субагент presentation — главный потребитель — 286,5 тыс. токенов (76%) и 128 с (70% времени). Причина: много итераций write_python_file / run_python_file , ошибки в коде ( slide_width ), повторные прогоны и растущий контекст.

Контекст растёт по цепочке. Input на последнем LLM-вызове presentation-субагента — 24 706 токенов (против 4 445 на первом).

Контекст растёт по цепочке. Input на последнем LLM-вызове presentation-субагента — 24 706 токенов (против 4 445 на первом).

Самые дорогие вызовы — генерация кода , а не sandbox. write_python_file с output 1 777 токенов vs run_python_file с output 56 токенов.

Самые дорогие вызовы — генерация кода , а не sandbox. write_python_file с output 1 777 токенов vs run_python_file с output 56 токенов.

Cache помогает, но не спасает от роста контекста. Суммарно из cache обслужено 252 384 токенов input (~70% у presentation-субагента).

Cache помогает, но не спасает от роста контекста. Суммарно из cache обслужено 252 384 токенов input (~70% у presentation-субагента).

Практический вывод: песочница изолирует исполнение , но не стоимость . Чтобы удержать token budget, нужны те же приёмы, что и в инжиниринге контекста: субагенты с чистым окном, лимиты на размер tool output, сокращение stdout/stderr, skills по требованию ( read_skill ), а не в каждый промпт целиком.

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

Docker Sandbox с жёсткими флагами — рабочий вариант для локального MVP: отдельная сессия для подзадачи/сессии исполнения кода в песочнице, изоляция исполнения, понятный обмен артефактами через /workspace/input и /workspace/out .

Docker Sandbox с жёсткими флагами — рабочий вариант для локального MVP: отдельная сессия для подзадачи/сессии исполнения кода в песочнице, изоляция исполнения, понятный обмен артефактами через /workspace/input и /workspace/out .

Оркестратор + субагенты помогают не смешивать планирование и «грязный» контекст исполнения кода.

Оркестратор + субагенты помогают не смешивать планирование и «грязный» контекст исполнения кода.

Основное время тратится на работу LLM. 7 запусков кода заняли ~5 с; основная стоимость — в генерации и накоплении контекста.

Основное время тратится на работу LLM. 7 запусков кода заняли ~5 с; основная стоимость — в генерации и накоплении контекста.

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

Теорию про риски, ограничения и альтернативы (Wasm, gVisor, microVM) смотрите в первой части . Код, логи и все артефакты смотрите в репозитории на GitHub .

P.S. Спасибо за прочтение. Приглашаю в тг-канал « В погоне за NLP » (@chasing_nlp) — там про новости из мира AI/ML, анонсы новых статей и опыт разработки проектов с LLM и агентами.

Серия «LLM Sandbox»

Часть 1. Теория — риски, ограничения, способы изоляции.

Часть 1. Теория — риски, ограничения, способы изоляции.

Часть 2. Практика — эта статья.

Часть 2. Практика — эта статья.

Другие мои статьи

← Cybersecurity