Agents and LangGraph: orchestration for investment decisions
create_agent, middleware, StateGraph, persistence, real human-in-the-loop, and multi-agent patterns.
Learning objectives
Upon completing this module, the reader will be able to:
- Build a research agent with
create_agent(LangChain v1) and explain why the returned object is aCompiledStateGraphthat can be nested inside larger graphs. (Github) - Stack production middleware — summarization, retries, PII, human-in-the-loop — and inject the user's risk profile with
@dynamic_promptandcontext_schema. (futureagi.com) - Model decision flows as a
StateGraphwith channels, reducers, andCommand, choosing the checkpointer (memory, SQLite, Postgres) according to the audit requirement. (Github) - Implement a human interruption (
interrupt()) between the agent's decision and the submission of an order, with resumption viaCommand(resume=...). (arXiv.org) - Select among subagents, handoffs, supervisor, and swarm for a given trading problem, quantifying the token cost of each pattern. (arXiv.org)
- Apply the FSM → graph → agent → multi-agent matrix to justify to a risk committee how much autonomy belongs at each stage of the pipeline. (Grepture)
Module 2 built deterministic pipelines with LCEL: composable chains of Runnables with structured output. This module's leap is small on the surface and deep in its consequences: an agent is a compiled graph that inherits the Runnable interface. What changes is not the invocation mechanism — it is still invoke/stream/batch — but who decides the path: no longer the programmer, but the model, turn by turn. That transfer of control is what must be learned to bound, audit, and, at the critical points, revoke. These patterns will reappear in module 5 (RAG as an agent tool), module 7 (the risk gate as a deterministic node), and the module 12 capstone.
3.1 create_agent: the v1 standard
3.1.1 The factory on top of LangGraph: model + tools + system_prompt → CompiledStateGraph
Since the LangChain 1.0 GA (October 2025), create_agent —in langchain.agents— is the standard way to build agents. (Github) Underneath, it compiles the classic agentic loop (call the model, let it choose tools, execute them, return the result to the model, and finish when there are no more tool calls) as a LangGraph graph: create_agent runs on LangGraph; LangChain to get started fast, LangGraph for bespoke orchestration. (Github)
The pedagogically decisive point is the return type: create_agent does not return an opaque object but a CompiledStateGraph. (Github) Three practical consequences. First, the agent is invoked like any graph: agent.invoke({"messages": [...]}). Second, it can be nested as a node or subgraph inside your own StateGraph — the foundation of the multi-agent teams of section 3.3. Third, everything LangGraph offers is inherited with no additional code: durability (checkpoints after every step), streaming of tokens and states, human-in-the-loop (interruptions and resumption), and time travel (replay and fork from historical checkpoints). (PrivacyFilter)
The two APIs this course does not teach: AgentExecutor, which lives in the langchain-classic compatibility package, (MDPI) and langgraph.prebuilt.create_react_agent, deprecated in LangGraph v1 with removal planned for v2.0. (is4.ai) The official migration guide documents the correspondence: pre_model_hook/post_model_hook hooks → middleware (before_model/after_model); prompt → system_prompt; dynamic prompts → @dynamic_prompt; custom state → TypedDict only; runtime context → the context argument instead of config["configurable"]. (Github)
The course's first agent is a research analyst with two deterministic tools — a price query and a metrics calculator — under the course's governing principle: the LLM never computes; it orchestrates tools that do.
from langchain.agents import create_agent
from langchain.tools import tool
@tool
def get_ohlcv(ticker: str, start: str, end: str) -> dict:
"""Returns daily OHLCV for a ticker between two dates (point-in-time)."""
... # implementation against the market data API from module 4
@tool
def compute_rsi(prices: list[float], window: int = 14) -> float:
"""Computes Wilder's RSI over a series of closes."""
... # deterministic math, testable on its own
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."}]
})
The loop that invoke runs — model, tools, model — is now a graph with persistent state on every edge:
In plain terms: the agent is a flowchart that chooses its own arrows
Think of the difference between handing a junior analyst a procedures manual — "first download prices, then compute the RSI, then write it up" — and handing a senior analyst an objective and a phone: "assess NVDA's momentum; here is access to prices and the calculator". The junior runs an LCEL chain: the programmer fixed the path. The senior is the agent: on each turn it decides which tool to call, or whether it already has enough to close the verdict.
What the module stresses is that this flexibility does not turn the agent into an opaque box. create_agent returns a CompiledStateGraph: the org chart of the model → tools → model loop still exists, drawable and auditable; it is just that one specific transition ("do I call another tool or finish?") is decided by the LLM at runtime. That is why it inherits for free what LangGraph gives any graph — checkpoints, streaming, interruptions, time travel — and why it can hang as a node of a larger graph. Autonomy inside, structure outside: that is the idea to take away from 3.1.
3.1.2 Middleware: the loop's control layer
Middleware is the official v1 way to control what happens inside the agent: logging, prompt transformation and tool selection, retries and early termination, rate limits, guardrails, and PII detection. (futureagi.com) The extension model is six hooks — node-style (@before_agent, @before_model, @after_model, @after_agent) and wrap-style (@wrap_model_call, @wrap_tool_call) — plus the @dynamic_prompt decorator. (langchain.js)
For a quant desk, four built-in middleware cover the minimum defensive stack: SummarizationMiddleware (compresses the history once a token threshold is exceeded — essential in long research sessions), (Github) HumanInTheLoopMiddleware (pauses before sensitive tools; section 3.2.3), (🦜️🔗 LangChain) PIIMiddleware (detects and masks personal data before it leaves the perimeter) (Github) and model retry middleware (retries with exponential backoff on model API failures, added in langchain v1.1.0). (Github) Retry can also be implemented by hand with @wrap_model_call, which illustrates the wrap-style mechanics — the middleware receives the request and a handler, and decides how many times to invoke it: (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}")
Dynamic system prompts are built with @dynamic_prompt on top of ModelRequest, which exposes the conversation state (request.messages), the long-term memory store (request.runtime.store), and the runtime context (request.runtime.context). (Ailog) The distinction to internalize: context_schema defines immutable-per-invocation configuration — user, desk, risk limits — injected with agent.invoke(..., context=...); state_schema defines the graph's mutable state (in v1, TypedDict only); and the store is cross-thread memory. (Ailog) The user's risk profile belongs in the context, not in a hardcoded prompt:
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 # e.g. "equity-ls", "macro"
max_notional: float # per-order notional limit, in 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"),
)
This mechanism is the natural route to regulatory personalization: the same agent serves a PM with a broad mandate and a junior analyst with strict limits, and the difference travels in a typed, auditable, immutable object — not in an editable prompt string.
In plain terms: context, state, and store — the agent's three memories
The context_schema / state_schema / store distinction is best understood through the furniture of a desk:
- Context is the laminated mandate card that travels with every work order: who you are (
user_id), your desk, your maximum notional, your risk profile. It travels with the invocation, is typed, and cannot be crossed out mid-session — immutable per invocation. That is why the risk profile lives there and not in an editable prompt. - State is today's session notebook: the debate messages, the partial reports, the round counter. It is written and rewritten at every node, and the checkpointer snapshots it step by step.
- Store is the desk's thesis archive: distilled lessons ("the NVDA debate ignored inventories") that any future session, on any thread, can consult before reopening a name.
Mnemonic rule: if it is identity and limits, it goes in the context; if it is today's conversation, it goes in the state; if it must survive the session, it goes in the store. Mixing up the layers is the fast route to a risk limit that vanishes when the thread restarts.
3.2 LangGraph in depth
3.2.1 StateGraph: channels, reducers, and Command
When the flow must be decided by code rather than the model, you drop one level of abstraction: from create_agent to StateGraph. A StateGraph is a builder whose state is a TypedDict; each field becomes a channel with an optional reducer function; nodes are callables (state) -> partial_update; and edges can be static (add_edge), conditional (add_conditional_edges), or implicit via Command. At compile time, LangGraph validates the graph (no orphan nodes, existing destinations, START with outgoing edges) and builds a Pregel graph with channel-driven, super-step execution. (51CTO)
The reduction rule is the number-one source of bugs in multi-node graphs: without an explicit reducer, writes to a key overwrite each other (last-write-wins); with Annotated[..., reducer] they merge. (Github) The built-in add_messages reducer appends messages and updates existing ones by id; Overwrite bypasses the reducer and sets the channel directly — and only one node may use it on the same key within a super-step, on pain of InvalidUpdateError. (Github) Translated to trading: if four analysts write the report field in parallel, without a reducer only one survives. That is why this course teaches reducers before multi-agent.
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] # merged
ticker: str # plain overwrite
reports: Annotated[list[str], lambda a, b: a + b] # accumulated
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") # parallel fan-out: both write `reports`
.add_edge("market_analyst", END)
.add_edge("news_analyst", END)
.compile()
)
For nodes that must update state and route at the same time, LangGraph offers Command, a primitive with four parameters (update, goto, graph, resume) on which the multi-agent handoffs of section 3.3 are built. A warning from the docs themselves: do not mix static edges and goto for the same node — both would fire. (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"]) # deterministic function
return Command(
update={"reports": [f"regime: {vol_regime}"]},
goto="bull_researcher" if vol_regime == "risk-on" else "bear_researcher",
)
Finally, subgraphs: any compiled StateGraph — including the one returned by create_agent — can be added as a node of a parent graph; shared keys flow parent↔child, and each subgraph manages its own checkpoint namespace. (PrivacyFilter) It is the composition mechanism behind the research/risk/execution teams of case 3.3.2.
Worked example: which report survives when nobody defines the reducer
Take the section's fan-out graph, but declare the state without a reducer on reports:
class ResearchStateBuggy(TypedDict):
messages: Annotated[list[AnyMessage], add_messages]
ticker: str
reports: list[str] # no reducer: last-write-wins
The four analysts (market, sentiment, news, fundamentals) start in the same super-step from START, and each returns {"reports": ["informe de X"]}. When merging the super-step's writes, LangGraph has no way to combine four lists: it applies the last write. Observable result:
out = graph.invoke({"ticker": "NVDA", "messages": [], "reports": []})
print(out["reports"]) # ['informe de fundamentals'] — only ONE, and which one survives
# is not guaranteed: it depends on completion order
The technical, sentiment, and news reports were generated (and billed in tokens) but silently lost: the number-one source of bugs the module flags. The fix is one line — reports: Annotated[list[str], lambda a, b: a + b] — and now out["reports"] holds all four reports concatenated, in deterministic channel order. Two more points exercise 2 will ask you to verify: with a reducer, the merge happens as writes enter the channel, not at the end of the graph; and if two nodes in the same super-step use Overwrite on the same key, LangGraph raises InvalidUpdateError — the one case where the problem, luckily, shouts instead of quietly losing data.
3.2.2 Persistence: checkpoints, store, and time travel
The checkpointer serializes the complete graph state after every step — enabling pause/resume, failure recovery, and auditing — indexed by thread_id (one thread = one conversation or one analysis). (IDEAS/RePEc) The backend choice is not an infrastructure detail: it is a compliance decision.
Table 3.1 — Checkpointer decision criteria
| Scenario | Checkpointer | Advantage | Risk / limit |
|---|---|---|---|
| Unit tests, local development | MemorySaver / InMemorySaver |
Zero setup, fast, restarts clean between tests | All state is lost when the process restarts; forbidden in production |
| Single-process server, low concurrency | SqliteSaver |
Durability in a local file, no external dependency, ACID writes | SQLite write-locks serialize concurrent threads; does not scale horizontally |
| Multi-process / container deployment | PostgresSaver / AsyncPostgresSaver |
ACID, SQL-queryable, doubles as an audit log, scales with pooling | Requires connection pooling (asyncpg/psycopg_pool); blocking connections stall the event loop |
| HITL flows with an audit requirement | PostgresSaver / AsyncPostgresSaver |
SQL queries over the checkpoint table replace separate audit-log infrastructure | No native TTL for paused threads: store the interrupt timestamps in state |
Synthesis of the official persistence documentation and specialized deployment guides. (IDEAS/RePEc)
Interpretation. The decisive row for a regulated desk is the last one: when a human approves an order proposed by an agent, the evidence of who approved what, when, and on what market state must remain queryable years later. PostgresSaver turns the checkpoint table into that evidence with no additional infrastructure — a SELECT over the thread reconstructs the exact graph state at the moment of approval. (🦜️🔗 LangChain) SqliteSaver is the honest choice for local research (TradingAgents uses it to resume long per-ticker analyses), (langchain.js) but its write-locks rule it out for concurrent services, and MemorySaver belongs in tests. Two operational details from the docs: thread_id under 255 characters, and scheduled checkpoint pruning — the table grows without bound. (IDEAS/RePEc) Setup runs once per deployment:
from langgraph.checkpoint.postgres import PostgresSaver
checkpointer = PostgresSaver.from_conn_string("postgresql://langgraph:****@db.internal/lg")
checkpointer.setup() # creates tables and indexes
# Schedule a cron job that deletes checkpoints older than the retention policy
Checkpointer and store are not interchangeable: the checkpointer persists state per thread (today's analysis session); the Store (BaseStore: InMemoryStore, PostgresStore) is the official cross-thread memory layer, with tuple-style namespaces and semantic search. (estudy247.com) The separation in trading is sharp: the checkpointer holds this morning's bull/bear debate; the store holds thesis memory — lessons per ticker and strategy, accessible from any future session. It is the productionized version of the reflective log TradingAgents uses as memory. (langchain.js)
from langgraph.store.memory import InMemoryStore
store = InMemoryStore()
graph = builder.compile(checkpointer=checkpointer, store=store)
# When a thesis closes, a node distills the lesson into the 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."},
)
# Weeks later, any agent retrieves it before reopening the name
results = await store.asearch(("thesis", "NVDA"), query="riesgos de supply chain")
The third pillar is time travel: replay and fork from historical checkpoints, with three documented uses — debugging (rerun a failed run), A/B evaluation (same history, different prompt or model), and error recovery (rewind to just before the node that failed). (DevPress官方社区) For a desk, the fork is the quantitative audit tool: it answers "would the decision have changed with a more conservative analyst prompt?" by re-executing from the checkpoint before the debate, on the same market state. A property to know before designing HITL: interrupt() calls fire again during time travel. (arXiv.org)
Decision map: which checkpointer fits each deployment
Table 3.1 gives the criteria; here is the same reasoning in decision order, which is how it is applied in practice:
- Tests or local development? →
InMemorySaver: zero setup and a clean restart between tests — and forbidden in production, because all state dies with the process. - Single-process research? →
SqliteSaver: durability in a local file with ACID writes; its write-locks serialize concurrent threads, so it is ruled out for services. - Multi-process service or containers? →
PostgresSaver/AsyncPostgresSaverwith pooling (asyncpg/psycopg_pool); watch out: blocking connections stall the event loop. - HITL with an audit requirement? → Postgres, non-negotiable: a
SELECTover the checkpoint table reconstructs the exact graph state at the moment of approval, with no separate audit-log infrastructure.
Two operational details the list does not capture but the module pins down: thread_id under 255 characters, and the checkpoint table grows without bound without scheduled pruning per the retention policy. And do not mix up the layers: the checkpointer answers "where does this thread's state live?"; the store, "what do all threads remember?".
Time travel deserves its own paragraph: it is the audit tool that does not exist outside LangGraph. With get_state_history you locate the checkpoint before the debate, fork with update_state — a different analyst prompt, a different risk threshold — and re-execute on the same market state. It is the operational answer to the committee question "would the decision have changed with more conservative parameters?", with the caveat the module pins down and exercise 4 practices: interrupt() calls fire again during time travel, so the forensic replay will also pause at the risk gate.
3.2.3 Real human-in-the-loop: the order never leaves without sign-off
There are two HITL mechanisms, and they should not be confused. HumanInTheLoopMiddleware is configured with interrupt_on (a tool → configuration mapping), supports approve/edit/reject/respond decisions, and requires a checkpointer; since langchain>=1.3.3 it accepts a conditional predicate to interrupt only the calls that exceed a threshold — the "interrupt only orders with notional > X" pattern. (🦜️🔗 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, # safe read: no approval
},
description_prefix="Operación pendiente de aprobación del trader",
),
],
# HITL requires checkpointing. In production: AsyncPostgresSaver.
checkpointer=InMemorySaver(),
)
The second, more surgical mechanism is interrupt() inside a node: it pauses execution conditionally at an exact point and resumes with Command(resume=...) on the same thread_id. (arXiv.org) It is the right pattern when the pause is part of the business logic — for example, between the trader's plan and the transmission to the 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:
# Pause: the human trader decides on THIS exact state
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"]} # the human fixes quantity/price
return {"status": "approved"}
def send_to_broker(state: ExecutionState) -> dict:
... # FIX/REST to the OMS; only reached with 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) # pauses at interrupt()
# After human review in the supervision UI:
graph.invoke(Command(resume={"action": "approve"}), config) # continues to send_to_broker
For the supervision UI fed by that interrupt, LangGraph exposes streaming with node- and token-level granularity: since ≥1.1 the unified v2 format (version="v2", StreamPart {type, ns, data} chunks) and since v1.2 an event-streaming API with per-projection iterators. (PrivacyFilter) The number-one pitfall with nested agents — create_agent returns a compiled graph, so every agent inside a graph is a subgraph — is that without subgraphs=True the parent's messages stream does not emit the inner LLM's tokens: (PrivacyFilter)
for chunk in graph.stream(
{"messages": [{"role": "user", "content": "Analiza NVDA y propone acción."}]},
stream_mode="messages",
subgraphs=True, # without this, silence from the inner agent
version="v2",
):
print(chunk["type"]) # "messages"
print(chunk["ns"]) # () root, ("agent:<task_id>",) subgraph → routing by UI panel
print(chunk["data"]) # (token, metadata)
Risk note — compound reliability and the accountability gap. Two numbers should govern every autonomy decision. First, a chain's reliability degrades non-linearly with its length: a 10-step pipeline at 90% per step delivers only \(0{,}9^{10} \approx 0{,}35\) — 34.9% end-to-end success; each extra autonomous turn buys flexibility paid for in latency, tokens, and early-error propagation. (CSDN博客) Second, the human-agent collaboration literature identifies the accountability gap — unintended harmful actions with no clear owner — as a risk that grows with autonomy, "particularly in critical decision-making scenarios involving finance". (langchain.js) This course's operating rule follows from both: between "the agent decides to sell" and "the order reaches the broker" there must always be a checkpoint with
interrupt(). In the production guide's formulation: the agents that fail in production are not those whose LLM made a bad decision — that is expected and recoverable — but those in which the bad decision had no checkpoint between the decision and the consequence. (DevPress官方社区)
Common pitfall: decorative HITL
Wiring up the interrupt() is the easy part; making the human signature actually protect you is another matter. Four real ways to end up with a purely decorative human-in-the-loop:
- Blind signing. The supervision UI shows ticker and side ("BUY NVDA") but not the notional, the limit price, or the state that motivated the proposal. The
interrupt()payload must carry the full order and the reason for the gate — that is what the human is signing. - Resuming the wrong way. After the pause, re-running
graph.invoke({"order": ...})creates a new execution and leaves the paused one hanging; the correct path isCommand(resume=...)on the samethread_id. Two open threads on the same order is a duplication waiting to happen. - Volatile checkpointer in production. With
InMemorySaver, a process restart leaves orders pending signature in limbo: nobody can reconstruct what was on the trader's desk. Hence the HITL row of Table 3.1: Postgres or nothing. - Forgetting that time travel re-fires interrupts. If you fork from an earlier checkpoint for an A/B audit, the
interrupt()will fire again: plan for resumption in forensic replays too.
The quantitative reason this sign-off point is not optional is the compound reliability from the risk note: each autonomous step multiplies the error probability of the ones before it.

With 90% per-step reliability — optimistic for an LLM in production — ten autonomous steps in a row deliver the correct result only one time in three. The interrupt() between decision and order turns the final leg into deterministic + sign-off, taking it out of the multiplicative chain.
3.3 Multi-agent patterns for trading
3.3.1 The official v1 taxonomy: subagents, handoffs, supervisor, swarm, deepagents, and MCP
The LangChain v1 documentation recognizes four canonical multi-agent patterns: (arXiv.org) subagents (a main agent coordinates subagents invoked as tools; stateless by design — strong context isolation at the cost of repeating the flow on every call), handoffs (agents transfer control to each other via tool calls), skills (a single agent loads specialized knowledge on demand without giving up control), and router (a classification step routes the input to the specialist). The key economic data point: stateful patterns (handoffs, skills) save 40-50% of model calls on repeated requests. (arXiv.org) The mapping to trading: subagents are "analysts as the PM's tools", handoffs are "bull → bear → trader", and router is the market-regime classifier.
On top of that taxonomy, the official ecosystem publishes three orchestration libraries. langgraph-supervisor creates a hierarchical supervisor whose LLM decides which agent to delegate to via handoff tools; the returned StateGraph can be nested as an agent of another supervisor (hierarchical teams). (arXiv.org) langgraph-swarm implements the swarm: create_handoff_tool generates transfer_to_<agente> tools, and an active_agent field in the shared state acts as the relay baton — whoever holds it, executes. (genai.qa) The measurable operational difference: in the supervisor every step goes through the central coordinator (more tokens); in the swarm the handoff is direct agent→agent (fewer tokens). (genai.qa) The third, deepagents (create_deep_agent), packages a coordinated research harness — planning with write_todos, a virtual filesystem with a tool allowlist, isolated-context subagents, HITL with per-subagent interrupt_on (LangChain Forum) — whose restricted filesystem is the least-privilege pattern applicable to broker tools: the research agent is only shown read_file, ls, glob, grep, never place_order. (LangChain Forum)
The piece that connects everything to real data is MCP (Model Context Protocol), the open protocol that standardizes how applications expose tools to LLMs: with langchain-mcp-adapters, MultiServerMCPClient loads tools from multiple servers (stdio/HTTP) as standard BaseTools. (Github) It is stateless by default (each invocation creates a new ClientSession); with handle_tool_errors=True tool errors return to the model as a ToolMessage with status="error" for self-correction; and it supports interceptors with access to context, state, and store to block sensitive tools. (Github) Institutional financial-data MCP servers already exist — production forks of TradingAgents integrate FSI servers (Kensho, Aiera, FactSet, Morningstar, LSEG) as tools for the analysts. (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": { # the desk's internal MCP server (ohlcv, fundamentals)
"transport": "http",
"url": "http://mcp-marketdata.internal:8000/mcp",
},
"news": { # external vendor via MCP
"transport": "http",
"url": "https://mcp.vendor.com/mcp",
},
}
)
tools = await client.get_tools() # standard BaseTool, prefixed per server
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())
In institutional practice — Kensho/S&P Global. The best-documented production case of LangGraph orchestration on financial data is "Grounding", the multi-agent framework from Kensho (S&P Global's AI engine): a router decomposes each query into sub-queries for Data Retrieval Agents specialized by domain (equity research, fixed income, macro) and aggregates the answers in a map-reduce pattern. (IDEAS/RePEc) Two transferable lessons. First, the architecture is deliberately modest in autonomy: a router at the edge and bounded retrieval agents, not a free swarm — the industrial instance of the router pattern from the v1 taxonomy. Second, evaluation is multi-stage with exact-match routing and tool calling: it measures whether the router chose the right agent and whether the tool received the right arguments, with exact equality, not another LLM's judgment — because an LLM evaluating a financial figure is circular. (IDEAS/RePEc) On top of this layer Kensho deploys an equity research assistant, an ESG compliance agent, and the MCP server that exposes S&P data to external clients. (IDEAS/RePEc) Module 11 of the course (observability and evaluation with LangSmith) returns to this multi-stage pipeline as a CI template.
Supervisor vs swarm: where the tokens travel
The ecosystem's two hierarchical patterns differ on a single question: who speaks after each step? In the supervisor, every turn goes through the coordinator; in the swarm, the agent holding the baton (active_agent) hands control directly to the next one:
The economic consequence is what Table 3.2 quantifies: in the supervisor each delegation costs two passes through the coordinator LLM (out and back), while the swarm saves the center but pays context overhead on each handoff — hence the ≈3× tokens-per-call multiplier of a five-agent swarm versus a monolith. Neither is free: the choice is made with a calculator, not with a diagram.
An operational note on the MCP layer feeding these agents: MultiServerMCPClient is stateless by default — each invocation opens a new ClientSession — and with handle_tool_errors=True tool errors return to the model as a ToolMessage with status="error", letting it self-correct instead of breaking the loop.
3.3.2 Canonical case: TradingAgents' bull/bear/judge debate
TradingAgents (Tauric Research; arXiv 2412.20138, AAAI 2025) is the reference multi-agent trading framework and this module's worked case. (LangChain Forum) It replicates the org chart of a real firm: four analysts in parallel (market/technical, sentiment/social, news, fundamentals) write their reports into the shared state; two researchers, bull and bear, debate them for max_debate_rounds rounds; a research manager judges the debate; a trader turns the thesis into a plan; a second three-way debate (aggressive, conservative, neutral) examines the risk; and a portfolio manager issues the final approval. (langchain.js) The implementation is a StateGraph whose nodes are the roles — closures that read and write a typed state (InvestDebateState, RiskDebateState) — with SQLite checkpoints to resume long analyses and two LLM tiers (deep_think_llm for synthesis, quick_think_llm for reading). (langchain.js) The cost of a full analysis is on the order of 30-50 LLM calls per ticker. (langchain.js)
The central architectural lesson is that the debate, despite involving several LLMs, is a deterministic graph: edges fixed by code, bounded rounds, the judge as just another node with explicit routing. The LLM generates content inside each node; the graph decides who speaks next — the materialization of the pattern "the FSM governs the process, the LLM solves the sub-problem inside each state". (Morph AI)
A minimal version of the debate core — two researchers alternating with a round counter and a judge — shows how little machinery is needed on top of the primitives from section 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] # debate history
reports: list[str] # reports from the 4 analysts (input)
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: structured synthesis of the debate → verdict
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) # resumable per ticker
)
The case also demands a critical reading. The paper reports CR of 26.62% and Sharpe of 8.21 on AAPL over three months of 2024, and it is the authors themselves who warn in a footnote that this Sharpe exceeds their expected empirical range (SR>2 very good, SR>3 excellent) due to the absence of pullbacks in the period. (langchain.js) The window also falls inside the training data of the GPT-4-class models used — parametric leakage risk — and the independent large-scale replication (FINSABER: two decades, more than 100 symbols) finds that the advantages of LLM strategies deteriorate significantly outside those narrow windows. (arXiv.org) Academic criticism adds the structural nuance: existing financial multi-agent systems are broad architectures, not controlled tests of the value of multi-agent reasoning — the superior performance may come from more information or better risk management, not from the debate itself. (Github) Pedagogical verdict: TradingAgents is taught as a design pattern (roles, typed state, bounded cycles, checkpoints, LLM tiers), never as evidence of alpha. Serious forks point the same way: ThesisAgent separates reasoning (LLM) from decision (deterministic math) and reports identical decisions between the full LLM pipeline and the math-only engine. (LangChain中文文档)
Worked example: the monthly P&L of a multi-agent debate
The TradingAgents case costs 30–50 LLM calls per ticker. Let us turn the midpoint (40) into a budget line, using the framework's two tiers — deep_think_llm for synthesis, quick_think_llm for reading — with illustrative rates as of Jul-2026 (same caveat as Figure 3.1: recalculate at current rates before budgeting):
| Tier | Calls | Tokens in / out per call | Illustrative rate (in / out, per Mtok) | Cost |
|---|---|---|---|---|
| deep (debate, judge, trader) | 8 | 4,000 / 1,000 | 3.00 USD / 15.00 USD | 8 × (0.012 + 0.015) = 0.216 USD |
| quick (analysts, readings) | 32 | 3,000 / 500 | 0.30 USD / 1.50 USD | 32 × (0.0009 + 0.00075) ≈ 0.053 USD |
| Total per analysis | 40 | ≈ 0.27 USD |
Scaled to a small desk's pace: a 60-ticker watchlist, one analysis per ticker per day, 22 sessions a month → 1,320 analyses → ≈ 360 USD/month. Now the sensitivity that matters: if the same flow is built as a five-agent swarm with the ≈3× per-call overhead from Table 3.2, the bill approaches 1,000 USD/month without adding a single extra decision. Three lessons: the tier mix dominates the cost (the 32 quick calls cost a fifth of the 8 deep ones); architecture is a P&L decision before an elegance one; and the honest metric is the one from the production forks the module cites — LLM cost versus realized P/L, reviewed monthly.
3.4 The architectural decision matrix
The debate is no longer "agents, yes or no?" but "how much autonomy does each step of the pipeline justify?". The canonical reference is Anthropic's distinction between workflows — LLMs and tools orchestrated along predefined code paths — and agents — the LLM dynamically directs its own process — with the recommendation to find the simplest possible solution, because agentic systems trade latency and cost for performance. (Grepture) The winning condition: agents win when the number of steps is not known in advance and there is verifiable per-turn feedback; workflows win when the process is repeatable and auditability and fixed SLAs rule (LangChain Forum) — an exact description of a regulated desk.
The architectural layer on which the industry has converged in 2026 is "model your agent as a state machine first": the FSM governs the process flow deterministically while the LLM operates freely inside each state; you can draw the diagram, enumerate every path, and verify safety properties with deterministic tests. (Morph AI) The point that closes the circle: a StateGraph with static edges is an executable FSM; conditional edges turn it into a statechart with guards; and create_agent is a minimal FSM (model → tools → model) whose transition the LLM decides. (Morph AI) The full spectrum — pure FSM → graph with LLM guards → autonomous agent → multi-agent — is what the following matrix quantifies.
Table 3.2 — FSM → graph → agent → multi-agent matrix
| Criterion | FSM / deterministic graph (static edges) | Graph with LLM guards (conditional edges) | Autonomous agent (create_agent) |
Multi-agent (supervisor / swarm / debate) |
|---|---|---|---|---|
| Flow control | Code, 100% auditable | LLM chooses among predefined routes | LLM decides route and tools | Several LLMs decide routing and content |
| Cost / latency | Minimal (LLM only inside nodes) | Low-medium | Medium (N-iteration loop) | High: 30-50+ calls; 5-agent swarm ≈ 3× tokens per call (context overhead) |
| End-to-end reliability | High (enumerable paths) | Medium-high | Medium (chained errors; \(0{,}9^{10}\approx0{,}35\)) | Medium-low without guardrails |
| Auditability | Excellent (checkpoint + fixed paths) | Good | Requires HITL + logging | Requires HITL + per-agent tracing |
| When to use in trading | Order execution, risk checks, scheduled rebalancing | Regime classification, signal triage | Ad-hoc research, Q&A over data, hypothesis generation | Thesis debate, simulated investment committee, deep per-ticker analysis |
| Course equivalent | Execution state machine (M7, M12) | Strategy router | Analyst with tools (M5: RAG as a tool) | TradingAgents-style "trading firm" |
Synthesis of Anthropic, the FSM-orchestration literature, and cost/reliability measurements of production topologies. (Grepture)
Interpretation. The reliability column is the one a risk manager should read first: compound reliability decays exponentially with the length of the autonomous chain, so moving a step from "graph with guards" to "autonomous agent" does not add linear risk but multiplicative risk across all subsequent steps. (CSDN博客) The cost column has its own threshold: a five-agent swarm can triple token consumption per equivalent call versus a monolith, because every handoff drags context overhead — a multiplier that compounds with the call count that Module 11 budgets per architecture (§11.4). (CSDN博客) With 30-50 calls per analysis (langchain.js) and a daily watchlist of dozens of tickers, (qiankunli.github.io) the LLM line item becomes a budgetable P&L line — hence the "LLM cost vs realized P/L" metric of the production forks. (qiankunli.github.io) The last row is the course's decision rule: research = agent, thesis debate = graph with bounded cycles, execution = deterministic FSM with a human interrupt(). Autonomy grows upward in the stack, never toward execution: between decision and order there is always an FSM, a Postgres checkpoint, and a human signature. (Morph AI)

Figure 3.1 uses illustrative data built from the published benchmark of 30-50 LLM calls per analysis (langchain.js) and the ~3× tokens-per-call multiplier for five-agent swarms; (CSDN博客) unit prices are indicative as of July 2026 and must be recalculated at current rates before any real budget.
The module's intellectual close is a sentence you can defend before a committee: the LLM reasons, the graph governs, the math decides the sizing, and a human signs the order. Later modules instantiate this matrix: module 5 turns RAG into a tool of the research agent; module 7 implements the risk gate as a deterministic node with CVaR computed in code; module 12 integrates everything in the capstone.
Exercises
-
Research agent with middleware. Extend the agent from 3.1.1 with (a)
SummarizationMiddlewarewithtrigger={"tokens": 4000}, (b) theretry_modelmiddleware from 3.1.2, and (c) a customstate_schemaaddingwatchlist: list[str]. Verify that the custom state persists across calls withInMemorySaverand a fixedthread_id. -
The reducer bug. Build the graph from 3.2.1 with four analysts in parallel writing
reportswithout a reducer; run it and check which report survives. Add the concatenation reducer and repeat. Document in two sentences why last-write-wins is unacceptable in a research pipeline and which exception LangGraph raises if two nodes useOverwriteon the same key in one super-step. -
HITL with editing. Implement the
risk_gatefrom 3.2.3 withinterrupt()and a SQLite checkpointer. Run three scenarios: direct approval, human editing of the quantity (verify thatsend_to_brokerreceives the edited order), and rejection (verify that the graph ends without calling the broker). Then locate the interrupt's checkpoint in the SQLite database. -
Forensic time travel. On the graph from exercise 3, use
get_state_historyto locate the checkpoint before therisk_gate, fork withupdate_statechanging the threshold, and compare the two trajectories. Write the compliance paragraph explaining what was tested and on which exact market state. -
Bounded debate. Complete the debate
StateGraphfrom 3.3.2 with thetradernode and a downstreamrisk_gatethat interrupts if the judge's verdict isBUYwith notional above a threshold injected viacontext_schema. Measure LLM calls per analysis with a counter in a@wrap_model_callmiddleware and contrast with the 30-50 range of the TradingAgents case. (langchain.js) -
Design exercise — FSM vs graph. On paper (state diagram, no code), design the "signal → proposal → approval → execution → reconciliation" pipeline of a long-only desk, assigning each transition one of three categories: static edge (code), LLM guard (conditional edge), autonomous turn (agent). Justify each assignment with the corresponding row of Table 3.2, mark the exact point of the mandatory
interrupt(), and estimate end-to-end reliability with \(p=0{,}95\) per LLM step and \(p=1{,}0\) per deterministic step: \(P = \prod_i p_i\). Deliver the diagram and the arithmetic.
Module 3 of 15 — LangChain for Quantitative Trading. Data and versions verified as of July 2026; prices and market figures subject to change. LangChain 1.3.14 / langchain-core 1.5.2.
Elite resources to go deeper
- LangChain v1 — agent documentation (
create_agent, middleware): the normative reference for section 3.1, with the hooks model and the built-in middleware. - LangChain — multi-agent patterns guide: the official taxonomy (subagents, handoffs, skills, router) with selection criteria and cost data.
- LangGraph — persistence (checkpointers, store, memory): the source behind Table 3.1; includes the checkpointer/store separation and the operational limits.
- LangGraph — interrupts and human-in-the-loop: the exact semantics of
interrupt(),Command(resume=...), and their interaction with time travel. - Anthropic — Building Effective Agents: the canonical workflow-vs-agent essay behind the section 3.4 matrix; the "simplest solution that works" recommendation comes from here.
- arXiv 2412.20138 — TradingAgents: the bull/bear/judge debate paper; read it in full, including the footnote where the authors qualify their own Sharpe.
- arXiv 2505.07078 — FINSABER: the independent replication across two decades and more than 100 symbols that puts narrow-window results in context; the module's empirical counterweight.
- Model Context Protocol — specification: to understand what MCP standardizes before connecting market-data servers with
MultiServerMCPClient.
Check your understanding
Self-assessment with instant feedback. No scores are stored: it is just for you.