QAO // QUANT AGENTIC ORCHESTRATION
AGENTS · MODULE 03/12

Agents and LangGraph: orchestration for investment decisions

create_agent, middleware, StateGraph, persistence, real human-in-the-loop, and multi-agent patterns.

READ ~34 MIN LEVEL INTERMEDIATE-ADVANCED EDITION JUL 2026

Learning objectives

Upon completing this module, the reader will be able to:


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

Diagrama
EXPANDE

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.

EXPANDE

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.

EXPANDE

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)

EXPANDE

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:

  1. Tests or local development?InMemorySaver: zero setup and a clean restart between tests — and forbidden in production, because all state dies with the process.
  2. 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.
  3. Multi-process service or containers?PostgresSaver / AsyncPostgresSaver with pooling (asyncpg/psycopg_pool); watch out: blocking connections stall the event loop.
  4. HITL with an audit requirement? → Postgres, non-negotiable: a SELECT over 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
Diagrama

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官方社区)

EXPANDE

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 is Command(resume=...) on the same thread_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.

End-to-end reliability of a pipeline as a function of the number of autonomous steps and per-step reliability

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.

EXPANDE

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:

Diagrama

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)

Diagrama

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

EXPANDE

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 — Token cost by agentic architecture (illustrative data)

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

  1. Research agent with middleware. Extend the agent from 3.1.1 with (a) SummarizationMiddleware with trigger={"tokens": 4000}, (b) the retry_model middleware from 3.1.2, and (c) a custom state_schema adding watchlist: list[str]. Verify that the custom state persists across calls with InMemorySaver and a fixed thread_id.

  2. The reducer bug. Build the graph from 3.2.1 with four analysts in parallel writing reports without 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 use Overwrite on the same key in one super-step.

  3. HITL with editing. Implement the risk_gate from 3.2.3 with interrupt() and a SQLite checkpointer. Run three scenarios: direct approval, human editing of the quantity (verify that send_to_broker receives the edited order), and rejection (verify that the graph ends without calling the broker). Then locate the interrupt's checkpoint in the SQLite database.

  4. Forensic time travel. On the graph from exercise 3, use get_state_history to locate the checkpoint before the risk_gate, fork with update_state changing the threshold, and compare the two trajectories. Write the compliance paragraph explaining what was tested and on which exact market state.

  5. Bounded debate. Complete the debate StateGraph from 3.3.2 with the trader node and a downstream risk_gate that interrupts if the judge's verdict is BUY with notional above a threshold injected via context_schema. Measure LLM calls per analysis with a counter in a @wrap_model_call middleware and contrast with the 30-50 range of the TradingAgents case. (langchain.js)

  6. 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.


EXPANDE

Elite resources to go deeper

CHECK

Check your understanding

Self-assessment with instant feedback. No scores are stored: it is just for you.