Strona główna / Artykuły

Tworzenie systemu badawczego z wieloma agentami przy użyciu LangChain Deep Agents

Praktyczny przegląd wzorca Deep Agents: orkiestrator, specjalistyczne agenty, bezpieczne narzędzia oraz interfejs Streamlit, przetestowane z lokalnymi i chmurowymi modelami językowymi.

Wydawca: REACTAPP.TOP10 min czytania2403 słów

Źródła: Deep Agents in Action: Building a Multi-Agent Research System

Pętle oparte na pojedynczych poleceniach ustępują miejsca systemom wieloagentowym. Nowa struktura dzieli prace, zachowuje stan między poszczególnymi krokami i dostarcza każdemu „pracownikowi” wąski zestaw narzędzi. Test jednego z takich orkiestratorów, Deep Agents od LangChain, stanowi doskonały przykład studium przypadku – poniżej opisano, czym się on charakteryzuje oraz jak na jego bazie stworzono małą aplikację badawczą.

Czym zajmuje się architektura Deep Agents

LangChain niedawno ogłosiło zarządzaną platformę do uruchamiania topologii głębokich agentów. Ten model ma na celu przezwyciężenie trzech słabości pojedynczego agenta pracującego nad długim zadaniem: pogarsza się okno kontekstowe, agent ma trudności z wyborem odpowiedniego narzędzia, a błędy w rozumowaniu nasilają się w miarę przybywania kroków.

Struktura ta ma postać hierarchii:

  • Orkiestrator najwyższego poziomu dzieli skomplikowane zadanie na części i przekazuje każdą z nich specjaliście.
  • Każdy specjalista uruchamia swój własny, izolowany pętlę ReAct z własnymi narzędziami i pamięcią.
  • Specjaliści wysyłają z powrotem ustrukturyzowane wyniki, a orkiestrator łączy je ze sobą.
  • Części te są wzajemnie powiązane, więc orkiestrator może sam pełnić rolę podagenta kogoś innego.

W porównaniu z jednym agentem, który robi wszystko sam, różnica wygląda następująco:

| 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            |

Dokumentacja samego frameworka opisuje go jako najszybszą drogę do agentów sterowanych przez LLM-y, z wbudowanymi systemami plików do zarządzania kontekstem, tworzenia podagentów oraz pamięci długoterminowej. Planowanie zadań i umiejętności są dodatkami opcjonalnymi. Wymienia następujące możliwości: działanie w środowisku za pomocą narzędzi, plików i wykonywania kodu; pobieranie pamięci, umiejętności i wiedzy specjalistycznej we właściwym momencie; podsumowywanie historii i usuwanie dużych wyników poza kontekst; delegowanie zadań podagentom w oddzielnych oknach kontekstowych; pauzowanie w kluczowych momentach w oczekiwaniu na zatwierdzenie człowieka; oraz aktualizowanie pamięci, umiejętności i promptów na podstawie rzeczywistego użytkowania.

Repozytorium projektu informuje, że jest to narzędzie o otwartym kodzie źródłowym, zaprojektowane w określony sposób, gotowe do użycia od razu, a jego każdą część można rozszerzyć, przejąć lub zastąpić. Domyślne ustawienia są dostosowane do długotrwałych, wieloetapowych zadań; akceptowany jest każdy model wywoływania narzędzi (frontier, open-weight lub lokalny). Narzędzie opiera się na LangGraph do przepływu danych, przechowywania informacji i tworzenia punktów kontrolnych, a śledzenie działania, ocena oraz wdrażanie odbywają się za pośrednictwem LangSmith. Lista funkcji obejmuje podagenty z izolowanym kontekstem, system plików oparty na dostępnych backendach, zarządzanie kontekstem umożliwiające zapisywanie wyników narzędzi na dysku, dostęp do wiersza poleceń w środowisku sandbox, pamięć trwałą, zatwierdzanie wywołań narzędzi przez człowieka, umiejętności wielokrotnego użycia oraz dostosowane narzędzia lub dowolny serwer MCP.

Główne koncepcje

  • Hierarchiczna orkiestracja. Agent nadzorczy otrzymuje cel na wysokim poziomie i przekazuje podzadania specjalistom, zamiast przechodzić przez każdy krok w jednym pętli.
  • Specjalistyczne role. Każdy agent ma ściśle określone instrukcje systemowe, własne narzędzia oraz oddzielne okno kontekstowe do wykonywania zadań takich jak wyszukiwanie, obliczenia czy tworzenie dokumentów.
  • Izolowana pamięć. Każdy agent posiada własną pamięć roboczą, fakty oraz informacje o zdarzeniach, dzięki czemu stan jednej fazy nie przenika do drugiej.
  • Rozumowanie typu ReAct. Podagenty powtarzają cykle Myślenie, Działanie, Obserwacja przy użyciu zweryfikowanych interfejsów narzędziowych.

Implementacja

Aplikacja demonstracyjna to niewielka, dydaktyczna konstrukcja stworzona przy użyciu IBM Bob, która może działać lokalnie lub w chmurze. Implementuje wzorzec Deep Agents w umiarkowanym skali.

Zadanie przepływa przez krótki drzewo strukturalne:

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

Cztery warstwy utrzymują to w całości:

  1. Orchestracja (agents/orchestrator.py) przyjmuje dane od użytkownika, ocenia ich złożoność, tworzy plan, deleguje podzadania kolejno i łączy wyniki.
  2. Specjaliści (agents/specialists.py) definiują ResearcherAgent, AnalystAgent oraz WriterAgent.
  3. Narzędzia (tools/) zapewniają bezpieczną obliczalność za pomocą AST, izolowaną eksploatację Pythona, abstrakcję operacji wyszukiwania w sieci oraz zabezpieczone operacje wejścia/wyjścia plików.
  4. Przekaz i interfejs użytkownika (app.py i main.py) oferują interfejs Streamlit, który asynchronicznie synchronizuje stan pomiędzy wątkami i w czasie rzeczywistym śledzi poszczególne fazy, a także narzędzia do linii poleceń.

Stack składa się z zwykłego Pythona z Streamlit do tworzenia interfejsu. Do lokalnej inferyencji został przetestowany z Ollamą, a llama.cpp jest również obsługiwany poprzez konfigurację.

Struktura folderów wynikająca z wymagań:

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

Konfiguracja jest elastyczna, dzięki czemu kod dostosowuje się do powszechnych ustawień. Serwer może komunikować się z lokalnymi środowiskami wykonywania, takimi jak llama.cpp i Ollama, lub z dostawcami chmury za pośrednictwem kluczy API. Plik środowiskowy z komentarzami opisuje wszystkie ustawienia:

# =============================================================================
# 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

Kod wart przejrzenia

Aplikacja ma na celu pokazanie, w jaki sposób zadania są dzielone i przekazywane do różnych zestawów narzędzi. Została zaprojektowana tak, by można ją było rozszerzać, dzięki czemu dodatkowe narzędzia i specjaliści mogą być łatwo włączeni.

Bezpieczny ewaluator arytmetyki

Pozwolenie modelowi na wykonywanie dowolnego kodu do obliczeń matematycznych stanowiłoby lukę bezpieczeństwa. Dlatego kalkulator analizuje wyrażenia za pomocą modułu ast w Pythonie i przyjmuje tylko wyraźnie określoną listę dozwolonych typów węzłów i funkcji, nigdy surowy kod 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__}")

Egzekutor kodu w środowisku izolowanym

Dla bardziej intensywnych zadań z danymi egzekutor zapewnia izolowany przestrzeń nazw z ograniczonymi wbudowanymi funkcjami, kilkoma już zaimportowanymi modułami naukowymi, przekierowanym wynikiem wyjściowym oraz czasem wygaśnięcia opartym na wątkach:

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.")

Bезpieczny relaj do pracy z interfejsem Streamlit

Wątki robocze Streamlit nie mogą bezpośrednio zapisywać do st.session_state, ponieważ brakuje im obiektu ScriptRunContext. Aplikacja rozwiązuje ten problem za pomocą relaju typu singleton, który jest przechowywany w pamięci cache za pomocą @st.cache_resource i chroniony kluczem reentrantnym (threading.RLock), co umożliwia przesyłanie danych pomiędzy wątkami do strony:

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()

Przykład zrealizowany: zmiany klimatyczne

Zadanie polegało na analizie wpływu rosnących stężeń CO2 na ekosystemy na całym świecie, w tym aktualnych poziomów CO2, procentowego wzrostu od czasów przedprzemysłowych oraz rekomendacji politycznych na zakończenie.

Kolejne kroki:

  1. Koordynator dzieli zadanie na badania, analizę i pisanie.
  2. Naukowiec przeprowadza trzy wyszukiwania w internecie dotyczące trendów CO2, wpływu na ekosystemy oraz ram politycznych.
  3. Analizator używa narzędzia kalkulatora: (421 - 280) / 280 * 100, co daje przybliżony wzrost o 50,36%.
  4. Pisarz tworzy ustrukturyzowany dokument w formacie Markdown zawierający streszczenie, wyniki badań, analizę danych oraz rekomendacje.
  5. Koordynator łączy wszystko w ostateczny raport.

Wynik jest zapisywany w pliku output/deep_agents_report_<timestamp>.md, a przykład wygląda następująco:

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>

Dodawanie kolejnego specjalisty

Nowe narzędzia i agenci integrują się bez większych trudności. Aby dodać specjalistę:

  1. Stwórz plik agents/my_specialist.py jako podklasę BaseAgent.
  2. Zaimplementuj właściwość role_description.
  3. Zarejestruj agenta w metodzie OrchestratorAgent.__init__.
  4. Dodaj logikę delegacji w metodzie OrchestratorAgent.run_task.

Szkielet, od którego można zacząć:

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…"

Podsumowanie

Projekt pokazuje, jak w praktyce funkcjonuje wzorzec Deep Agents: delegowanie zadań wzdłuż hierarchii, bezpieczne narzędzia dzięki sandboxowi AST oraz interfejs użytkownika, który przesyła aktualizacje bez naruszania zasad wątkowania Streamlit. Złożone łańcuchy agentów mogą pracować niezawodnie zarówno z usługami chmurowymi, jak i lokalnymi środowiskami LLM, takimi jak Ollama i llama.cpp.