// 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.
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:
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.
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.
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)
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ónCheckpointer 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)
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_brokerPara 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)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())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中文文档)
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; ) | 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)
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 por paso con LLM y por paso determinista: . Entregar el diagrama y la cuenta.
Módulo 3 de 15 — LangChain para Trading Cuantitativo. Datos y versiones verificados a julio de 2026; precios y cifras de mercado sujetos a cambio. LangChain 1.3.14 / langchain-core 1.5.2.
// check
Comprueba lo aprendido
Autoevaluación con respuesta inmediata. No se guarda ninguna puntuación.
Ver respuesta
B. El tipo de retorno es el punto pedagógico decisivo de 3.1.1: el agente se invoca como cualquier grafo (invoke con messages), se anida como subgrafo —base del multi-agente de 3.3— y hereda la durabilidad, streaming, interrupciones y time travel de LangGraph. AgentExecutor vive en langchain-classic y create_react_agent está deprecado en v1.
Ver respuesta
C. La distinción clave de 3.1.2: context_schema es configuración inmutable por invocación (usuario, mesa, límites de riesgo), el state es el estado mutable del grafo y el store es memoria cross-thread. Un límite en el prompt es editable; en el state podría sobrescribirse a mitad de sesión; el context lo hace inmutable y auditable.
Ver respuesta
B. Es la regla de reducción de 3.2.1 y la fuente número uno de bugs en grafos multi-nodo: sin Annotated[..., reducer], las escrituras a una clave sobrescriben. El InvalidUpdateError solo aparece si dos nodos usan Overwrite sobre la misma clave en el mismo super-paso. Con el reducer de concatenación sobreviven los cuatro informes.
Ver respuesta
C. Es la fila decisiva de la Tabla 3.1: para flujos HITL con requisito de auditoría, un SELECT sobre la tabla de checkpoints de Postgres reconstruye el estado exacto del grafo en el momento de la aprobación. SqliteSaver serializa hilos concurrentes con write-locks, InMemorySaver pierde todo al reiniciar el proceso, y el store es memoria cross-thread, no evidencia por hilo.
Ver respuesta
C. El patrón de 3.2.3: interrupt() pausa sobre un estado exacto indexado por thread_id y solo Command(resume=...) sobre ese mismo hilo continúa la ejecución hacia send_to_broker. Relanzar invoke con entrada nueva crea una ejecución distinta y deja la pausada colgada —duplicación de órdenes en potencia—; requiere además checkpointer configurado.
Ver respuesta
C. La fiabilidad compuesta de la nota de riesgo de 3.2.3: los pasos se multiplican, no se promedian. 0,9^10 ≈ 0,35, es decir, una de cada tres ejecuciones falla. Es el argumento cuantitativo de la matriz de 3.4: mover un paso a «agente autónomo» añade riesgo multiplicativo, no lineal, y justifica el interrupt() entre decisión y orden.
Ver respuesta
B. La Tabla 3.2 asigna la clasificación de régimen y el triaje de señales a la columna «grafo con guards LLM»: el número de rutas es conocido y enumerable, el LLM solo elige entre ellas, y se conserva auditabilidad con bajo coste. El agente autónomo es para research ad-hoc sin número de pasos conocido, y el régimen de mercado no es una función determinista cerrada.
Ver respuesta
D. Es la lectura crítica de 3.3.2: SR>3 ya es excelente según los autores, la ventana de tres meses de 2024 no tuvo pullbacks, existe riesgo de leakage paramétrico y la réplica a dos décadas y más de 100 símbolos deteriora las ventajas. El valor pedagógico del caso es arquitectónico —roles, estado tipado, ciclos acotados, tiers de LLM—, no su backtest.