Startseite / Artikel

Aufbau eines Mehr-Agenten-Forschungssystems mit LangChain Deep Agents

Ein praktischer Überblick über das Deep Agents-Muster: ein Orchesterator, spezialisierte Agenten, sichere Tools sowie eine Streamlit-Oberfläche, getestet an lokalen und cloud-basierten LLMs.

Verlag: REACTAPP.TOP10 Min. Lesezeit2403 Wörter

Quellen: Deep Agents in Action: Building a Multi-Agent Research System

Einfache Prompt-Schleifen weichen zunehmend mehragentigen Systemen. Die neue Architektur verteilt die Aufgaben, speichert den Zustand zwischen den Schritten und stellt jedem „Arbeiter“ eine begrenzte Auswahl an Werkzeugen zur Verfügung. Ein Testlauf eines weiteren Orchestriersystems, das auf dieser Idee basiert – LangChain’s Deep Agents – ist ein gutes Fallbeispiel; hier wird erklärt, wozu es dient und wie eine kleine Forschungsanwendung darauf aufgebaut wurde.

Was die Deep Agents-Architektur löst

LangChain hat kürzlich eine verwaltete Plattform für den Betrieb solcher Deep-Agent-Topologien vorgestellt. Dieses Konzept behebt drei Schwächen eines einzelnen Agenten bei der Erledigung langer Aufgaben: Das Kontextfenster verschlechtert sich, der Agent ist unsicher, welches Werkzeug er verwenden soll, und Fehler im Denken häufen sich mit zunehmenden Schritten.

Die Struktur ist hierarchisch:

  • Ein oberster Orchestrierer teilt eine komplexe Aufgabe in Teilaufgaben auf und weist jede davon einem Spezialisten zu.
  • Jeder Spezialist führt seinen eigenen isolierten ReAct-Loop mit eigenen Tools und Speicherressourcen aus.
  • Die Spezialisten senden strukturierte Ergebnisse zurück, und der Orchestrator verknüpft sie miteinander.
  • Die Komponenten sind hierarchisch angeordnet, sodass ein Orchestrator selbst als Sub-Agent eines anderen dienen kann.

Verglichen mit einem Agenten, der alles selbst erledigt, sieht der Unterschied so aus:

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

Die eigene Dokumentation des Frameworks beschreibt es als den schnellsten Weg zu von LLMs gesteuerten Agenten, wobei Dateisysteme zur Verwaltung des Kontexts, zum Erstellen von Unteragenten sowie zu einer integrierten Langzeitmerkfunktion vorhanden sind. Aufgabenplanung und Fähigkeiten sind optionale Erweiterungen. Es nennt folgende Funktionen: Handeln in einer Umgebung durch Tools, Dateien und Ausführung von Code; Abrufen von Erinnerungen, Fähigkeiten und Fachwissen zum richtigen Zeitpunkt; Zusammenfassen der Vorgeschichte sowie Ausblenden großer Ergebnisse aus dem Kontext; Delegieren an Unteragenten in separaten Kontextfenstern; Pausieren zur menschlichen Freigabe an entscheidenden Punkten sowie Aktualisierung von Erinnerungen, Fähigkeiten und Anweisungen auf Basis der tatsächlichen Nutzung.

Der Repository des Projekts gibt an, dass es sich um eine Open-Source-Lösung handelt, die ohne weitere Einstellungen funktioniert und deren jeder Teil erweitert, überschrieben oder ersetzt werden kann. Die Standardeinstellungen sind für langfristige, mehrschrittige Aufgaben optimiert; jedes Tool-Aufruf-Modell wird akzeptiert (Frontier, Open-Weight oder lokal). Die Lösung basiert auf LangGraph für Streaming, Persistenz und Checkpointing sowie für Tracing, Bewertung und Bereitstellung über LangSmith. Zu den Funktionen gehören Sub-Agenten mit isoliertem Kontext, ein Dateisystem über plattformunabhängige Backends, eine Kontextverwaltung, die Tool-Ausgaben auf der Festplatte speichert, Shell-Zugriff in einer Sandbox, persistente Speicherung, menschliche Freigabe von Tool-Aufrufen, wiederverwendbare Fähigkeiten sowie benutzerdefinierte Tools oder beliebige MCP-Server.

Kernideen

  • Hierarchische Orchestrierung. Ein Überwachungsagent erhält ein übergeordnetes Ziel und weist Unteraufgaben an Spezialisten zu, anstatt jeden Schritt in einer einzigen Schleife durchzuführen.
  • Spezialisierte Rollen. Jeder Agent verfügt über einen präzise definierten Systemauftrag, eigene Tools sowie ein eigenes Kontextfenster für Aufgaben wie Informationsabruf, Berechnung oder Erstellung von Dokumenten.
  • Isolierte Speicherung. Jeder Agent behält seinen eigenen Arbeitspeicher sowie Fakten und Ereignisse bei, sodass der Zustand einer Phase nicht in eine andere übergeht.
  • ReAct-Logik. Unteragente wiederholen die Zyklen Gedanke, Aktion, Beobachtung anhand validierter Tool-Interfaces.

Die Implementierung

Die Demo-App ist eine kleine, lehrreiche Anwendung, die mit IBM Bob entwickelt wurde und lokal oder gegen Cloud-Modelle ausgeführt werden kann. Sie folgt dem Deep Agents-Muster in begrenztem Umfang.

Die Aufgabe verläuft über einen kurzen Baum:

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

Vier Schichten halten es zusammen:

  1. Orchestrierung (agents/orchestrator.py) nimmt die Eingabe des Benutzers entgegen, beurteilt deren Komplexität, erstellt einen Plan, delegiert Aufgaben nacheinander und synthetisiert die Ergebnisse.
  2. Spezialisten (agents/specialists.py) definieren einen ResearcherAgent, einen AnalystAgent und einen WriterAgent.
  3. Werkzeuge (tools/) bieten sichere arithmetische Berechnungen über den AST, isolierte Python-Exekution, eine Abstraktion für Web-Suche sowie sandbox-basierte Datei-Eingabe/Ausgabe.
  4. Vermittlung und Benutzeroberfläche (app.py und main.py) stellen eine Streamlit-Oberfläche bereit, die den Zustand zwischen Threads asynchron synchronisiert und die Phasen in Echtzeit verfolgt, sowie eine Kommandozeilenschnittstelle.

Die Basis ist reines Python mit Streamlit für die Benutzeroberfläche. Für lokale Inferenz wurde es mit Ollama getestet, und llama.cpp ist ebenfalls über die Konfiguration unterstützt.

Die aus den Anforderungen resultierende Ordnerstruktur:

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

Die Konfiguration ist flexibel, sodass der Code sich an gängige Einrichtungen anpasst. Der Backend kann mit lokalen Laufzeiten wie llama.cpp und Ollama oder mit über API-Schlüssel erreichbaren Cloud-Anbietern kommunizieren. Eine kommentierte Umgebungsdatei enthält die Einstellungen:

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

Kodex, der sich lohnt

Die App dient dazu, zu zeigen, wie Aufgaben aufgeteilt und an verschiedene Werkzeugsätze weitergeleitet werden. Sie ist so konzipiert, dass sie erweitert werden kann, sodass zusätzliche Tools und Spezialisten leicht integriert werden können.

Ein sicheres Arithmetik-Evaluierungsprogramm

Es wäre ein Sicherheitsrisiko, einem Modell die Ausführung beliebigen Codes für mathematische Berechnungen zu ermöglichen. Deshalb analysiert die Rechenmaschine Ausdrücke mit Pythons ast-Modul und akzeptiert nur eine explizit definierte Liste von Knotentypen und Funktionen – niemals einen rohen eval()-Aufruf:

# 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__}")

Ein in einer Sandbox ausgeführter Code-Executor

Für aufwändigere Datenverarbeitungen bietet der Executor einen isolierten Namensraum mit eingeschränkten Standardfunktionen, bereits importierten wissenschaftlichen Modulen, umgeleitetem Ausgabeinhalt sowie einer zeitbasierten Timeout-Funktion basierend auf Threads:

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

Ein thread-sicheres Relais für die Streamlit-Oberfläche

Die Worker-Threads von Streamlit können nicht direkt in st.session_state schreiben, da ihnen ein ScriptRunContext fehlt. Die Anwendung umgeht dieses Problem mithilfe eines Singleton-Relais, das über @st.cache_resource gekachtet und durch einen reentranten Schutzmechanismus (threading.RLock) abgesichert wird. Dadurch können Agentenschritte über Threadgrenzen hinweg in die Seite gestreamt werden:

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

Ein Beispiel: Klimawandel

Die Aufgabenstellung bestand darin, zu analysieren, wie steigende CO2-Konzentrationen Ökosysteme weltweit beeinflussen – einschließlich der aktuellen CO2-Werte, des Prozentsatzanstiegs seit der Vorkindheitszeit sowie abschließender politischer Empfehlungen.

Was danach passiert:

  1. Der Koordinator teilt die Aufgabe in Forschung, Analyse und Verfassen auf.
  2. Der Forscher führt drei Websuchen durch, die sich auf CO2-Trends, Auswirkungen auf Ökosysteme und politische Rahmenbedingungen beziehen.
  3. Der Analyst ruft das Rechenwerkzeug auf: (421 - 280) / 280 * 100, was einen Anstieg von etwa 50,36 % ergibt.
  4. Der Verfasser erstellt ein strukturiertes Markdown-Dokument mit einem Executive Summary, Ergebnissen, Datenanalyse und Empfehlungen.
  5. Der Koordinator fügt alles zusammen zu dem endgültigen Bericht.

Das Ergebnis wird in output/deep_agents_report_<timestamp>.md gespeichert, und ein Beispiel sieht so aus:

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>

Einen weiteren Spezialisten hinzufügen

Neue Tools und Agenten integrieren sich nahezu reibungslos. Um einen Spezialisten hinzuzufügen:

  1. Erstellen Sie agents/my_specialist.py als Unterklasse von BaseAgent.
  2. Implementieren Sie die Eigenschaft role_description.
  3. Registrieren Sie den Agenten in OrchestratorAgent.__init__.
  4. Fügen Sie die Delegierungslogik in OrchestratorAgent.run_task hinzu.

Ein Skelett zum Ausgangspunkt:

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

Zusammenfassung

Das Projekt zeigt das Deep Agents-Muster in der Praxis: Delegation entlang einer Hierarchie, Tools, die durch eine AST-Sandbox sicher gemacht werden, sowie eine Benutzeroberfläche, die Updates streamt, ohne die Thread-Regeln von Streamlit zu verletzen. Komplexe Agent-Pipelines können zuverlässig sowohl gegen Cloud-Endpunkte als auch gegen lokale LLM-Runtimes wie Ollama und llama.cpp ausgeführt werden.