// AGENTES · MÓDULO 03/12

Agentes y LangGraph: orquestación para decisiones de inversión

create_agent, middleware, StateGraph, persistencia, human-in-the-loop real y patrones multi-agente.

Lectura ~35 minNivel intermedio-avanzadoEdición jul 2026Read in English


En el módulo 2 se construyeron pipelines deterministas con LCEL: cadenas de Runnables componibles con salida estructurada. El salto de este módulo es pequeño en la superficie y profundo en las consecuencias: un agente es un grafo compilado que hereda la interfaz Runnable. Lo que cambia no es el mecanismo de invocación — sigue siendo invoke/stream/batch — sino quién decide el camino: ya no el programador, sino el modelo, turno a turno. Esa cesión de control es lo que hay que aprender a acotar, auditar y, en los puntos críticos, revocar. Estos patrones reaparecerán en el módulo 5 (RAG como herramienta del agente), el módulo 7 (el risk gate como nodo determinista) y el capstone del módulo 12.

3.1 create_agent: el estándar v1

3.1.1 La factory sobre LangGraph: model + tools + system_prompt → CompiledStateGraph

Desde el GA de LangChain 1.0 (octubre de 2025), create_agent —en langchain.agents— es la forma estándar de construir agentes. (Github) Por debajo, compila el bucle agéntico clásico (llamar al modelo, dejar que elija herramientas, ejecutarlas, devolver el resultado al modelo, y terminar cuando no hay más tool calls) como un grafo LangGraph: create_agent corre sobre LangGraph; LangChain para empezar rápido, LangGraph para orquestación a medida. (Github)

El punto pedagógicamente decisivo es el tipo de retorno: create_agent no devuelve un objeto opaco sino un CompiledStateGraph. (Github) Tres consecuencias prácticas. Primera, el agente se invoca como cualquier grafo: agent.invoke({"messages": [...]}). Segunda, puede anidarse como nodo o subgrafo dentro de un StateGraph propio — la base de los equipos multi-agente de la sección 3.3. Tercera, todo lo que LangGraph ofrece se hereda sin código adicional: durabilidad (checkpoints tras cada paso), streaming de tokens y estados, human-in-the-loop (interrupciones y reanudación) y time travel (replay y fork desde checkpoints históricos). (PrivacyFilter)

Las dos APIs que este curso no enseña: AgentExecutor, que vive en el paquete de compatibilidad langchain-classic, (MDPI) y langgraph.prebuilt.create_react_agent, deprecado en LangGraph v1 con eliminación prevista en v2.0. (is4.ai) La guía oficial de migración documenta la correspondencia: hooks pre_model_hook/post_model_hook → middleware (before_model/after_model); prompt → system_prompt; prompts dinámicos → @dynamic_prompt; estado custom → solo TypedDict; contexto de ejecución → argumento context en lugar de config["configurable"]. (Github)

El primer agente del curso es un analista de research con dos herramientas deterministas — una consulta de precios y un calculador de métricas — bajo el principio rector del curso: el LLM nunca calcula, orquesta herramientas que sí lo hacen.

python
from langchain.agents import create_agent
from langchain.tools import tool

@tool
def get_ohlcv(ticker: str, start: str, end: str) -> dict:
    """Devuelve OHLCV diario de un ticker entre dos fechas (point-in-time)."""
    ...  # implementación contra la API de market data del módulo 4

@tool
def compute_rsi(prices: list[float], window: int = 14) -> float:
    """Calcula el RSI de Wilder sobre una serie de cierres."""
    ...  # matemática determinista, testeable por separado

analyst = create_agent(
    model="claude-sonnet-4-6",
    tools=[get_ohlcv, compute_rsi],
    system_prompt=(
        "Eres un analista cuantitativo senior. Usa las herramientas para "
        "obtener datos y calcular métricas; nunca estimes cifras de memoria. "
        "Cierra con un veredicto BULLISH / BEARISH / NEUTRAL justificado."
    ),
)

result = analyst.invoke({
    "messages": [{"role": "user",
                  "content": "Evalúa el momentum de NVDA en el último trimestre."}]
})

El bucle que ejecuta ese invoke — modelo, herramientas, modelo — es ahora un grafo con estado persistente en cada arista:

Grafo de agente: el modelo llama a tools deterministas hasta que no quedan tool_calls, y el checkpointer guarda el estado tras cada paso.START → model LLM decide; model LLM decide → tools ejecución determinista (tool_calls); tools ejecución determinista → model LLM decide; model LLM decide → END (sin tool_calls); model LLM decide → checkpointer estado tras cada paso (persiste); tools ejecución determinista → checkpointer estado tras cada paso (persiste)tool_callssin tool_callspersistepersisteSTARTmodelLLM decidetoolsejecución deterministaENDcheckpointerestado tras cada paso
Bucle del agente como grafo. El modelo decide si llama a tools o termina, y el checkpointer persiste el estado tras cada paso del bucle.

3.1.2 Middleware: la capa de control del bucle

Middleware es la vía oficial de v1 para controlar lo que ocurre dentro del agente: logging, transformación de prompts y selección de herramientas, reintentos y terminación temprana, rate limits, guardarraíles y detección de PII. (futureagi.com) El modelo de extensión son seis hooks — node-style (@before_agent, @before_model, @after_model, @after_agent) y wrap-style (@wrap_model_call, @wrap_tool_call) — más el decorador @dynamic_prompt. (langchain.js)

Para un desk cuantitativo, cuatro middleware built-in cubren la pila defensiva mínima: SummarizationMiddleware (comprime el historial al superar un umbral de tokens — imprescindible en sesiones de research largas), (Github) HumanInTheLoopMiddleware (pausa antes de herramientas sensibles; sección 3.2.3), (🦜️🔗 LangChain) PIIMiddleware (detecta y enmascara datos personales antes de que salgan del perímetro) (Github) y model retry middleware (reintentos con backoff exponencial ante fallos de la API del modelo, añadido en langchain v1.1.0). (Github) El reintento también puede implementarse a mano con @wrap_model_call, lo que ilustra la mecánica wrap-style — el middleware recibe la petición y un handler, y decide cuántas veces invocarlo: (langchain.js)

python
from langchain.agents import create_agent
from langchain.agents.middleware import (
    wrap_model_call, ModelRequest, ModelResponse,
)
from typing import Callable

@wrap_model_call
def retry_model(
    request: ModelRequest,
    handler: Callable[[ModelRequest], ModelResponse],
) -> ModelResponse:
    for attempt in range(3):
        try:
            return handler(request)
        except Exception as e:
            if attempt == 2:
                raise
            print(f"Retry {attempt + 1}/3 after error: {e}")

Los system prompts dinámicos se construyen con @dynamic_prompt sobre ModelRequest, que expone el estado de la conversación (request.messages), el store de memoria larga (request.runtime.store) y el contexto de ejecución (request.runtime.context). (Ailog) La distinción que hay que interiorizar: context_schema define configuración inmutable por invocación — usuario, mesa, límites de riesgo — inyectada con agent.invoke(..., context=...); state_schema define el estado mutable del grafo (en v1, solo TypedDict); y el store es memoria cross-thread. (Ailog) El perfil de riesgo del usuario pertenece al contexto, no al prompt hardcodeado:

python
from dataclasses import dataclass
from langchain.agents import create_agent
from langchain.agents.middleware import dynamic_prompt, ModelRequest

@dataclass
class DeskContext:
    user_id: str
    desk: str                 # p. ej. "equity-ls", "macro"
    max_notional: float       # límite de notional por orden, en USD
    risk_profile: str         # "conservative" | "balanced" | "aggressive"

@dynamic_prompt
def risk_aware_prompt(request: ModelRequest) -> str:
    ctx: DeskContext = request.runtime.context
    return (
        "Eres un asistente de trading institucional. "
        f"Mesa: {ctx.desk}. Perfil de riesgo: {ctx.risk_profile}. "
        f"Nunca propongas órdenes con notional superior a {ctx.max_notional:,.0f} USD; "
        "si la tesis lo requiere, propón escalonar la entrada."
    )

agent = create_agent(
    model="claude-sonnet-4-6",
    tools=[get_ohlcv, compute_rsi],
    middleware=[risk_aware_prompt, retry_model],
    context_schema=DeskContext,
)

agent.invoke(
    {"messages": [{"role": "user", "content": "Prepara una propuesta long en MSFT."}]},
    context=DeskContext(user_id="pm-014", desk="equity-ls",
                        max_notional=2_000_000, risk_profile="balanced"),
)

Este mecanismo es la vía natural de personalización regulatoria: el mismo agente sirve a un PM con mandato amplio y a un analista junior con límites estrictos, y la diferencia viaja en un objeto tipado, auditable e inmutable — no en una cadena de prompt editable.

3.2 LangGraph en profundidad

3.2.1 StateGraph: canales, reducers y Command

Cuando el flujo lo debe decidir el código y no el modelo, se baja un nivel de abstracción: de create_agent a StateGraph. Un StateGraph es un builder cuyo estado es un TypedDict; cada campo se convierte en un canal con una función reductora opcional; los nodos son callables (state) -> partial_update; y las aristas pueden ser estáticas (add_edge), condicionales (add_conditional_edges) o implícitas vía Command. Al compilar, LangGraph valida el grafo (sin nodos huérfanos, destinos existentes, START con salidas) y construye un grafo Pregel con ejecución por super-pasos dirigida por canales. (51CTO)

La regla de reducción es la fuente número uno de bugs en grafos multi-nodo: sin reducer explícito, las escrituras a una clave sobrescriben (last-write-wins); con Annotated[..., reducer] se fusionan. (Github) El reducer built-in add_messages añade mensajes y actualiza existentes por id; Overwrite bypassa el reducer y fija el canal directamente — y solo un nodo puede usarlo sobre la misma clave en un super-paso, so pena de InvalidUpdateError. (Github) Traducción a trading: si cuatro analistas escriben en paralelo el campo report, sin reducer solo sobrevive uno. Por eso el curso enseña reducers antes que multi-agente.

python
from typing import Annotated, TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langchain_core.messages import AnyMessage

class ResearchState(TypedDict):
    messages: Annotated[list[AnyMessage], add_messages]  # se fusionan
    ticker: str                                          # sobrescritura simple
    reports: Annotated[list[str], lambda a, b: a + b]    # se acumulan

def market_analyst(state: ResearchState) -> dict:
    return {"reports": [f"technical: {state['ticker']} RSI en zona neutral"]}

def news_analyst(state: ResearchState) -> dict:
    return {"reports": [f"news: {state['ticker']} sin catalizadores 48h"]}

graph = (
    StateGraph(ResearchState)
    .add_node("market_analyst", market_analyst)
    .add_node("news_analyst", news_analyst)
    .add_edge(START, "market_analyst")
    .add_edge(START, "news_analyst")   # fan-out paralelo: ambos escriben `reports`
    .add_edge("market_analyst", END)
    .add_edge("news_analyst", END)
    .compile()
)

Para nodos que deben actualizar estado y enrutar a la vez, LangGraph ofrece Command, una primitiva con cuatro parámetros (update, goto, graph, resume) sobre la que se construyen los handoffs multi-agente de la sección 3.3. Advertencia de las propias docs: no mezclar aristas estáticas y goto para el mismo nodo — ambas se ejecutarían. (arXiv.org)

python
from typing import Literal
from langgraph.types import Command

def regime_router(state: ResearchState) -> Command[Literal["bull_researcher", "bear_researcher"]]:
    vol_regime = classify_regime(state["ticker"])  # función determinista
    return Command(
        update={"reports": [f"regime: {vol_regime}"]},
        goto="bull_researcher" if vol_regime == "risk-on" else "bear_researcher",
    )

Finalmente, los subgrafos: cualquier StateGraph compilado — incluido el que devuelve create_agent — puede añadirse como nodo de un grafo padre; las claves compartidas fluyen padre↔hijo y cada subgrafo gestiona su propio namespace de checkpoints. (PrivacyFilter) Es el mecanismo de composición de los equipos research/risk/execution del caso 3.3.2.

3.2.2 Persistencia: checkpoints, store y time travel

El checkpointer serializa el estado completo del grafo tras cada paso — habilitando pausa/reanudación, recuperación de fallos y auditoría — indexado por thread_id (un hilo = una conversación o un análisis). (IDEAS/RePEc) La elección de backend no es un detalle de infraestructura: es una decisión de compliance.

Tabla 3.1 — Criterios de decisión de checkpointer

EscenarioCheckpointerVentajaRiesgo / límite
Tests unitarios, desarrollo localMemorySaver / InMemorySaverCero setup, rápido, reinicia limpio entre testsTodo el estado se pierde al reiniciar el proceso; prohibido en producción
Servidor mono-proceso, baja concurrenciaSqliteSaverDurabilidad en fichero local, sin dependencia externa, escrituras ACIDLos write-locks de SQLite serializan hilos concurrentes; no escala horizontalmente
Despliegue multi-proceso / contenedoresPostgresSaver / AsyncPostgresSaverACID, consultable con SQL, dobla como audit log, escala con poolingRequiere connection pooling (asyncpg/psycopg_pool); conexiones bloqueantes paran el event loop
Flujos HITL con requisito de auditoríaPostgresSaver / AsyncPostgresSaverLas consultas SQL sobre la tabla de checkpoints sustituyen infraestructura de audit log separadaSin TTL nativo para hilos pausados: guardar timestamps del interrupt en el estado

Síntesis de la documentación oficial de persistencia y de guías de despliegue especializadas. (IDEAS/RePEc)

python
from langgraph.checkpoint.postgres import PostgresSaver

checkpointer = PostgresSaver.from_conn_string("postgresql://langgraph:****@db.internal/lg")
checkpointer.setup()  # crea tablas e índices
# Programar un cron que borre checkpoints más antiguos que la política de retención

Checkpointer y store no son intercambiables: el checkpointer persiste el estado por hilo (la sesión de análisis de hoy); el Store (BaseStore: InMemoryStore, PostgresStore) es la capa oficial de memoria cross-thread, con namespaces tipo tupla y búsqueda semántica. (estudy247.com) La separación en trading es nítida: el checkpointer guarda el debate bull/bear de esta mañana; el store guarda la memoria de tesis — lecciones por ticker y estrategia, accesibles desde cualquier sesión futura. Es la versión productizada del log reflexivo que TradingAgents usa como memoria. (langchain.js)

python
from langgraph.store.memory import InMemoryStore

store = InMemoryStore()
graph = builder.compile(checkpointer=checkpointer, store=store)

# Al cerrar una tesis, un nodo destila la lección al store (cross-thread)
await store.aput(
    ("thesis", "NVDA"), "postmortem-2026Q2",
    {"lesson": "El debate ignoró la señal de inventarios; añadir check de supply chain."},
)
# Semanas después, cualquier agente la recupera antes de reabrir el nombre
results = await store.asearch(("thesis", "NVDA"), query="riesgos de supply chain")

El tercer pilar es el time travel: replay y fork desde checkpoints históricos, con tres usos documentados — debugging (repetir un run fallido), evaluación A/B (mismo historial, otro prompt o modelo) y recuperación de errores (rebobinar justo antes del nodo que falló). (DevPress官方社区) Para un desk, el fork es la herramienta de auditoría cuantitativa: responde "¿habría cambiado la decisión con un prompt de analista más conservador?" re-ejecutando desde el checkpoint anterior al debate, sobre el mismo estado de mercado. Propiedad a conocer antes de diseñar el HITL: los interrupt() se re-disparan durante el time travel. (arXiv.org)

3.2.3 Human-in-the-loop real: la orden nunca sale sin firma

Hay dos mecanismos HITL y conviene no confundirlos. HumanInTheLoopMiddleware se configura con interrupt_on (mapping herramienta → configuración), soporta decisiones approve/edit/reject/respond y requiere checkpointer; desde langchain>=1.3.3 admite un predicado condicional para interrumpir solo las llamadas que superen un umbral — el patrón "interrumpir solo órdenes con notional > X". (🦜️🔗 LangChain)

python
from langchain.agents import create_agent
from langchain.agents.middleware import HumanInTheLoopMiddleware
from langgraph.checkpoint.memory import InMemorySaver

agent = create_agent(
    model="gpt-5.5",
    tools=[get_ohlcv, place_order, cancel_order],
    middleware=[
        HumanInTheLoopMiddleware(
            interrupt_on={
                "place_order": {"allowed_decisions": ["approve", "edit", "reject"]},
                "cancel_order": {"allowed_decisions": ["approve", "reject"]},
                "get_ohlcv": False,  # lectura segura: sin aprobación
            },
            description_prefix="Operación pendiente de aprobación del trader",
        ),
    ],
    # El HITL exige checkpointing. En producción: AsyncPostgresSaver.
    checkpointer=InMemorySaver(),
)

El segundo mecanismo, más quirúrgico, es interrupt() dentro de un nodo: pausa la ejecución de forma condicional en un punto exacto y se reanuda con Command(resume=...) sobre el mismo thread_id. (arXiv.org) Es el patrón correcto cuando la pausa forma parte de la lógica de negocio — por ejemplo, entre el plan del trader y la transmisión al broker:

python
from langgraph.types import interrupt, Command
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.memory import InMemorySaver
from typing import TypedDict

class ExecutionState(TypedDict):
    order: dict          # {"ticker", "side", "qty", "limit_price"}
    status: str

def risk_gate(state: ExecutionState) -> dict:
    order = state["order"]
    if order["qty"] * order["limit_price"] > 500_000:
        # Pausa: el trader humano decide sobre ESTE estado exacto
        decision = interrupt({"pending_order": order,
                              "reason": "notional > 500k USD"})
        if decision["action"] == "reject":
            return {"status": "rejected_by_trader"}
        if decision["action"] == "edit":
            return {"order": decision["order"]}  # el humano corrige cantidad/precio
    return {"status": "approved"}

def send_to_broker(state: ExecutionState) -> dict:
    ...  # FIX/REST al OMS; solo se alcanza con status == "approved"
    return {"status": "sent"}

graph = (
    StateGraph(ExecutionState)
    .add_node("risk_gate", risk_gate)
    .add_node("send_to_broker", send_to_broker)
    .add_edge(START, "risk_gate")
    .add_edge("risk_gate", "send_to_broker")
    .compile(checkpointer=InMemorySaver())
)

config = {"configurable": {"thread_id": "order-2026-07-29-0147"}}
graph.invoke({"order": {...}, "status": "new"}, config)   # se pausa en interrupt()
# Tras revisión humana en la UI de supervisión:
graph.invoke(Command(resume={"action": "approve"}), config)  # continúa a send_to_broker
Una orden sobre el umbral de notional se pausa con checkpoint; solo llega al broker tras aprobación o edición humana, y todo queda en el audit trail.trader_agent propone orden → risk_gate checks deterministas; risk_gate checks deterministas → status = approved (notional ≤ umbral); risk_gate checks deterministas → interrupt PAUSA + checkpoint (notional > umbral); interrupt PAUSA + checkpoint → UI de supervisión streaming v2; UI de supervisión streaming v2 → status = approved (approve); UI de supervisión streaming v2 → order actualizada status = approved (edit); UI de supervisión streaming v2 → status = rejected fin + audit log (reject); status = approved → send_to_broker; order actualizada status = approved → send_to_broker; send_to_broker → PostgresSaver audit trail; status = rejected fin + audit log → PostgresSaver audit trailnotional ≤ umbralnotional > umbralapproveeditrejecttrader_agentpropone ordenrisk_gatechecks deterministasstatus = approvedinterruptPAUSA + checkpointUI de supervisiónstreaming v2order actualizadastatus = approvedstatus = rejectedfin + audit logsend_to_brokerPostgresSaveraudit trail
Risk gate con interrupt humano. Las órdenes bajo el umbral de notional pasan directas; las que lo superan se pausan con interrupt hasta que la UI de supervisión aprueba, edita o rechaza.

Para la UI de supervisión que alimenta ese interrupt, LangGraph expone streaming con granularidad de nodo y de token: desde ≥1.1 el formato unificado v2 (version="v2", chunks StreamPart {type, ns, data}) y desde v1.2 una API de event streaming con iteradores por proyección. (PrivacyFilter) La trampa número uno con agentes anidados — create_agent devuelve un grafo compilado, así que todo agente dentro de un grafo es un subgrafo — es que sin subgraphs=True el stream messages del padre no emite los tokens del LLM interno: (PrivacyFilter)

python
for chunk in graph.stream(
    {"messages": [{"role": "user", "content": "Analiza NVDA y propone acción."}]},
    stream_mode="messages",
    subgraphs=True,   # sin esto, silencio del agente interno
    version="v2",
):
    print(chunk["type"])  # "messages"
    print(chunk["ns"])    # () raíz, ("agent:<task_id>",) subgrafo → routing por panel de la UI
    print(chunk["data"])  # (token, metadata)

3.3 Patrones multi-agente para trading

3.3.1 Taxonomía oficial v1: subagents, handoffs, supervisor, swarm, deepagents y MCP

La documentación de LangChain v1 reconoce cuatro patrones multi-agente canónicos: (arXiv.org) subagents (un agente principal coordina subagentes invocados como herramientas; stateless por diseño — aislamiento de contexto fuerte a costa de repetir el flujo en cada llamada), handoffs (los agentes se transfieren el control mediante tool calls), skills (un único agente carga conocimiento especializado bajo demanda sin ceder el control) y router (un paso de clasificación dirige la entrada al especialista). El dato económico clave: los patrones con estado (handoffs, skills) ahorran un 40-50% de llamadas al modelo en peticiones repetidas. (arXiv.org) El mapeo a trading: subagentes son "analistas como herramientas del PM", handoffs son "bull → bear → trader", y router es el clasificador de régimen de mercado.

Sobre esa taxonomía, el ecosistema oficial publica tres librerías de orquestación. langgraph-supervisor crea un supervisor jerárquico cuyo LLM decide a qué agente delegar mediante handoff tools; el StateGraph devuelto puede anidarse como agente de otro supervisor (hierarchical teams). (arXiv.org) langgraph-swarm implementa el enjambre: create_handoff_tool genera herramientas transfer_to_<agente>, y un campo active_agent en el estado compartido actúa de testigo de relevo — quien lo tiene, ejecuta. (genai.qa) La diferencia operativa medible: en el supervisor cada paso pasa por el coordinador central (más tokens); en el swarm el handoff es directo agente→agente (menos tokens). (genai.qa) La tercera, deepagents (create_deep_agent), empaqueta un harness de research coordinado — planificación con write_todos, sistema de ficheros virtual con allowlist de herramientas, subagentes de contexto aislado, HITL con interrupt_on por subagente (LangChain Forum) — cuyo filesystem restringido es el patrón least-privilege aplicable a herramientas de broker: al agente de research solo se le muestran read_file, ls, glob, grep, nunca place_order. (LangChain Forum)

La pieza que conecta todo con datos reales es MCP (Model Context Protocol), el protocolo abierto que estandariza cómo las aplicaciones exponen herramientas a los LLMs: con langchain-mcp-adapters, MultiServerMCPClient carga herramientas de múltiples servidores (stdio/HTTP) como BaseTool estándar. (Github) Es stateless por defecto (cada invocación crea una ClientSession nueva); con handle_tool_errors=True los errores de herramienta vuelven al modelo como ToolMessage con status="error" para autocorrección; y soporta interceptores con acceso a contexto, estado y store para bloquear herramientas sensibles. (Github) Ya existen servidores MCP de datos financieros institucionales — forks operativos de TradingAgents integran servidores FSI (Kensho, Aiera, FactSet, Morningstar, LSEG) como herramientas de los analistas. (qiankunli.github.io)

python
import asyncio
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain.agents import create_agent

async def main():
    client = MultiServerMCPClient(
        {
            "market_data": {   # servidor MCP interno del desk (ohlcv, fundamentals)
                "transport": "http",
                "url": "http://mcp-marketdata.internal:8000/mcp",
            },
            "news": {          # vendor externo vía MCP
                "transport": "http",
                "url": "https://mcp.vendor.com/mcp",
            },
        }
    )
    tools = await client.get_tools()   # BaseTool estándar, con prefijo por servidor
    analyst = create_agent("claude-sonnet-4-6", tools)
    return await analyst.ainvoke({
        "messages": [{"role": "user",
                      "content": "Cruce de momentum y noticias de MSFT, última semana."}]
    })

asyncio.run(main())

3.3.2 Caso canónico: el debate bull/bear/judge de TradingAgents

TradingAgents (Tauric Research; arXiv 2412.20138, AAAI 2025) es el framework multi-agente de trading de referencia y el caso desarrollado de este módulo. (LangChain Forum) Replica el organigrama de una firma real: cuatro analistas en paralelo (mercado/técnico, sentimiento/social, noticias, fundamentales) escriben sus informes en el estado compartido; dos investigadores, bull y bear, debaten sobre ellos durante max_debate_rounds rondas; un research manager juzga el debate; un trader convierte la tesis en plan; un segundo debate a tres bandas (agresivo, conservador, neutral) examina el riesgo; y un portfolio manager emite la aprobación final. (langchain.js) La implementación es un StateGraph cuyos nodos son los roles — closures que leen y escriben un estado tipado (InvestDebateState, RiskDebateState) — con checkpoints SQLite para reanudar análisis largos y dos tiers de LLM (deep_think_llm para síntesis, quick_think_llm para lectura). (langchain.js) El coste de un análisis completo es del orden de 30-50 llamadas LLM por ticker. (langchain.js)

La lección arquitectónica central es que el debate, a pesar de involucrar a varios LLMs, es un grafo determinista: aristas fijadas por código, rondas acotadas, juez como nodo más con routing explícito. El LLM genera contenido dentro de cada nodo; el grafo decide quién habla después — la materialización del patrón "la FSM gobierna el proceso, el LLM resuelve el sub-problema dentro de cada estado". (Morph AI)

El debate multiagente es un grafo determinista: analistas en paralelo, rondas bull/bear acotadas, juez, trader, debate de riesgo y aprobación del portfolio manager.START → fan-out paralelo; fan-out paralelo → market_analyst; fan-out paralelo → sentiment_analyst; fan-out paralelo → news_analyst; fan-out paralelo → fundamentals_analyst; market_analyst → bull_researcher; sentiment_analyst → bull_researcher; news_analyst → bull_researcher; fundamentals_analyst → bull_researcher; bull_researcher → bear_researcher (max_debate_rounds); bear_researcher → research_manager juez del debate; research_manager juez del debate → trader plan de acción; trader plan de acción → risk debate agresivo / conservador / neutral; risk debate agresivo / conservador / neutral → portfolio_manager aprobación final; portfolio_manager aprobación final → END; portfolio_manager aprobación final → SqliteSaver por ticker (checkpoint por pas)max_debate_roundscheckpoint por pasSTARTfan-out paralelomarket_analystsentiment_analystnews_analystfundamentals_analystbull_researcherbear_researcherresearch_managerjuez del debatetraderplan de acciónrisk debateagresivo / conservador / neutralportfolio_manageraprobación finalENDSqliteSaverpor ticker
Grafo de debate de TradingAgents. Cuatro analistas en paralelo alimentan un debate bull/bear acotado, un juez, el trader y el debate de riesgo; el código fija quién habla después.

Una versión mínima del núcleo del debate — dos investigadores alternándose con contador de rondas y un juez — muestra cuán poca maquinaria hace falta sobre las primitivas de la sección 3.2:

python
from typing import Annotated, Literal, TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.types import Command
from langchain_core.messages import AnyMessage, HumanMessage

MAX_ROUNDS = 2

class InvestDebateState(TypedDict):
    messages: Annotated[list[AnyMessage], add_messages]  # historial del debate
    reports: list[str]        # informes de los 4 analistas (entrada)
    round: int
    verdict: str

def make_researcher(role: str, llm):
    def researcher(state: InvestDebateState) -> Command[Literal["bull", "bear", "judge"]]:
        argument = llm.invoke([
            HumanMessage(content=(
                f"Eres el investigador {role.upper()}. Informes: {state['reports']}. "
                f"Historial del debate: {state['messages']}. Contraargumenta en <200 palabras."
            ))
        ])
        nxt = "bear" if role == "bull" else ("bull" if state["round"] < MAX_ROUNDS else "judge")
        return Command(
            update={"messages": [argument], "round": state["round"] + (role == "bear")},
            goto=nxt,
        )
    return researcher

def judge(state: InvestDebateState) -> dict:
    # research_manager: síntesis estructurada del debate → veredicto
    return {"verdict": synthesize_verdict(state["messages"])}

debate = (
    StateGraph(InvestDebateState)
    .add_node("bull", make_researcher("bull", deep_llm))
    .add_node("bear", make_researcher("bear", deep_llm))
    .add_node("judge", judge)
    .add_edge(START, "bull")
    .add_edge("judge", END)
    .compile(checkpointer=sqlite_checkpointer)  # reanudable por ticker
)

El caso exige también la lectura crítica. El paper reporta CR 26,62% y Sharpe 8,21 en AAPL en tres meses de 2024, y son los propios autores quienes advierten en nota al pie que ese Sharpe excede su rango empírico esperado (SR>2 muy bueno, SR>3 excelente) por ausencia de pullbacks en el periodo. (langchain.js) La ventana cae además dentro del entrenamiento de los modelos GPT-4-class usados — riesgo de leakage paramétrico — y la réplica independiente a gran escala (FINSABER: dos décadas, más de 100 símbolos) encuentra que las ventajas de estrategias LLM se deterioran significativamente fuera de esas ventanas estrechas. (arXiv.org) La crítica académica añade el matiz estructural: los sistemas multi-agente financieros existentes son arquitecturas amplias, no tests controlados del valor del razonamiento multi-agente — el rendimiento superior puede deberse a más información o mejor gestión de riesgo, no al debate en sí. (Github) Veredicto pedagógico: TradingAgents se enseña como patrón de diseño (roles, estado tipado, ciclos acotados, checkpoints, tiers de LLM), nunca como evidencia de alfa. Los forks serios apuntan igual: ThesisAgent separa razonamiento (LLM) de decisión (matemática determinista) y reporta decisiones idénticas entre el pipeline LLM completo y el motor solo-matemático. (LangChain中文文档)

3.4 La matriz de decisión arquitectónica

El debate ya no es "¿agentes sí o no?" sino "¿cuánta autonomía justifica cada paso del pipeline?". La referencia canónica es la distinción de Anthropic entre workflows — LLMs y herramientas orquestados por caminos de código predefinidos — y agentes — el LLM dirige dinámicamente su propio proceso — con la recomendación de encontrar la solución más simple posible, porque los sistemas agénticos intercambian latencia y coste por rendimiento. (Grepture) La condición de victoria: los agentes ganan cuando el número de pasos no se conoce de antemano y hay feedback verificable por turno; los workflows ganan cuando el proceso es repetible y mandan la auditabilidad y los SLAs fijos (LangChain Forum) — una descripción exacta de un desk regulado.

La capa arquitectónica en la que la industria ha convergido en 2026 es "model your agent as a state machine first": la FSM gobierna el flujo de proceso determinísticamente mientras el LLM opera libremente dentro de cada estado; se puede dibujar el diagrama, enumerar todos los caminos y verificar propiedades de seguridad con tests deterministas. (Morph AI) El punto que cierra el círculo: un StateGraph con aristas estáticas es una FSM ejecutable; las conditional edges lo convierten en statechart con guards; y create_agent es una FSM mínima (modelo → herramientas → modelo) cuya transición decide el LLM. (Morph AI) El espectro completo — FSM pura → grafo con guards LLM → agente autónomo → multi-agente — es lo que cuantifica la matriz siguiente.

Tabla 3.2 — Matriz FSM → grafo → agente → multi-agente

CriterioFSM / grafo determinista (aristas estáticas)Grafo con guards LLM (conditional edges)Agente autónomo (create_agent)Multi-agente (supervisor / swarm / debate)
Control de flujoCódigo, 100% auditableLLM elige entre rutas predefinidasLLM decide ruta y herramientasVarios LLMs deciden routing y contenido
Coste / latenciaMínimo (LLM solo dentro de nodos)Bajo-medioMedio (bucle de N iteraciones)Alto: 30-50+ llamadas; swarm de 5 agentes ≈ 3× tokens por llamada (overhead de contexto)
Fiabilidad end-to-endAlta (caminos enumerables)Media-altaMedia (errores encadenados; 0,910≈0,350{,}9^{10}\approx0{,}35)Media-baja sin guardarraíles
AuditabilidadExcelente (checkpoint + caminos fijos)BuenaRequiere HITL + loggingRequiere HITL + trazado por agente
Cuándo usar en tradingEjecución de órdenes, risk checks, rebalanceo programadoClasificación de régimen, triaje de señalesResearch ad-hoc, Q&A sobre datos, generación de hipótesisDebate de tesis, comité de inversión simulado, análisis profundo por ticker
Equivalente en el cursoMáquina de estados de ejecución (M7, M12)Router de estrategiasAnalista con tools (M5: RAG como tool)"Firma de trading" estilo TradingAgents

Síntesis de Anthropic, la literatura de orquestación con FSM y las mediciones de coste/fiabilidad de topologías en producción. (Grepture)

Figura 3.1 — Coste de tokens por arquitectura agéntica (datos ilustrativos)Agente único (research): 18; Debate bull/bear (grafo acotado): 95; Supervisor (jerárquico): 130; Swarm 5 agentes (handoffs): 210 · Agente único (research): $0,35; Debate bull/bear (grafo acotado): $1,85; Supervisor (jerárquico): $2,55; Swarm 5 agentes (handoffs): $4,10Tokens por análisis completo050100150200250miles de tokens (input + output)18kAgenteúnico(research)95kDebatebull/bear(grafoacotado)130kSupervisor(jerárquico)210kSwarm 5agentes(handoffs)Coste estimado por análisis (jul-2026)012345USD$0,35Agenteúnico(research)$1,85Debatebull/bear(grafoacotado)$2,55Supervisor(jerárquico)$4,10Swarm 5agentes(handoffs)

La figura 3.1 usa datos ilustrativos construidos a partir del benchmark publicado de 30-50 llamadas LLM por análisis (langchain.js) y del multiplicador ~3× tokens por llamada para swarms de cinco agentes; (CSDN博客) los precios unitarios son orientativos a julio de 2026 y deben recalcularse con la tarifa vigente antes de cualquier presupuesto real.

El cierre intelectual del módulo es una frase defendible ante un comité: el LLM razona, el grafo gobierna, la matemática decide el sizing y un humano firma la orden. Los módulos posteriores instancian esta matriz: el 5 convierte el RAG en herramienta del agente de research; el 7 implementa el risk gate como nodo determinista con CVaR calculado en código; el 12 integra todo en el capstone.

Ejercicios

  1. Agente de research con middleware. Extender el agente de 3.1.1 con (a) SummarizationMiddleware con trigger={"tokens": 4000}, (b) el middleware retry_model de 3.1.2 y (c) un state_schema propio que añada watchlist: list[str]. Verificar que el estado custom persiste entre llamadas con InMemorySaver y un thread_id fijo.

  2. Bug del reducer. Construir el grafo de 3.2.1 con cuatro analistas en paralelo que escriban reports sin reducer; ejecutarlo y comprobar qué informe sobrevive. Añadir el reducer de concatenación y repetir. Documentar en dos frases por qué last-write-wins es inaceptable en un pipeline de research y qué excepción lanza LangGraph si dos nodos usan Overwrite sobre la misma clave en un super-paso.

  3. HITL con edición. Implementar el risk_gate de 3.2.3 con interrupt() y checkpointer SQLite. Ejecutar tres escenarios: aprobación directa, edición de la cantidad por el humano (verificar que send_to_broker recibe la orden editada) y rechazo (verificar que el grafo termina sin llamar al broker). Localizar después el checkpoint del interrupt en la base SQLite.

  4. Time travel forense. Sobre el grafo del ejercicio 3, usar get_state_history para localizar el checkpoint anterior al risk_gate, hacer fork con update_state cambiando el umbral, y comparar las dos trayectorias. Redactar el párrafo para compliance explicando qué se probó y sobre qué estado de mercado exacto.

  5. Debate acotado. Completar el StateGraph de debate de 3.3.2 con el nodo trader y un risk_gate posterior que interrumpa si el veredicto del juez es BUY con notional superior a un umbral inyectado vía context_schema. Medir las llamadas LLM por análisis con un contador en un middleware @wrap_model_call y contrastar con el rango 30-50 del caso TradingAgents. (langchain.js)

  6. Ejercicio de diseño — FSM vs grafo. Diseñar sobre papel (diagrama de estados, sin código) el pipeline "señal → propuesta → aprobación → ejecución → reconciliación" de un desk long-only, asignando a cada transición una de tres categorías: arista estática (código), guard con LLM (conditional edge), turno autónomo (agente). Justificar cada asignación con la fila correspondiente de la Tabla 3.2, marcar el punto exacto del interrupt() obligatorio y estimar la fiabilidad end-to-end con p=0,95p=0{,}95 por paso con LLM y p=1,0p=1{,}0 por paso determinista: P=∏ipiP = \prod_i p_i. Entregar el diagrama y la cuenta.


Módulo 3 de 15 — LangChain para Trading Cuantitativo. Datos y versiones verificados a julio de 2026; precios y cifras de mercado sujetos a cambio. LangChain 1.3.14 / langchain-core 1.5.2.


// check

Comprueba lo aprendido

Autoevaluación con respuesta inmediata. No se guarda ninguna puntuación.

  1. ¿Qué implica en la práctica que `create_agent` devuelva un `CompiledStateGraph`?

    Ver respuesta

    B. El tipo de retorno es el punto pedagógico decisivo de 3.1.1: el agente se invoca como cualquier grafo (invoke con messages), se anida como subgrafo —base del multi-agente de 3.3— y hereda la durabilidad, streaming, interrupciones y time travel de LangGraph. AgentExecutor vive en langchain-classic y create_react_agent está deprecado en v1.

  2. Un PM con mandato amplio y un analista junior comparten el mismo agente de trading. ¿Dónde debe viajar el límite de notional de cada uno?

    Ver respuesta

    C. La distinción clave de 3.1.2: context_schema es configuración inmutable por invocación (usuario, mesa, límites de riesgo), el state es el estado mutable del grafo y el store es memoria cross-thread. Un límite en el prompt es editable; en el state podría sobrescribirse a mitad de sesión; el context lo hace inmutable y auditable.

  3. Cuatro analistas en fan-out paralelo escriben cada uno su informe en la clave `reports`, declarada como list[str] sin reducer. ¿Qué contiene el estado al final?

    Ver respuesta

    B. Es la regla de reducción de 3.2.1 y la fuente número uno de bugs en grafos multi-nodo: sin Annotated[..., reducer], las escrituras a una clave sobrescriben. El InvalidUpdateError solo aparece si dos nodos usan Overwrite sobre la misma clave en el mismo super-paso. Con el reducer de concatenación sobreviven los cuatro informes.

  4. Un desk regulado exige que la aprobación humana de cada orden propuesta por un agente sea consultable con SQL años después. ¿Qué backend de persistencia corresponde?

    Ver respuesta

    C. Es la fila decisiva de la Tabla 3.1: para flujos HITL con requisito de auditoría, un SELECT sobre la tabla de checkpoints de Postgres reconstruye el estado exacto del grafo en el momento de la aprobación. SqliteSaver serializa hilos concurrentes con write-locks, InMemorySaver pierde todo al reiniciar el proceso, y el store es memoria cross-thread, no evidencia por hilo.

  5. Un risk_gate pausó el grafo con interrupt() ante una orden de 600.000 USD. ¿Cómo se reanuda correctamente tras la revisión humana?

    Ver respuesta

    C. El patrón de 3.2.3: interrupt() pausa sobre un estado exacto indexado por thread_id y solo Command(resume=...) sobre ese mismo hilo continúa la ejecución hacia send_to_broker. Relanzar invoke con entrada nueva crea una ejecución distinta y deja la pausada colgada —duplicación de órdenes en potencia—; requiere además checkpointer configurado.

  6. Un pipeline de research encadena 10 pasos autónomos con fiabilidad del 90 % por paso. ¿Cuál es la fiabilidad extremo a extremo?

    Ver respuesta

    C. La fiabilidad compuesta de la nota de riesgo de 3.2.3: los pasos se multiplican, no se promedian. 0,9^10 ≈ 0,35, es decir, una de cada tres ejecuciones falla. Es el argumento cuantitativo de la matriz de 3.4: mover un paso a «agente autónomo» añade riesgo multiplicativo, no lineal, y justifica el interrupt() entre decisión y orden.

  7. Para el clasificador que decide al inicio del pipeline si el régimen de mercado es risk-on o risk-off y enruta al investigador correspondiente, ¿qué arquitectura recomienda la matriz del módulo?

    Ver respuesta

    B. La Tabla 3.2 asigna la clasificación de régimen y el triaje de señales a la columna «grafo con guards LLM»: el número de rutas es conocido y enumerable, el LLM solo elige entre ellas, y se conserva auditabilidad con bajo coste. El agente autónomo es para research ad-hoc sin número de pasos conocido, y el régimen de mercado no es una función determinista cerrada.

  8. ¿Cuál es la lectura correcta del Sharpe 8,21 que TradingAgents reporta en AAPL?

    Ver respuesta

    D. Es la lectura crítica de 3.3.2: SR>3 ya es excelente según los autores, la ventana de tres meses de 2024 no tuvo pullbacks, existe riesgo de leakage paramétrico y la réplica a dos décadas y más de 100 símbolos deteriora las ventajas. El valor pedagógico del caso es arquitectónico —roles, estado tipado, ciclos acotados, tiers de LLM—, no su backtest.