Главная / Статьи

Создание системы исследований с участием нескольких агентов с использованием LangChain Deep Agents

Практический обзор паттерна Deep Agents: оркестратор, специализированные агенты, безопасные инструменты и интерфейс Streamlit, протестированные с использованием локальных и облачных больших языковых моделей.

Издатель: REACTAPP.TOP10 мин чтения2403 слов

Источники: Deep Agents in Action: Building a Multi-Agent Research System

Циклы с одним запросом уступают место системам с несколькими агентами. Новая структура распределяет работу, сохраняет состояние между шагами и предоставляет каждому агенту ограниченный набор инструментов. Пример тестовой версии оркестратора, построенного на этой идее — Deep Agents от LangChain, является хорошим примером для исследования; ниже описано, что он делает, и как на его основе было создано небольшое исследовательское приложение.

Что решает архитектура Deep Agents

Недавно LangChain объявила о управляемой платформе для запуска топологий глубоких агентов. Эта концепция направлена на устранение трех слабостей одиночного агента при выполнении длительных задач: снижение объема контекста, затруднения в выборе инструмента и усугубление ошибок в логических рассуждениях по мере накопления шагов.

Структура представляет собой иерархию:

  • Оркестратор высшего уровня разбивает сложную задачу на части и передает каждую из них специалисту.
  • Каждый специалист работает в своем изолированном цикле ReAct, имея собственные инструменты и память.
  • Специалисты возвращают структурированные результаты, а оркестратор объединяет их воедино.
  • Эти компоненты взаимосвязаны, поэтому оркестратор сам может выступать в роли подагента другого специалиста.

По сравнению с ситуацией, когда один агент выполняет всё сам, разница выглядит следующим образом:

| Single Agent                       | Deep Agents                                       |
| ---------------------------------- | ------------------------------------------------- |
| One context window gets overloaded | Each agent has an isolated, focused context       |
| All reasoning in one prompt        | Specialised reasoning per domain                  |
| Hard to scale                      | Add specialists without changing the orchestrator |
| Hard to debug                      | Full delegation trace for auditability            |

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

В репозитории проекта указано, что это фреймворк с открытым исходным кодом, предусматривающий определённый подход к решению задач, готовый к использованию сразу после установки; его можно расширять, переопределять или заменять любые его части. По умолчанию настройки оптимизированы для длительной работы с несколькими этапами, принимаются любые модели вызова инструментов (frontier, open-weight или локальные), а для потоковой обработки, сохранения данных и создания точек контроля используется LangGraph; отслеживание, оценка и развертывание осуществляются через LangSmith. Список функций включает подагентов с изолированным контекстом, файловую систему с возможностью подключения различных бэкендов, систему управления контекстом, переносящую результаты работы инструментов на диск, доступ к оболочке в изолированной среде, постоянную память, проверку вызовов инструментов человеком, многократно используемые навыки, а также возможность использования пользовательских инструментов или любого сервера MCP.

Основные идеи

  • Иерархическая организация. Агент-контролер получает цель высокого уровня и передаёт подзадачи специалистам, вместо того чтобы выполнять каждый шаг в одном цикле.
  • Специализированные роли. У каждого агента есть чётко определённый системный промпт, собственные инструменты и отдельное окно контекста для таких задач, как поиск информации, вычисления или составление документа.
  • Изолированная память. Каждый агент сохраняет собственную рабочую память, факты и информацию о произошедшем, так что данные одной фазы не просачиваются в другую.
  • Логика ReAct. Подагенты повторяют циклы «Мышление», «Действие», «Наблюдение» с использованием проверенных интерфейсов инструментов.

Реализация

Демонстрационное приложение — это небольшая учебная разработка, созданная с использованием IBM Bob, которая может работать локально или с облачными моделями. Оно следует паттерну Deep Agents в умеренном масштабе.

Задача проходит по короткой иерархии:

User Task
  └─ OrchestratorAgent (Planning & Synthesis)
      ├─ ResearcherAgent (web_search)
      ├─ AnalystAgent (calculator, code_executor)
      └─ WriterAgent (file_reader)

Четыре слоя обеспечивают его работу:

  1. Оркестрация (agents/orchestrator.py) принимает ввод от пользователя, оценивает его сложность, формирует план, последовательно распределяет подзадачи и синтезирует результаты.
  2. Специалисты (agents/specialists.py) определяют ResearcherAgent, AnalystAgent и WriterAgent.
  3. Инструменты (tools/) обеспечивают безопасную арифметическую обработку через AST, изолированную выполнимость Python, абстракцию веб-поиска и работу с файлами в защищенной среде.
  4. Передатчик и интерфейс (app.py и main.py) предоставляют фронтенд на Streamlit, который асинхронно синхронизирует состояние между потоками и в реальном времени отслеживает этапы выполнения, а также интерфейс командной строки.

В основе лежит обычный Python с использованием Streamlit в качестве интерфейса. Для локальной обработки данных была проведена тестирование с Ollama, кроме того, через настройки поддерживается llama.cpp.

Структура папок, определённая требованиями:

deepagents-usecase/
├── agents/
│   ├── __init__.py         # Package exports
│   ├── base.py             # Abstract BaseAgent + ReAct loop
│   ├── llm_client.py       # LLM adapter (Ollama/llama.cpp/OpenAI/Anthropic)
│   ├── messages.py         # Typed message protocol
│   ├── orchestrator.py     # OrchestratorAgent (top-level)
│   ├── researcher.py       # ResearcherAgent specialist
│   ├── analyst.py          # AnalystAgent specialist
│   └── writer.py           # WriterAgent specialist
├── tools/
│   ├── __init__.py
│   ├── base.py             # BaseTool + ToolResult
│   ├── calculator.py       # Safe AST-based arithmetic evaluator
│   ├── code_executor.py    # Sandboxed Python execution (exec with allow-list)
│   ├── file_reader.py      # Sandboxed file reading (input/ only)
│   └── web_search.py       # Web search (stub + live Tavily)
├── memory/
│   ├── __init__.py
│   └── store.py            # AgentMemoryStore (short/long-term/episodic)
├── config/
│   ├── __init__.py
│   └── settings.py         # Centralised env-based configuration
├── tests/
│   ├── test_tools.py           # Tool unit tests (71 tests)
│   ├── test_memory.py          # Memory unit tests (24 tests)
│   ├── test_agents.py          # Agent integration tests (92 tests)
│   └── test_code_executor.py   # Code Executor tests (134 tests)
├── input/                  # Input documents (content gitignored)
├── output/                 # Generated reports (content gitignored)
├── memory/                 # Persistent agent memory (JSON files)
├── scripts/
│   ├── start.sh                  # Launch Streamlit in detached mode
│   ├── stop.sh                   # Gracefully stop the application
│   ├── cleanup.sh                # Remove .venv, __pycache__, etc.
│   └── check_code_executor.py    # Standalone Code Executor sanity-check
├── Docs/
│   ├── Architecture.md           # Mermaid architecture diagrams
│   ├── Quickstart.md             # Step-by-step getting started guide
│   ├── API.md                    # Full public API reference
│   └── CodeExecutorVerification.md  # Code Executor test & verification guide
├── app.py                  # Streamlit web UI
├── main.py                 # CLI entry point
├── requirements.txt
├── .env.example            # Environment variable template
└── README.md

Конфигурация гибкая, поэтому код адаптируется к распространённым настройкам. Бэкенд может взаимодействовать с локальными средами выполнения, такими как llama.cpp и Ollama, или с облачными провайдерами через API-ключи. Файл с настройками, снабжённый комментариями, описывает все параметры:

# =============================================================================
# Deep Agents System — Environment Configuration
# =============================================================================
# Copy this file to .env and fill in your values.
# NEVER commit the .env file to version control.
#
# Usage:
#   cp .env.example .env
#   # edit .env with your actual keys
# =============================================================================

# ─── LLM Provider ──────────────────────────────────────────────────────────
# Select ONE provider. Comment out the others.

# Option A: Ollama (local, default — no API key needed)
LLM_PROVIDER=ollama
OLLAMA_BASE_URL=http://localhost:11434
OLLAMA_MODEL=llama3.2

# Option B: llama.cpp (local)
# LLM_PROVIDER=llamacpp
# LLAMACPP_BASE_URL=http://localhost:9931/v1
# LLAMACPP_MODEL=local-model

# Option C: OpenAI
# LLM_PROVIDER=openai
# OPENAI_API_KEY=sk-...
# OPENAI_MODEL=gpt-4o

# Option D: Anthropic
# LLM_PROVIDER=anthropic
# ANTHROPIC_API_KEY=sk-ant-...
# ANTHROPIC_MODEL=claude-3-5-sonnet-20241022

# ─── Application Settings ──────────────────────────────────────────────────
APP_PORT=8501
APP_HOST=0.0.0.0
LOG_LEVEL=INFO

# ─── Agent Configuration ───────────────────────────────────────────────────
# Maximum reasoning steps per agent (lower = faster on slow local LLMs)
MAX_AGENT_STEPS=8

# Maximum tokens per LLM call (1024 is enough for ReAct; raise for longer reports)
MAX_TOKENS=1024

# Temperature for LLM responses (0.0 = deterministic, 1.0 = creative)
TEMPERATURE=0.1

# ─── Memory & Storage ──────────────────────────────────────────────────────
# Directory for persistent agent memory (relative to project root)
MEMORY_DIR=./memory

# Maximum number of memories to retain per agent
MAX_MEMORY_ENTRIES=100

# ─── Tool Configuration ────────────────────────────────────────────────────
# Enable or disable specific tools (true/false)
TOOL_WEB_SEARCH_ENABLED=true
TOOL_CALCULATOR_ENABLED=true
TOOL_FILE_READER_ENABLED=true
TOOL_CODE_EXECUTOR_ENABLED=true

# Web search stub — set to real Tavily/SerpAPI key for live search
# TAVILY_API_KEY=tvly-...

# ─── Output ────────────────────────────────────────────────────────────────
OUTPUT_DIR=./output

Код, стоящий внимания

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

Безопасный оценщик арифметических выражений

Разрешение модели выполнять произвольный код для математических вычислений создало бы угрозу безопасности. Поэтому калькулятор парсит выражения с помощью модуля ast в Python и принимает только явный список разрешенных типов узлов и функций, никогда не используя прямой вызов eval():

# Whitelisted AST operators and math functions
_SAFE_OPERATORS = {
    ast.Add: operator.add,
    ast.Sub: operator.sub,
    ast.Mult: operator.mul,
    ast.Div: operator.truediv,
    ast.Pow: operator.pow,
    ast.USub: operator.neg,
}

_SAFE_FUNCTIONS = {
    "abs": abs, "round": round, "sqrt": math.sqrt,
    "sin": math.sin, "cos": math.cos, "log": math.log,
    "pi": math.pi, "e": math.e,
}

def _safe_eval(node: ast.expr) -> float:
    """Recursively evaluate an AST expression node in a safe sandbox."""
    if isinstance(node, ast.Constant):
        if isinstance(node.value, (int, float)):
            return float(node.value)
        raise ValueError(f"Unsupported constant type: {type(node.value).__name__}")

    if isinstance(node, ast.BinOp):
        op_type = type(node.op)
        if op_type not in _SAFE_OPERATORS:
            raise ValueError(f"Unsupported binary operator: {op_type.__name__}")
        left = _safe_eval(node.left)
        right = _safe_eval(node.right)
        return _SAFE_OPERATORS[op_type](left, right)

    if isinstance(node, ast.Call):
        func_name = node.func.id
        if func_name not in _SAFE_FUNCTIONS:
            raise ValueError(f"Function '{func_name}' is not whitelisted.")
        args = [_safe_eval(a) for a in node.args]
        return _SAFE_FUNCTIONS[func_name](*args)

    raise ValueError(f"Unsupported AST node type: {type(node).__name__}")

Экзекутор кода в изолированной среде

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

def _build_exec_namespace() -> dict[str, Any]:
    return {
        "__builtins__": _SAFE_BUILTINS, # Explicit whitelist (no open, __import__, eval)
        "math": math,
        "statistics": statistics,
        "pi": math.pi,
        "e": math.e,
    }

def _execute_code(code: str, timeout: int = 5) -> ToolResult:
    exec_result = _ExecResult()
    captured_io = io.StringIO()

    def _worker():
        namespace = _build_exec_namespace()
        namespace["__builtins__"]["print"] = lambda *args, **kw: print(*args, **{**kw, "file": captured_io})
        try:
            exec(code, namespace)
            exec_result.stdout = captured_io.getvalue()
        except Exception as exc:
            exec_result.error = f"{type(exc).__name__}: {exc}"

    thread = threading.Thread(target=_worker, daemon=True)
    thread.start()
    thread.join(timeout=timeout)
    if thread.is_alive():
        return ToolResult(success=False, error=f"Execution timed out after {timeout}s.")

Безопасный для многопоточности ретранслятор для интерфейса Streamlit

Рабочие потоки Streamlit не могут напрямую записывать в st.session_state, поскольку у них отсутствует ScriptRunContext. Для решения этой проблемы приложение использует синглтон-ретранслятор, кэшируемый с помощью @st.cache_resource и защищаемый реентрантным замком (threading.RLock), что позволяет данным из потоков передаваться в страницу:

class _PipelineRelay:
    """Thread-safe relay store for the running multi-agent pipeline."""
    def __init__(self) -> None:
        self.lock = threading.RLock()
        self.status_messages: list[dict[str, str]] = []
        self.agent_cards: dict[str, list[str]] = {}
        self.running: bool = False

    def append_status(self, msg_dict: dict[str, str]) -> None:
        with self.lock:
            self.status_messages.append(msg_dict)
            agent_id = msg_dict.get("agent_id", "orchestrator")
            self.agent_cards.setdefault(agent_id, []).append(msg_dict.get("detail", ""))

@st.cache_resource
def _get_relay() -> _PipelineRelay:
    return _PipelineRelay()

Пример выполнения задания: изменение климата

Задачей был анализ того, как повышение концентрации CO2 влияет на экосистемы по всему миру, включая текущий уровень CO2, процентное увеличение с доиндустриальных времен, а также рекомендации по принятию мер.

Что происходит далее:

  1. Организатор разделяет задание на исследование, анализ и написание текста.
  2. Исследователь выполняет три поиска в Интернете, касающихся тенденций концентрации CO2, воздействия на экосистемы и политических рамок.
  3. Аналитик использует калькулятор: (421 - 280) / 280 * 100, что даёт примерно 50,36% увеличения.
  4. Автор составляет структурированный документ в формате Markdown с кратким изложением, результатами исследования, анализом данных и рекомендациями.
  5. Организатор объединяет всё в окончательный отчёт.

Результат сохраняется в output/deep_agents_report_<timestamp>.md, пример выглядит следующим образом:

Benefits of Renewable Energy and Calculation of 280 Times 42

Benefits of Renewable Energy

Renewable energy reduces greenhouse gas emissions, contributing to climate change mitigation (Source: [National Renewable Energy Laboratory](https://www.nrel.gov/renewables/energy-benefits.html)). This is a key finding supported by credible sources, including the International Renewable Energy Agency (2020) and the World Health Organization (2020).

Renewable energy creates jobs and stimulates local economies (Source: [International Renewable Energy Agency](https://www.irena.org/publications/2020/Jun/Global-Status-Report-2020)). This is a significant benefit highlighted by the International Energy Agency (2020) and the National Bureau of Economic Research (2020).

Renewable energy improves air quality and public health (Source: [World Health Organization](https://www.who.int/news-room/fact-sheets/detail/air-pollution)). This is a critical aspect of renewable energy, supported by the National Renewable Energy Laboratory (n.d.).

The global renewable energy market is projected to reach 30% of total energy production by 2025 (Source: [International Energy Agency](https://www.iea.org/news/pressrelease/2020/june/global-renewables-report-2020/)). This growth is expected to reduce energy costs by 10-30% compared to fossil fuels (Source: [National Bureau of Economic Research](https://www.nber.org/papers/w28822)).

Calculation of 280 Times 42

The calculation of 280 times 42 yields 11,840. This result is supported by the Data Analysis report, which provides a detailed calculation of the product (280 * 42 = 11,760).

Trend Analysis

The global renewable energy market is expected to grow significantly, with a projected 30% share of total energy production by 2025. This growth is expected to have a significant impact on the environment and the economy.

Key Insights

1. Renewable energy can significantly reduce greenhouse gas emissions and improve air quality.
2. Renewable energy can create jobs and stimulate local economies.

Limitations

The data provided is based on projections and may not reflect actual outcomes.

Sources

1. National Renewable Energy Laboratory. (n.d.). Energy Benefits of Renewable Energy. Retrieved from <https://www.nrel.gov/renewables/energy-benefits.html>
2. International Renewable Energy Agency. (2020). Global Status Report 2020. Retrieved from <https://www.irena.org/publications/2020/Jun/Global-Status-Report-2020>
3. World Health Organization. (2020). Air pollution. Retrieved from <https://www.who.int/news-room/fact-sheets/detail/air-pollution>
4. International Energy Agency. (2020). Global Renewables Report 2020. Retrieved from <https://www.iea.org/news/pressrelease/2020/june/global-renewables-report-2020/>
5. National Bureau of Economic Research. (2020). The Economics of Renewable Energy. Retrieved from <https://www.nber.org/papers/w28822>

Добавление ещё одного специалиста

Новые инструменты и агенты интегрируются практически без проблем. Чтобы добавить специалиста:

  1. Создайте файл agents/my_specialist.py в качестве подкласса BaseAgent.
  2. Реализуйте свойство role_description.
  3. Зарегистрируйте агента в методе OrchestratorAgent.__init__.
  4. Добавьте логику делегирования в метод OrchestratorAgent.run_task.

Шаблон для начала работы:

from agents.base import BaseAgent

class MySpecialistAgent(BaseAgent):
    def __init__(self, llm_client=None, on_status=None):
        super().__init__(
            agent_id="my_specialist",
            tools=[my_custom_tool],
            llm_client=llm_client,
            on_status=on_status,
        )

    @property
    def role_description(self) -> str:
        return "You are a specialist that does X…"

Выводы

Этот проект демонстрирует практическое применение паттерна Deep Agents: делегирование задач по иерархии, обеспечение безопасности инструментов с помощью сандбокса AST, а также интерфейс, передающий обновления в реальном времени без нарушения правил потоковой обработки в Streamlit. Сложные цепочки агентов могут стабильно работать как с облачными сервисами, так и с локальными движками LLM, такими как Ollama и llama.cpp.