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

от автора

Введение

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

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

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

Пара слов обо мне

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


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

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

Архитектура агента

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

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

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

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

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

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

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

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

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

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

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

Содержимое агента

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  8. 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, для продуктивной среды может понадобиться более серьезная изоляция в несколько уровней. См. теоретическую часть.

Инструменты работы с 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-совместимую модель.

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

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

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

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

# Plan- [>] eda-analysis: Exploratory data analysis of basic_dataset.csv **(active)**    - skills: eda, python-scripts    - inputs: /workspace/input/basic_dataset.csv    - success: EDA script runs successfully and prints key statistics, distributions, correlations, and missing value summaries to stdout.    - outputs: /workspace/out/eda_summary.csv- [ ] create-presentation: Create PowerPoint presentation with key EDA findings    - skills: presentation, python-scripts    - inputs: /workspace/input/basic_dataset.csv, /workspace/out/eda_summary.csv    - success: A .pptx file is generated with slides covering dataset overview, survival analysis, demographics, fare analysis, and key insights.    - outputs: /workspace/out/titanic_eda_presentation.pptx

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

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

status: successsummary: Listed 1 input file(s).- /workspace/input/basic_dataset.csv (61194 bytes)

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

import pandas as pdimport numpy as npdf = pd.read_csv('/workspace/input/basic_dataset.csv')# === SHAPE ===print("=== SHAPE ===")print(df.shape)print()# === DTYPES ===print("=== DTYPES ===")print(df.dtypes)print()... <truncated>

Результат:

status: successsummary: Staged `eda_analysis.py` (131 lines). Next: run_python_file({"script_path": "eda_analysis.py"}).

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

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

status: successsummary: success: Python execution succeeded (exit_code=0).exit_code: 0timed_out: Falsesucceeded: TrueSTDOUT:=== SHAPE ===(891, 12)=== DTYPES ===PassengerId      int64Survived         int64Pclass           int64Name               strSex                strAge            float64SibSp            int64Parch            int64Ticket             strFare           float64Cabin              strEmbarked           strdtype: object...<truncated>

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

{  "id": "eda-analysis",  "notes": "EDA script ran successfully, printed key statistics/distributions/correlations/missing values to stdout, and saved /workspace/out/eda_summary.csv with 25 summary metrics."}

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

STDERR:Traceback (most recent call last):  File "/workspace/code/deck_build_part1.py", line 11, in <module>    add_bg_rect(slide, DARK)  File "/workspace/code/deck_helpers.py", line 17, in add_bg_rect    width = slide.slide_width            ^^^^^^^^^^^^^^^^^AttributeError: 'Slide' object has no attribute 'slide_width'. Did you mean: 'slide_id'?

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

## Результаты анализа и презентация### 📊 Ключевые выводы EDA (Titanic, 891 пассажир)|Метрика|Значение||---|---||**Общий уровень выживших**|~38%||**Пропущенные данные**|Age: 20%, Cabin: 77.1%, Embarked: 0.22%|...### 📁 Созданные файлы|Файл|Описание||---|---||`titanic_eda_presentation.pptx`|Презентация из 9 слайдов с графиками и выводами||`eda_summary.csv`|Сводка из 25 метрик EDA||`chart_survival.png`|Общая выживаемость|...

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

Токен-экономика запуска

Пример выше — не только демонстрация песочницы, но и типичный агентский запуск с заметной «стоимостью» в токенах. Все 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 мин)

LLM-вызовов

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-трейса):

Шаг

Фаза

Действие

LLM-вызовов

Input

Output

Cached

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

Время

1

Оркестратор

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

1

3 429

409

0

3 838

5,5 с

2

Оркестратор

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

1

3 895

77

3 168

3 972

1,6 с

3

Субагент EDA

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

8

68 810

3 527

36 960

72 337

39,7 с

4

Оркестратор

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

1

4 280

53

3 168

4 333

1,1 с

5

Субагент presentation

skills → код → sandbox → .pptx

17

274 700

11 832

204 864

286 532

127,6 с

6

Оркестратор

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

1

4 799

653

4 224

5 452

7,0 с

Итого

29

359 913

16 551

252 384

376 464

182,5 с

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

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

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

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

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

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

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


Заключение

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

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

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

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

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

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

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


Серия «LLM Sandbox»

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

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

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

  1. Agent Harness: одна LLM, разные результаты — в чем секрет?

  2. Второй мозг и LLM-Wiki: Теория и практический гайд по созданию и поддержке личной базы знаний

ссылка на оригинал статьи https://habr.com/ru/articles/1051892/