Agentes y LangGraph: orquestación para decisiones de inversión
create_agent, middleware, StateGraph, persistencia, human-in-the-loop real y patrones multi-agente.
Objetivos de aprendizaje
Al completar este módulo, el lector será capaz de:
- Construir un agente de research con
create_agent(LangChain v1) y explicar por qué el objeto devuelto es unCompiledStateGraphanidable en grafos mayores. (Github) - Apilar middleware de producción — sumarización, reintentos, PII, human-in-the-loop — e inyectar el perfil de riesgo del usuario con
@dynamic_promptycontext_schema. (futureagi.com) - Modelar flujos de decisión como
StateGraphcon canales, reducers yCommand, eligiendo el checkpointer (memoria, SQLite, Postgres) según el requisito de auditoría. (Github) - Implementar una interrupción humana (
interrupt()) entre la decisión del agente y el envío de una orden, con reanudación víaCommand(resume=...). (arXiv.org) - Seleccionar entre subagentes, handoffs, supervisor y swarm para un problema de trading dado, cuantificando el coste de tokens de cada patrón. (arXiv.org)
- Aplicar la matriz FSM → grafo → agente → multi-agente para justificar ante un comité de riesgo cuánta autonomía corresponde a cada etapa del pipeline. (Grepture)
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.
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:
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.
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.
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)
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:
- ¿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. - ¿Research mono-proceso? →
SqliteSaver: durabilidad en fichero local con escrituras ACID; sus write-locks serializan hilos concurrentes, así que queda descartado para servicios. - ¿Servicio multi-proceso o contenedores? →
PostgresSaver/AsyncPostgresSavercon pooling (asyncpg/psycopg_pool); ojo: las conexiones bloqueantes paran el event loop. - ¿HITL con requisito de auditoría? → Postgres sí o sí: un
SELECTsobre 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
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官方社区)
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 esCommand(resume=...)sobre el mismothread_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.

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

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
-
Agente de research con middleware. Extender el agente de 3.1.1 con (a)
SummarizationMiddlewarecontrigger={"tokens": 4000}, (b) el middlewareretry_modelde 3.1.2 y (c) unstate_schemapropio que añadawatchlist: list[str]. Verificar que el estado custom persiste entre llamadas conInMemorySavery unthread_idfijo. -
Bug del reducer. Construir el grafo de 3.2.1 con cuatro analistas en paralelo que escriban
reportssin 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 usanOverwritesobre la misma clave en un super-paso. -
HITL con edición. Implementar el
risk_gatede 3.2.3 coninterrupt()y checkpointer SQLite. Ejecutar tres escenarios: aprobación directa, edición de la cantidad por el humano (verificar quesend_to_brokerrecibe 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. -
Time travel forense. Sobre el grafo del ejercicio 3, usar
get_state_historypara localizar el checkpoint anterior alrisk_gate, hacer fork conupdate_statecambiando el umbral, y comparar las dos trayectorias. Redactar el párrafo para compliance explicando qué se probó y sobre qué estado de mercado exacto. -
Debate acotado. Completar el
StateGraphde debate de 3.3.2 con el nodotradery unrisk_gateposterior que interrumpa si el veredicto del juez esBUYcon notional superior a un umbral inyectado víacontext_schema. Medir las llamadas LLM por análisis con un contador en un middleware@wrap_model_cally contrastar con el rango 30-50 del caso TradingAgents. (langchain.js) -
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.
Recursos de élite para seguir profundizando
- LangChain v1 — documentación de agentes (
create_agent, middleware): la referencia normativa de la sección 3.1, con el modelo de hooks y los middleware built-in. - LangChain — guía de patrones multi-agente: la taxonomía oficial (subagents, handoffs, skills, router) con criterios de elección y datos de coste.
- LangGraph — persistencia (checkpointers, store, memory): la fuente detrás de la Tabla 3.1; incluye la separación checkpointer/store y los límites operativos.
- LangGraph — interrupciones y human-in-the-loop: semántica exacta de
interrupt(),Command(resume=...)y su interacción con el time travel. - Anthropic — Building Effective Agents: el ensayo canónico workflow vs agente que sustenta la matriz de la sección 3.4; la recomendación de «la solución más simple que funcione» viene de aquí.
- arXiv 2412.20138 — TradingAgents: el paper del debate bull/bear/judge; léelo entero, incluida la nota al pie donde los autores matizan su propio Sharpe.
- arXiv 2505.07078 — FINSABER: la réplica independiente a dos décadas y más de 100 símbolos que pone en contexto los resultados de ventanas estrechas; el contrapeso empírico del módulo.
- Model Context Protocol — especificación: para entender qué estandariza MCP antes de conectar servidores de datos de mercado con
MultiServerMCPClient.
Comprueba lo aprendido
Autoevaluación con feedback inmediato. No se guarda ninguna puntuación: es solo para ti.