Home / Articles

Multi-Agent Financial Crime Graphs with LangChain, LangGraph, and LangSmith

A multi-agent financial crime graph with LangChain, LangGraph and LangSmith: supervisor routing, subgraphs, human in the loop interrupts, evals, guardrails.

Publisher: REACTAPP.TOP10 min read2374 words

This article is published in English.

Sources: Building Multi-Agent Financial Crime Detection Systems with LangGraph, LangChain, and LangSmith

The problem on the ground

Financial crime review is not a single prompt. A case may need KYC enrichment, sanctions screening, adverse media, transaction patterning, and a narrative for investigators—each with different tools, SLAs, and failure modes. A lone chain that tries to do everything becomes opaque, hard to test, and risky when one tool times out.

Why multi-agent beats a single mega-chain

Specialized agents keep prompts and tools focused. A supervisor can route or fan out work. Failures stay local. Human review can gate high-risk transitions. Observability can attribute cost and errors per role instead of one giant trace.

Three layers: LangChain, LangGraph, LangSmith

LangChain supplies models, tools, and message plumbing. LangGraph owns stateful orchestration—shared case state, edges, persistence, interrupts. LangSmith covers tracing, datasets, and evaluators. A plain while-loop agent can call tools, but it rarely gives durable case memory, parallel checks, subgraph reuse, or first-class HITL.

Shared case state

Start with an explicit state schema: case id, messages, scores, flags, routing decisions, and investigator notes.

from typing import TypedDict, Annotated, Literal
from langgraph.graph import StateGraph, END
from langgraph.graph.message import add_messages
from operator import add

class CaseState(TypedDict):
    case_id: str
    customer_id: str
    transactions: list[dict]
    messages: Annotated[list, add_messages]
    kyc_flags: Annotated[list[str], add]
    sanctions_hits: Annotated[list[dict], add]
    risk_score: float
    narrative: str
    requires_human_review: bool

One agent node with tools

Bind a model to domain tools (sanctions search, watchlist lookup) and let a tool node execute calls.

from langchain_anthropic import ChatAnthropic
from langchain_core.tools import tool

@tool
def screen_ofac(name: str, dob: str | None = None) -> dict:
    """Screen an individual or entity name against the UK list."""
    # In production: call a real screening API (e.g. UK100, Refinitiv)
    return {"name": name, "match": False, "score": 0.02}

@tool
def screen_un_sanctions(name: str) -> dict:
    """Screen a name against the UN Consolidated Sanctions List."""
    return {"name": name, "match": False, "score": 0.0}
llm = ChatAnthropic(model="claude-sonnet-4-6", temperature=0)
sanctions_llm = llm.bind_tools([screen_ofac, screen_un_sanctions])

def sanctions_agent(state: CaseState) -> dict:
    response = sanctions_llm.invoke(state["messages"])
    return {"messages": [response]}

Wiring the graph

Attach agent and tool nodes, use tools_condition (or equivalent) to loop until the model stops calling tools, then continue to the next stage.

from langgraph.prebuilt import ToolNode, tools_condition

graph = StateGraph(CaseState)
graph.add_node("kyc_agent", kyc_agent)
graph.add_node("sanctions_agent", sanctions_agent)
graph.add_node("sanctions_tools", ToolNode([screen_ofac, screen_un_sanctions]))
graph.add_node("tx_monitoring_agent", tx_monitoring_agent)
graph.add_node("narrative_agent", narrative_agent)
graph.set_entry_point("kyc_agent")
graph.add_edge("kyc_agent", "sanctions_agent")
# Conditional edge: loop back to tools if the LLM requested a tool call
graph.add_conditional_edges(
    "sanctions_agent",
    tools_condition,
    {"tools": "sanctions_tools", END: "tx_monitoring_agent"},
)
graph.add_edge("sanctions_tools", "sanctions_agent")
graph.add_edge("tx_monitoring_agent", "narrative_agent")
graph.add_edge("narrative_agent", END)
app = graph.compile()

Test nodes with fake state before assembling the full graph:

def test_sanctions_agent_flags_high_score_match():
    fake_state = {
        "messages": [HumanMessage(content="Screen: Igor Petrov, DOB 1978-03-11")],
    }
    result = sanctions_agent(fake_state)
    assert "tool_calls" in result["messages"][-1].additional_kwargs

Supervisor routing

A supervisor prompt classifies the next specialist—KYC, sanctions, adverse media—or ends with a summary.

from langchain_core.messages import SystemMessage

SUPERVISOR_PROMPT = """You are a financial crime case supervisor.
Route to exactly one of: kyc_agent, sanctions_agent, tx_monitoring_agent,
adverse_media_agent, narrative_agent, or FINISH if the case is fully investigated."""

def supervisor(state: CaseState) -> dict:
    routing_llm = llm.with_structured_output(RouteDecision)
    decision = routing_llm.invoke(
        [SystemMessage(content=SUPERVISOR_PROMPT)] + state["messages"]
    )
    return {"next": decision.next_agent}

def route_next(state: CaseState) -> str:
    return state["next"] if state["next"] != "FINISH" else END
graph.add_node("supervisor", supervisor)
for agent_name in ["kyc_agent", "sanctions_agent", "tx_monitoring_agent", "adverse_media_agent"]:
    graph.add_edge(agent_name, "supervisor")
graph.add_conditional_edges("supervisor", route_next)

Parallel fan-out

Independent checks can run after KYC without serializing everything:

graph.add_edge("kyc_agent", "sanctions_agent")
graph.add_edge("kyc_agent", "adverse_media_agent")
graph.add_edge("kyc_agent", "tx_monitoring_agent")

# Fan-in: all three must complete before scoring
graph.add_edge("sanctions_agent", "risk_scoring_agent")
graph.add_edge("adverse_media_agent", "risk_scoring_agent")
graph.add_edge("tx_monitoring_agent", "risk_scoring_agent")

Human-in-the-loop

Checkpoint, interrupt when risk exceeds policy, stream partial state to a case UI, then resume with investigator input.

from langgraph.checkpoint.memory import MemorySaver
from langgraph.types import interrupt, Command

def compliance_review(state: CaseState) -> dict:
    if state["risk_score"] > 0.7:
        decision = interrupt({
            "narrative": state["narrative"],
            "risk_score": state["risk_score"],
            "action_needed": "approve_or_reject_sar",
        })
        return {"requires_human_review": False, "human_decision": decision}
    return {"requires_human_review": False}

checkpointer = MemorySaver()
app = graph.compile(checkpointer=checkpointer)
config = {"configurable": {"thread_id": "case-2026-00931"}}
result = app.invoke({"case_id": "case-2026-00931", ...}, config=config)

# Later, once a human reviews in your case management UI:
final = app.invoke(Command(resume={"approved": True, "reviewer": "j.smith"}), config=config)
for chunk in app.stream(
    {"case_id": "case-2026-00931", ...},
    config=config,
    stream_mode="updates",
):
    for node_name, node_output in chunk.items():
        print(f"[{node_name}] emitted: {node_output}")
        # push node_output to a websocket / case UI here

Subgraphs for reusable units

Package KYC resolution as its own graph and mount it as a node so other case types reuse the same unit.

kyc_subgraph = StateGraph(CaseState)
kyc_subgraph.add_node("resolve_entity", resolve_entity_node)
kyc_subgraph.add_node("verify_documents", verify_documents_node)
kyc_subgraph.add_node("pep_check", pep_check_node)
kyc_subgraph.set_entry_point("resolve_entity")
kyc_subgraph.add_edge("resolve_entity", "verify_documents")
kyc_subgraph.add_edge("verify_documents", "pep_check")
kyc_subgraph.add_edge("pep_check", END)
compiled_kyc = kyc_subgraph.compile()
parent_graph = StateGraph(CaseState)
parent_graph.add_node("kyc_process", compiled_kyc)  # subgraph as a node

Observability with LangSmith

Enable tracing and tag runs with case ids and agent roles.

import os

os.environ["LANGSMITH_TRACING"] = "true"
os.environ["LANGSMITH_API_KEY"] = "ls__..."
os.environ["LANGSMITH_PROJECT"] = "financial-crime-agents"
result = app.invoke(
    {"case_id": "case-2026-00931", ...},
    config={
        "configurable": {"thread_id": "case-2026-00931"},
        "metadata": {"case_id": "case-2026-00931", "customer_tier": "high_risk"},
        "tags": ["sar_pipeline", "prod"],
    },
)

Evaluation-driven iteration

Dataset evals catch regressions when prompts or tools change. LLM-as-judge can score narrative clarity for SAR-like summaries—always with human gold where stakes are high.

from langsmith import Client
from langsmith.evaluation import evaluate

client = Client()
def correctness_evaluator(run, example) -> dict:
    predicted_risk = run.outputs.get("risk_score", 0)
    actual_risk = example.outputs.get("risk_score", 0)
    return {
        "key": "risk_score_accuracy",
        "score": 1.0 if abs(predicted_risk - actual_risk) < 0.15 else 0.0,
    }
results = evaluate(
    lambda inputs: app.invoke(inputs, config={"configurable": {"thread_id": "eval"}}),
    data="aml-labeled-cases-2026-h1",
    evaluators=[correctness_evaluator],
    experiment_prefix="sar-pipeline-v3",
)
from langsmith.evaluation import LangChainStringEvaluator

narrative_judge = LangChainStringEvaluator(
    "criteria",
    config={
        "criteria": {
            "cites_evidence": "Does the narrative cite specific transaction IDs and dates?",
            "regulatory_tone": "Is the language neutral and factual, avoiding speculation?",
        },
        "llm": ChatAnthropic(model="claude-sonnet-4-6", temperature=0),
    },
)
results = evaluate(
    lambda inputs: app.invoke(inputs, config=config),
    data="aml-labeled-cases-2026-h1",
    evaluators=[correctness_evaluator, narrative_judge],
    experiment_prefix="sar-narrative-v2",
)

Guardrails and failure handling

Retry flaky tool nodes, cap recursion, and fail closed on sanctions outages rather than inventing a clear result.

from langgraph.pregel import RetryPolicy

graph.add_node(
    "sanctions_tools",
    ToolNode([screen_ofac, screen_un_sanctions]),
    retry=RetryPolicy(max_attempts=3, backoff_factor=2.0),
)
result = app.invoke(initial_state, config={**config, "recursion_limit": 25})

Pitfalls in production AML graphs

Hidden shared mutable globals, unsupervised auto-filing, missing lineage from score to evidence, and supervisors that re-ask the same agent forever. Cap loops, require citations in state, and keep filing actions behind HITL.

Reference shape

A practical topology: supervisor in the center, specialist agents with tool loops, optional parallel branches, merge/narrative node, interrupt gate, then export—wrapping reusable subgraphs where units repeat.

             ┌─────────────┐
             │  Supervisor │◄───────────────-─┐
             └──────┬──────┘                  │
     ┌───────────────┼───────────────┐        │
     ▼               ▼               ▼        │
┌─────────┐   ┌──────────────┐  ┌───────────┐ │
│   KYC   │   │ Sanctions    │  │ TX Monitor│ │
│Subgraph │   │ Agent + Tools│  │  Agent    │ │
└────┬────┘   └──────┬───────┘  └─────┬─────┘ │
     └───────────────┴────────────────┘       │
                       ▼                      │
              ┌─────────────────┐             │
              │ Risk Scoring    │─────────────┘
              └────────┬────────┘
                       ▼
              ┌─────────────────┐        ┌──────────────┐
              │ Narrative Agent │───────►│ Human Review │
              └─────────────────┘        │ (interrupt)  │
                                         └──────┬───────┘
                                                 ▼
                                          SAR Filed / Closed

Cost and latency

Fan-out burns tokens; cache stable KYC profiles; use smaller models for routing and larger ones for narratives; stream UI updates so investigators see progress before the final write-up.

Closing notes

Multi-agent financial crime systems succeed when state is explicit, specialists are testable, orchestration is visible, and humans remain in the loop for irreversible actions. LangGraph gives the control plane; LangChain the tools; LangSmith the feedback loop that keeps the graph honest as typologies evolve.

Governance for regulated investigation agents

AML and fraud teams already live under model risk and audit requirements. An agent graph must emit evidence trails that investigators and regulators can follow: which tools ran, which lists matched, which thresholds fired, who approved an override, and which model version wrote the narrative. Store those fields in case state and export them with the case file, not only in a proprietary trace UI.

Prompt and tool changes should go through the same change-management path as rules engines. Shadow-run new supervisor prompts against historical cases, compare routing distributions, and require sign-off before promoting. LangSmith datasets help, but governance owners still need a written acceptance bar.

Human factors matter. If interrupts fire too often, investigators ignore them; if too rarely, risk escapes. Tune thresholds with sampling reviews. Streamed intermediate state should highlight uncertainty, not only confident prose, so reviewers know where to look.

Adversarial robustness is part of reliability: tool results can be noisy or hostile (poisoned web text in adverse media). Validation nodes should sanitize and cite. Never let a single scraped paragraph authorize a filing action without corroboration gates.

When measuring success, balance automation rate against quality: percentage of cases auto-closed, average time-to-triage, false-clear rate on sampled audits, and dollar cost per case. Graphs that save analyst time but raise false clears are not wins. Keep a kill switch that routes everything to humans if sanctions vendors degrade.

These practices turn the reference architecture from a clever diagram into an operable system. The code samples above are scaffolding; the operating model is what keeps financial crime agents trusted.

Building the specialist prompts without duplication

Each specialist agent should receive only the slice of case state it needs. Dumping the entire investigation into every prompt wastes tokens and leaks irrelevant PII into tools that do not need it. Define view functions: sanctions_view(state), kyc_view(state), and unit-test them. Supervisors see routing summaries; writers see evidence packs.

Shared tool clients belong in a library with retries and metrics. Agents should not each reimplement HTTP timeouts differently. Centralize list-provider credentials and circuit breakers so a sanctions outage fails consistently across the graph.

Data lineage and feature stores

Transaction features used by patterning agents should come from governed pipelines, not ad-hoc SQL inside a tool. Version feature definitions. When an analyst disputes a score, you must reconstruct inputs. Graphs that cannot explain a score will not survive model-risk review.

Parallelism and consistency

Fan-out branches must merge deterministically. Define reducers for list fields and last-writer rules for scalars. Test concurrent updates. If adverse media and sanctions both write risk_flags, use annotated reducers so flags union instead of overwrite.

Human workflow integration

Case management systems (Jira-like, custom CRMs) should own interrupts. Push interrupt payloads as tasks; on completion, call resume APIs with the task result. Do not build a one-off approval website if investigators already live in an existing console—embed there.

Red teaming the graph

Simulate tool poisoning, prompt injection in customer notes, and mismatched entity resolution. Ensure the graph refuses to file when corroboration is weak. Record these simulations as eval cases. Financial crime agents are dual-use; attackers will try to force clears.

Cost governance

Tag LangSmith (or equivalent) runs with case_type, agent, and model. Budget per case type. Alert on outliers. Prefer deterministic rules for obvious clears/declines and reserve LLMs for ambiguous narrative work. Hybrid systems beat pure LLM theater on both cost and auditability.

Staffing and ownership

Name an orchestration owner, a tools/data owner, and a compliance owner. Ambiguity here produces beautiful diagrams that nobody can change safely. Schedule joint reviews when adding a new specialist agent—every new node is a new failure domain.

With those practices, the LangGraph multi-agent pattern becomes an operating system for investigations rather than a demo that only works on one happy-path case id.

From pilot to line-of-business adoption

Pilots often run on a handful of synthetic cases. Line-of-business adoption needs localization of typologies, language support for narratives, and integration with existing case clocks and SLAs. Plan a staged rollout by segment: low-risk products first, higher-risk corridors later. Keep a control group of manually reviewed cases to measure lift. Communicate that model-assisted does not mean unsupervised. Training for investigators should cover how to read graph traces, how to override, and how to report bad tool data. Technology without that human curriculum underperforms even when the architecture diagram is flawless.

Expect vendors and internal data teams to change APIs underneath you. Version pins, canary graphs, and feature flags on supervisor prompts are mandatory. The multi-agent financial crime system is a living service, not a one-time notebook conversion.

Checklist before production traffic

State schema reviewed by compliance; tools wrapped with authz and timeouts; supervisor evals green on historical cases; HITL path exercised end-to-end; LangSmith (or peer) dashboards live; recursion limits set; kill switch documented; cost budgets alarmed; subgraph versioning agreed; parallel merge reducers tested; narrative citations required; on-call playbooks published. Multi-agent financial crime detection is less about clever prompts and more about these unglamorous boxes. Tick them, then scale case volume carefully, watching false-clear samples every week for the first quarter.

Partnering with investigators as product users

Investigators are not an afterthought UI. Sit with them during pilots. Watch where they distrust summaries, where they need raw tool dumps, and where they want one-click escalate. Encode those needs as state fields and interrupt payloads. A graph that optimizes only for automation rate will be bypassed, and bypasses erase the audit benefits you claimed.

Co-design dashboards that show why the supervisor routed to sanctions before KYC on a given Tuesday. Transparency builds adoption. Adoption builds the feedback that improves prompts and thresholds. Without that loop, even a perfect reference architecture stagnates after launch week.

Publish a monthly quality report that analysts help grade. Shared ownership keeps the multi-agent system aligned with the mission—catching financial crime—rather than aligned with demo metrics alone. Do that consistently and the LangGraph multi-agent design earns trust the only way that matters in financial crime work: by surviving contact with real cases, real investigators, and real audits—not only by looking elegant in a reference diagram. Ship the boring controls first; let clever routing earn its place only after the controls hold. Investigators will notice the difference immediately. Build for that scrutiny always. Nothing less suffices here.