Construcción de un sistema de investigación multiagente con LangChain Deep Agents
Una visión práctica del patrón Deep Agents: un orquestador, agentes especializados, herramientas seguras y una interfaz frontal en Streamlit, probado con modelos LLM locales y en la nube.
Fuentes: Deep Agents in Action: Building a Multi-Agent Research System
Los bucles de instrucción única están dando paso a sistemas multiagente. La nueva configuración divide el trabajo, mantiene el estado entre pasos y proporciona a cada agente un conjunto limitado de herramientas. Una prueba con otro orquestador basado en esta idea, Deep Agents de LangChain, constituye un buen estudio de caso; a continuación se explica qué hace y cómo se creó una pequeña aplicación de investigación sobre su base.
Qué aborda la arquitectura de Deep Agents
LangChain anunció recientemente una plataforma gestionada para ejecutar topologías de agentes profundos. Este enfoque resuelve tres debilidades de un agente individual al trabajar en tareas extensas: la ventana de contexto se deteriora, el agente se confunde sobre qué herramienta elegir y los errores de razonamiento se acumulan a medida que avanzan los pasos.
Su estructura es jerárquica:
- Un orquestador de nivel superior divide una tarea compleja en partes y las asigna a un especialista cada una.
- Cada especialista ejecuta su propio bucle ReAct aislado, con herramientas y memoria propias.
- Los especialistas envían resultados estructurados, y el orquestador los combina.
- Las partes se anidan entre sí, de modo que el orquestador puede funcionar como subagente de otro.
En comparación con un único agente que hace todo, la diferencia es la siguiente:
| 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 |
La propia documentación del framework lo describe como la vía más rápida hacia agentes impulsados por LLMs, con sistemas de archivos para gestionar el contexto, generación de subagentes y memoria a largo plazo integrados. La planificación de tareas y las habilidades son extras opcionales. Enumera estas capacidades: actuar en un entorno a través de herramientas, archivos y ejecución de código; recuperar memorias, habilidades y conocimientos del dominio en el momento adecuado; resumir la historia y sacar resultados extensos del contexto; delegar tareas a subagentes en ventanas de contexto separadas; hacer pausas para obtener la aprobación humana en puntos críticos; y actualizar la memoria, las habilidades y los prompts a partir del uso real.
El repositorio del proyecto indica que se trata de un framework de código abierto con especificaciones definidas que funciona sin necesidad de configuración adicional, y permite extender, sobrescribir o reemplazar cualquier parte de él. Los valores predeterminados están ajustados para tareas largas y multifase; se aceptan cualquier modelo de llamada a herramientas (frontier, open-weight o local). Se basa en LangGraph para la transmisión en tiempo real, la persistencia y la creación de puntos de control, e incluye funciones de seguimiento, evaluación e implementación a través de LangSmith. La lista de características abarca subagentes con contexto aislado, un sistema de archivos basado en backends intercambiables, gestión de contexto que guarda la salida de las herramientas en disco, acceso a shell en un entorno aislado, memoria persistente, aprobación humana de las llamadas a herramientas, habilidades reutilizables, así como herramientas personalizadas o cualquier servidor MCP.
Ideas clave
- Orquestación jerárquica. Un agente supervisor recibe un objetivo de alto nivel y asigna subtareas a especialistas, en lugar de procesar cada paso en un único bucle.
- Papeles especializados. Cada agente cuenta con una instrucción del sistema bien definida, sus propias herramientas y una ventana de contexto independiente, para tareas como la recuperación de información, cálculos o redacción de documentos.
- Memoria aislada. Cada agente mantiene su propia memoria de trabajo, datos e historial, de modo que el estado de una fase no se filtra a otra.
- Razonamiento ReAct. Los subagentes repiten ciclos de Pensamiento, Acción y Observación mediante interfaces de herramientas validadas.
La implementación
La aplicación de demostración es una construcción pequeña y didáctica, desarrollada con IBM Bob, que funciona localmente o contra modelos en la nube. Sigue el patrón de Deep Agents a una escala moderada.
La tarea se procesa a través de un árbol corto:
User Task
└─ OrchestratorAgent (Planning & Synthesis)
├─ ResearcherAgent (web_search)
├─ AnalystAgent (calculator, code_executor)
└─ WriterAgent (file_reader)
Hay cuatro capas que lo mantienen unido:
- Orquestación (
agents/orchestrator.py) recibe la entrada del usuario, evalúa su complejidad, elabora un plan, asigna las subtareas en secuencia y sintetiza los resultados. - Especialistas (
agents/specialists.py) definen unResearcherAgent, unAnalystAgenty unWriterAgent. - Herramientas (
tools/) ofrecen cálculos aritméticos evaluados de forma segura a través del AST, ejecución aislada de Python, una abstracción para búsquedas en la web y operaciones de entrada/salida de archivos en un entorno aislado. - Relevamiento e interfaz de usuario (
app.pyymain.py) proporcionan una interfaz frontal de Streamlit que sincroniza el estado entre hilos de forma asíncrona y muestra en tiempo real las fases del proceso, además de una herramienta de línea de comandos.
La estructura está hecha con Python puro y Streamlit para la interfaz. Para inferencia local se probó con Ollama, y también se admite llama.cpp a través de la configuración.
La estructura de carpetas que surgió de los requisitos:
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
La configuración es flexible, por lo que el código se adapta a las configuraciones comunes. El backend puede comunicarse con entornos locales como llama.cpp y Ollama, o con proveedores en la nube a los que se accede mediante claves API. Un archivo de entorno comentado detalla los parámetros necesarios:
# =============================================================================
# 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
Código que merece ser analizado
La aplicación existe para mostrar cómo se dividen las tareas y se asignan a conjuntos de herramientas distintos. Está diseñada para ser ampliada, por lo que se pueden integrar fácilmente herramientas adicionales y especialistas extra.
Evaluador aritmético seguro
Permitir que un modelo ejecute código arbitrario para cálculos matemáticos supondría una brecha de seguridad. Por eso, la calculadora analiza las expresiones con el módulo ast de Python y solo acepta una lista blanca explícita de tipos de nodos y funciones, nunca un eval() en bruto:
# 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__}")
Ejecutor de código en entorno aislado
Para tareas con grandes volúmenes de datos, el ejecutor ofrece un espacio de nombres aislado con funciones integradas restringidas, algunos módulos científicos ya importados, redirección de la salida y un tiempo de espera basado en hilos:
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.")
Un relé seguro para múltiples hilos en la interfaz de Streamlit
Los hilos de trabajo de Streamlit no pueden escribir directamente en st.session_state, ya que carecen de un ScriptRunContext. La aplicación soluciona esto mediante un relé de tipo singleton almacenado en caché con @st.cache_resource y protegido por un bloqueo reentrante (threading.RLock), lo que permite que los pasos del agente fluyan a través de los límites entre hilos hacia la página:
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()
Un ejemplo práctico: el cambio climático
La tarea inicial consistía en analizar cómo las crecientes concentraciones de CO2 afectan a los ecosistemas en todo el mundo, incluyendo los niveles actuales de CO2, el porcentaje de aumento desde la época preindustrial y un conjunto final de recomendaciones políticas.
Qué sucede a continuación:
- El coordinador divide el trabajo en investigación, análisis y redacción.
- El investigador realiza tres búsquedas en la web sobre las tendencias del CO2, el impacto en los ecosistemas y los marcos normativos.
- El analista utiliza la herramienta de cálculo:
(421 - 280) / 280 * 100, lo que arroja un aumento aproximado del 50,36 %. - El redactor elabora un documento estructurado en Markdown con un resumen ejecutivo, hallazgos, análisis de datos y recomendaciones.
- El coordinador combina todo en el informe final.
El resultado se guarda en output/deep_agents_report_<timestamp>.md, y una muestra se ve así:
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>
Agregar otro especialista
Los nuevos herramientas y agentes se integran sin grandes dificultades. Para agregar un especialista:
- Cree
agents/my_specialist.pycomo una subclase deBaseAgent. - Implemente la propiedad
role_description. - Registre el agente en
OrchestratorAgent.__init__. - Agregue la lógica de delegación en
OrchestratorAgent.run_task.
Ejemplo básico para comenzar:
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…"
Conclusión
El proyecto demuestra en la práctica el funcionamiento del patrón Deep Agents: delegación a lo largo de una jerarquía, herramientas seguras gracias a un entorno aislado tipo AST, y una interfaz de usuario que transmite actualizaciones sin violar las reglas de hilos de Streamlit. Los pipelines complejos de agentes pueden ejecutarse de manera fiable tanto en endpoints en la nube como en entornos locales de LLM como Ollama y llama.cpp.