QAO // QUANT AGENTIC ORCHESTRATION
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 MIN NIVEL INTERMEDIO-AVANZADO EDICIÓN JUL 2026

Objetivos de aprendizaje

Al completar este módulo, el lector será capaz de:


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); promptsystem_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.

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:

Diagrama
EXPANDE

En palabras llanas: el agente es un diagrama de flujo que elige sus propias flechas

Piensa en la diferencia entre darle a un analista junior un manual de procedimientos — «primero descarga precios, luego calcula el RSI, luego redacta» — y darle a un analista senior un objetivo y el teléfono: «evalúa el momentum de NVDA; aquí tienes acceso a precios y a la calculadora». El junior ejecuta una cadena LCEL: el camino lo fijó el programador. El senior es el agente: en cada turno decide qué herramienta llamar, o si ya tiene bastante para cerrar el veredicto.

Lo que subraya el módulo es que esa flexibilidad no convierte al agente en una caja opaca. create_agent devuelve un CompiledStateGraph: el organigrama del bucle modelo → herramientas → modelo sigue existiendo, dibujable y auditable, solo que una transición concreta («¿llamo a otra herramienta o termino?») la decide el LLM en tiempo de ejecución. Por eso hereda gratis lo que LangGraph da a cualquier grafo — checkpoints, streaming, interrupciones, time travel — y por eso puede colgarse como nodo de un grafo mayor. Autonomía dentro, estructura fuera: esa es la idea que hay que llevarse de 3.1.

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)

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:

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.

EXPANDE

En palabras llanas: context, state y store — las tres memorias del agente

La distinción context_schema / state_schema / store se entiende con el mobiliario de un desk:

  • Context es la tarjeta de mandato plastificada que acompaña a cada orden de trabajo: quién eres (user_id), tu mesa, tu notional máximo, tu perfil de riesgo. Viaja con la invocación, es tipada y no se puede tachar a mitad de sesión — inmutable por invocación. Por eso el perfil de riesgo vive ahí y no en un prompt editable.
  • State es la libreta de la sesión de hoy: los mensajes del debate, los informes parciales, el contador de rondas. Se escribe y reescribe en cada nodo, y el checkpointer la fotografía paso a paso.
  • Store es el archivo de tesis del desk: lecciones destiladas («el debate de NVDA ignoró inventarios») que cualquier sesión futura, de cualquier hilo, puede consultar antes de reabrir un nombre.

Regla mnemotécnica: si es identidad y límites, va al context; si es la conversación de hoy, va al state; si debe sobrevivir a la sesión, va al store. Confundir las capas es la vía rápida a un límite de riesgo que desaparece al reiniciar el hilo.

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.

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)

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.

EXPANDE

Ejemplo trabajado: qué informe sobrevive cuando nadie define el reducer

Toma el grafo de fan-out de la sección, pero declarando el estado sin reducer en reports:

class ResearchStateBuggy(TypedDict):
    messages: Annotated[list[AnyMessage], add_messages]
    ticker: str
    reports: list[str]        # sin reducer: last-write-wins

Los cuatro analistas (market, sentiment, news, fundamentals) arrancan en el mismo super-paso desde START y cada uno devuelve {"reports": ["informe de X"]}. Al fusionar las escrituras del super-paso, LangGraph no tiene cómo combinar cuatro listas: aplica la última escritura. Resultado observable:

out = graph.invoke({"ticker": "NVDA", "messages": [], "reports": []})
print(out["reports"])   # ['informe de fundamentals'] — solo UNO, y cuál sobrevive
                        # no está garantizado: depende del orden de finalización

El informe técnico, el de sentimiento y el de noticias se han generado (y facturado en tokens) pero se han perdido en silencio: es la fuente número uno de bugs que el módulo señala. La corrección es una línea — reports: Annotated[list[str], lambda a, b: a + b] — y ahora out["reports"] contiene los cuatro informes concatenados, en orden determinista por canal. Dos apuntes más que el ejercicio 2 te pedirá verificar: con reducer, la fusión ocurre al entrar las escrituras al canal, no al final del grafo; y si dos nodos del mismo super-paso usan Overwrite sobre la misma clave, LangGraph lanza InvalidUpdateError — el único caso en el que el problema, por suerte, grita en lugar de perder datos calladamente.

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

Escenario Checkpointer Ventaja Riesgo / límite
Tests unitarios, desarrollo local MemorySaver / InMemorySaver Cero setup, rápido, reinicia limpio entre tests Todo el estado se pierde al reiniciar el proceso; prohibido en producción
Servidor mono-proceso, baja concurrencia SqliteSaver Durabilidad en fichero local, sin dependencia externa, escrituras ACID Los write-locks de SQLite serializan hilos concurrentes; no escala horizontalmente
Despliegue multi-proceso / contenedores PostgresSaver / AsyncPostgresSaver ACID, consultable con SQL, dobla como audit log, escala con pooling Requiere connection pooling (asyncpg/psycopg_pool); conexiones bloqueantes paran el event loop
Flujos HITL con requisito de auditoría PostgresSaver / AsyncPostgresSaver Las consultas SQL sobre la tabla de checkpoints sustituyen infraestructura de audit log separada Sin 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)

Interpretación. La fila decisiva para un desk regulado es la última: cuando un humano aprueba una orden propuesta por un agente, la evidencia de quién aprobó qué, cuándo y sobre qué estado de mercado debe ser consultable años después. PostgresSaver convierte la tabla de checkpoints en esa evidencia sin infraestructura adicional — un SELECT sobre el hilo reconstruye el estado exacto del grafo en el momento de la aprobación. (🦜️🔗 LangChain) SqliteSaver es la opción honesta para research local (TradingAgents la usa para reanudar análisis largos por ticker), (langchain.js) pero sus write-locks la descartan para servicios concurrentes, y MemorySaver pertenece a los tests. Dos detalles operativos de las docs: thread_id por debajo de 255 caracteres y poda programada de checkpoints — la tabla crece sin límite. (IDEAS/RePEc) El setup se ejecuta una vez por despliegue:

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)

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)

EXPANDE

Mapa de decisión: qué checkpointer corresponde a cada despliegue

La Tabla 3.1 da los criterios; este es el mismo razonamiento en orden de decisión, que es como se aplica en la práctica:

  1. ¿Tests o desarrollo local?InMemorySaver: cero setup y reinicio limpio entre tests — y prohibido en producción, porque todo el estado muere con el proceso.
  2. ¿Research mono-proceso?SqliteSaver: durabilidad en fichero local con escrituras ACID; sus write-locks serializan hilos concurrentes, así que queda descartado para servicios.
  3. ¿Servicio multi-proceso o contenedores?PostgresSaver / AsyncPostgresSaver con pooling (asyncpg/psycopg_pool); ojo: las conexiones bloqueantes paran el event loop.
  4. ¿HITL con requisito de auditoría? → Postgres sí o sí: un SELECT sobre la tabla de checkpoints reconstruye el estado exacto del grafo en el momento de la aprobación, sin infraestructura de audit log separada.

Dos detalles operativos que la lista no recoge y el módulo sí fija: thread_id por debajo de 255 caracteres, y la tabla de checkpoints crece sin límite sin una poda programada según la política de retención. Y no confundir capas: el checkpointer responde «¿dónde vive el estado de este hilo?»; el store, «¿qué recuerdan todos los hilos?».

El time travel merece párrafo aparte: es la herramienta de auditoría que no existe fuera de LangGraph. Con get_state_history localizas el checkpoint anterior al debate, haces fork con update_state —otro prompt de analista, otro umbral de riesgo— y re-ejecutas sobre el mismo estado de mercado. Es la respuesta operativa a la pregunta de comité «¿habría cambiado la decisión con parámetros más conservadores?», con la salvedad que el módulo fija y el ejercicio 4 practica: los interrupt() se re-disparan durante el time travel, así que la réplica forense también pausará en el risk gate.

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)

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:

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
Diagrama

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)

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)

Nota de riesgo — fiabilidad compuesta y accountability gap. Dos números deben gobernar cualquier decisión de autonomía. Primero, la fiabilidad de una cadena se degrada de forma no lineal con su longitud: un pipeline de 10 pasos al 90% por paso entrega solo \(0{,}9^{10} \approx 0{,}35\) — un 34,9% de éxito extremo a extremo; cada turno autónomo extra compra flexibilidad pagando en latencia, tokens y propagación de errores tempranos. (CSDN博客) Segundo, la literatura sobre colaboración humano-agente identifica la accountability gap — acciones dañinas no intencionadas sin responsable claro — como riesgo creciente con la autonomía, "particularly in critical decision-making scenarios involving finance". (langchain.js) La regla operativa de este curso se deriva de ambos: entre "el agente decide vender" y "la orden llega al broker" debe existir siempre un checkpoint con interrupt(). En la formulación de la guía de producción: los agentes que fallan en producción no son los cuyo LLM tomó una mala decisión — eso es esperable y recuperable — sino aquellos en los que la mala decisión no tuvo checkpoint entre la decisión y la consecuencia. (DevPress官方社区)

EXPANDE

Trampa común: HITL decorativo

Montar el interrupt() es lo fácil; que la firma humana proteja de verdad es otra cosa. Cuatro formas reales de tener un human-in-the-loop de adorno:

  • Firma a ciegas. La UI de supervisión muestra ticker y lado («BUY NVDA») pero no el notional, el precio límite ni el estado que motivó la propuesta. El payload del interrupt() debe llevar la orden completa y la razón del gate — es lo que el humano está firmando.
  • Reanudar mal. Tras la pausa, relanzar graph.invoke({"order": ...}) crea una ejecución nueva y deja la pausada colgada; la vía correcta es Command(resume=...) sobre el mismo thread_id. Dos hilos abiertos sobre la misma orden es una duplicación esperando a ocurrir.
  • Checkpointer volátil en producción. Con InMemorySaver, un reinicio del proceso deja las órdenes pendientes de firma en el limbo: nadie puede reconstruir qué estaba sobre la mesa del trader. De ahí la fila HITL de la Tabla 3.1: Postgres o nada.
  • Olvidar que el time travel re-dispara interrupts. Si haces fork desde un checkpoint anterior para una auditoría A/B, el interrupt() se volverá a disparar: planifica la reanudación también en las réplicas forenses.

La razón cuantitativa de que este punto de firma no sea opcional es la fiabilidad compuesta de la nota de riesgo: cada paso autónomo multiplica la probabilidad de error de los anteriores.

Fiabilidad extremo a extremo de un pipeline según el número de pasos autónomos y la fiabilidad por paso

Con fiabilidad del 90 % por paso —optimista para un LLM en producción—, diez pasos autónomos seguidos entregan el resultado correcto solo una de cada tres veces. El interrupt() entre decisión y orden convierte el último tramo en determinista + firma, sacándolo de la cadena multiplicativa.

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)

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

En la práctica institucional — Kensho/S&P Global. El caso de producción mejor documentado de orquestación LangGraph en datos financieros es "Grounding", el framework multi-agente de Kensho (motor de IA de S&P Global): un router descompone cada consulta en sub-consultas para Data Retrieval Agents especializados por dominio (equity research, fixed income, macro) y agrega las respuestas en patrón map-reduce. (IDEAS/RePEc) Dos lecciones trasladables. Primera, la arquitectura es deliberadamente modesta en autonomía: un router en el borde y agentes de retrieval acotados, no un enjambre libre — la instancia industrial del patrón router de la taxonomía v1. Segunda, la evaluación es multi-etapa con exact-match de routing y tool-calling: se mide si el router eligió el agente correcto y si la herramienta recibió los argumentos correctos, con igualdad exacta, no con juicio de otro LLM — porque un LLM evaluando una cifra financiera es circular. (IDEAS/RePEc) Sobre esta capa Kensho despliega un asistente de equity research, un agente de compliance ESG y el servidor MCP que expone los datos de S&P a clientes externos. (IDEAS/RePEc) El módulo 11 del curso (observabilidad y evaluación con LangSmith) retoma este pipeline multi-etapa como plantilla de CI.

EXPANDE

Supervisor vs swarm: dónde viajan los tokens

Los dos patrones jerárquicos del ecosistema se distinguen por una sola pregunta: ¿quién habla después de cada paso? En el supervisor, toda vuelta pasa por el coordinador; en el swarm, el agente que tiene el testigo (active_agent) cede el control directamente al siguiente:

Diagrama

La consecuencia económica es la que cuantifica la Tabla 3.2: en el supervisor cada delegación cuesta dos pasadas por el LLM coordinador (ida y vuelta), mientras que el swarm ahorra el centro pero paga overhead de contexto en cada handoff — de ahí el multiplicador ≈3× tokens por llamada de un enjambre de cinco agentes frente a un monolito. Ninguno es gratis: la elección se hace con la calculadora, no con el diagrama.

Apunte operativo sobre la capa MCP que alimenta a estos agentes: MultiServerMCPClient es stateless por defecto —cada invocación abre una ClientSession nueva— y con handle_tool_errors=True los errores de herramienta vuelven al modelo como ToolMessage con status="error", permitiéndole autocorregir en lugar de romper el bucle.

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)

Diagrama

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:

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中文文档)

EXPANDE

Ejemplo trabajado: el P&L mensual de un debate multi-agente

El caso TradingAgents cuesta 30–50 llamadas LLM por ticker. Convirtamos el punto medio (40) en una línea de presupuesto, usando los dos tiers del framework — deep_think_llm para síntesis, quick_think_llm para lectura — con tarifas ilustrativas a jul-2026 (mismo aviso que la figura 3.1: recalcular con la tarifa vigente antes de presupuestar):

Tier Llamadas Tokens in / out por llamada Tarifa ilustrativa (in / out, por Mtok) Coste
deep (debate, juez, trader) 8 4.000 / 1.000 3,00 USD / 15,00 USD 8 × (0,012 + 0,015) = 0,216 USD
quick (analistas, lecturas) 32 3.000 / 500 0,30 USD / 1,50 USD 32 × (0,0009 + 0,00075) ≈ 0,053 USD
Total por análisis 40 ≈ 0,27 USD

Escalado al ritmo de un desk pequeño: watchlist de 60 tickers, un análisis por ticker y día, 22 sesiones al mes → 1.320 análisis → ≈ 360 USD/mes. Ahora la sensibilidad que importa: si el mismo flujo se monta como swarm de cinco agentes con el overhead ≈3× por llamada de la Tabla 3.2, la factura roza los 1.000 USD/mes sin añadir ni una sola decisión más. Tres lecciones: el mix de tiers domina el coste (los 32 quick cuestan la quinta parte que los 8 deep); la arquitectura es una decisión de P&L antes que de elegancia; y la métrica honesta es la de los forks operativos que cita el módulo — coste LLM frente a P/L realizado, revisada mensualmente.

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

Criterio FSM / grafo determinista (aristas estáticas) Grafo con guards LLM (conditional edges) Agente autónomo (create_agent) Multi-agente (supervisor / swarm / debate)
Control de flujo Código, 100% auditable LLM elige entre rutas predefinidas LLM decide ruta y herramientas Varios LLMs deciden routing y contenido
Coste / latencia Mínimo (LLM solo dentro de nodos) Bajo-medio Medio (bucle de N iteraciones) Alto: 30-50+ llamadas; swarm de 5 agentes ≈ 3× tokens por llamada (overhead de contexto)
Fiabilidad end-to-end Alta (caminos enumerables) Media-alta Media (errores encadenados; \(0{,}9^{10}\approx0{,}35\)) Media-baja sin guardarraíles
Auditabilidad Excelente (checkpoint + caminos fijos) Buena Requiere HITL + logging Requiere HITL + trazado por agente
Cuándo usar en trading Ejecución de órdenes, risk checks, rebalanceo programado Clasificación de régimen, triaje de señales Research ad-hoc, Q&A sobre datos, generación de hipótesis Debate de tesis, comité de inversión simulado, análisis profundo por ticker
Equivalente en el curso Máquina de estados de ejecución (M7, M12) Router de estrategias Analista 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)

Interpretación. La columna de fiabilidad es la que un risk manager debe leer primero: la fiabilidad compuesta decae exponencialmente con la longitud de la cadena autónoma, así que mover un paso de "grafo con guards" a "agente autónomo" no añade riesgo lineal sino multiplicativo sobre todos los pasos posteriores. (CSDN博客) La columna de coste tiene su propio umbral: un swarm de cinco agentes puede triplicar el consumo de tokens por llamada equivalente frente a un monolito, porque cada handoff arrastra overhead de contexto — multiplicador que se compone con el de número de llamadas que el Módulo 11 presupuesta por arquitectura (§11.4). (CSDN博客) Con 30-50 llamadas por análisis (langchain.js) y una watchlist de decenas de tickers diaria, (qiankunli.github.io) la partida de LLM se convierte en línea de P&L presupuestable — de ahí la métrica "coste LLM vs P/L realizado" de los forks operativos. (qiankunli.github.io) La última fila es la regla de decisión del curso: research = agente, debate de tesis = grafo con ciclos acotados, ejecución = FSM determinista con interrupt() humano. La autonomía crece hacia arriba en la pila, nunca hacia la ejecución: entre decisión y orden hay siempre FSM, checkpoint en Postgres y firma humana. (Morph AI)

Figura 3.1 — Coste de tokens por arquitectura agéntica (datos ilustrativos)

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{,}95\) por paso con LLM y \(p=1{,}0\) por paso determinista: \(P = \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.


EXPANDE

Recursos de élite para seguir profundizando

CHECK

Comprueba lo aprendido

Autoevaluación con feedback inmediato. No se guarda ninguna puntuación: es solo para ti.