A multi-agent AI system built with LangGraph that turns a single question into a polished, well-researched report. A team of specialized agents — a Researcher, a Writer, and a Reviewer — collaborate under a central Supervisor that orchestrates the workflow as a stateful graph.
It demonstrates the supervisor / orchestrator-worker pattern for agentic systems, dynamic LLM-based routing via conditional edges, an iterative writer↔reviewer refinement loop, tool use for live web search, and a fully provider-agnostic LLM layer (Groq, Gemini, OpenAI, or Anthropic — switchable with one environment variable).
- Multi-agent orchestration — cooperating agents modeled as nodes in a LangGraph
StateGraph. - Dynamic supervisor routing — a coordinator node decides which agent runs next based on shared state, instead of a brittle hard-coded pipeline.
- Grounded, cite-or-refuse output — the Writer must cite retrieved sources with footnote markers (
[^n]); a deterministicvalidatenode refuses to ship a draft that has no sources, no citations, or citations pointing at sources that don't exist (or that were retrieved empty). See the incident write-up for why. - Iterative refinement — the Writer and Reviewer loop until the draft is accepted or
MAX_REVISIONSis reached; termination is decided deterministically, so the loop provably halts. - Live web research — the Researcher retrieves keyless DuckDuckGo results and fetches the page text, carrying structured sources (title, URL, content) through the graph.
- Provider-agnostic LLM layer — run on Groq (free, default), Gemini, OpenAI, or Anthropic by changing
LLM_PROVIDER. - Streaming output — node-by-node progress is streamed to the terminal as the graph executes.
- Human-in-the-loop (opt-in) —
--human-reviewpauses the graph (LangGraphinterrupt+ aMemorySavercheckpointer) so a human makes the final accept/revise call, overriding the model's verdict. Because a human bounds the loop, a "revise" is honored even pastMAX_REVISIONS. Resumes viaCommand(resume=...). - Observability — LangSmith tracing auto-instruments every LLM/graph step when env vars are set; startup reports whether it's on.
- Zero-cost by default — Groq's free tier + keyless search means it runs without any paid API or credit card.
graph TD;
__start__([start]) --> supervisor
supervisor -.->|no sources| researcher
supervisor -.->|sources, no draft| writer
researcher --> supervisor
writer --> validate
validate -.->|grounded| reviewer
validate -.->|ungrounded → refuse| __end__([END])
reviewer -.->|REVISE| writer
reviewer -.->|ACCEPT / max revisions| __end__
This diagram is generated from the compiled graph itself:
build_graph().get_graph().draw_mermaid().
Two design choices worth calling out:
- The Supervisor sequences the early work (research → first draft), but it does not own termination. Once a draft exists, control flows
writer → validate → reviewer, and the Reviewer's verdict deterministically ends the loop (viaroute_after_review) rather than asking the LLM Supervisor to notice "ACCEPT." Delegating termination to the LLM was the original cause of a near-infinite loop. validateis a deterministic gate, not an agent. The LLM proposes a draft; plain Python disposes on whether it's grounded enough to ship. An agent graph shouldn't be all model calls — some edges are boring, testable code. This is what turns "confident hallucination" into "grounded answer or honest refusal."
| Agent | Responsibility | Tools |
|---|---|---|
| Supervisor | Inspects shared state and routes the early work (research → first draft). | LLM-based routing |
| Researcher | Retrieves web results and fetches page text, keeping structured sources (title, URL, content) in state — provenance is preserved, not summarised away. | web_search (DuckDuckGo + page fetch) |
| Writer | Transforms sources into a markdown report, citing each claim [^n]; incorporates reviewer feedback on revisions. |
LLM generation |
| Validate | Deterministic (no LLM). Refuses drafts with no sources, no citations, dangling citations, or citations to empty-content sources. | plain Python |
| Reviewer | Critiques the draft for accuracy, clarity, and completeness; issues an ACCEPT or REVISE verdict that deterministically ends or continues the loop. | LLM evaluation |
The system is a state machine. A single typed AgentState object flows through the graph, and each node returns a partial update that LangGraph merges into it.
Shared state (AgentState):
| Field | Purpose |
|---|---|
messages |
Running conversation log (uses the add_messages reducer to append). |
sources |
Structured {title, url, snippet, content} retrieved by the Researcher — the evidence every claim must trace back to. |
research_notes |
Numbered, URL-preserving view of the sources handed to the Writer. |
draft |
Current report produced by the Writer, with footnote [^n] citations. |
review_feedback |
Reviewer's critique; consumed and cleared by the Writer on each revision. |
verdict |
Reviewer's ACCEPT/REVISE decision — persisted so routing is deterministic. |
validation_error |
Set by the validate node when a draft isn't grounded; triggers refusal. |
next_agent |
The Supervisor's routing decision. |
revision_count |
Counts revision cycles; caps the loop at MAX_REVISIONS (3). |
Execution flow for one query:
- Supervisor sees no sources → routes to Researcher.
- Researcher searches + fetches pages → writes
sourcesandresearch_notes. - Supervisor sees sources but no draft → routes to Writer.
- Writer drafts a report citing
[^n]→ writesdraft. - Validate (deterministic) checks grounding →
ungroundedends the run with a refusal;grounded→ Reviewer. - Reviewer critiques →
ACCEPTorREVISE(verdict persisted to state). REVISE→ back to Writer (loop);ACCEPTorMAX_REVISIONS→ END.
| Component | Technology |
|---|---|
| Orchestration | LangGraph (StateGraph, conditional edges) |
| LLM abstraction | LangChain (langchain-core) |
| LLM — Groq (default) | Llama 3.3 70B via langchain-groq |
| LLM — Gemini | gemini-2.5-flash-lite via langchain-google-genai |
| LLM — OpenAI | GPT-4o via langchain-openai |
| LLM — Anthropic | Claude Sonnet 4.5 via langchain-anthropic |
| Web search | DuckDuckGo (ddgs) via langchain-community — no API key |
| Page fetching | httpx + stdlib HTML parsing (best-effort, falls back to snippets) |
| State & validation | Pydantic v2 |
| Configuration | python-dotenv |
| Build backend | Hatchling |
| Package manager | uv |
| Linting | Ruff |
| Containerization | Docker (Python 3.12 slim) |
- Python 3.12+
- uv (
curl -LsSf https://astral.sh/uv/install.sh | sh) - A free LLM API key (Groq recommended — no credit card)
# 1. Clone
git clone https://github.com/<your-username>/langgraph-multi-agent.git
cd langgraph-multi-agent
# 2. Configure environment
cp .env-template .env
# Edit .env and add your GROQ_API_KEY (free at https://console.groq.com/keys)
# 3. Install dependencies (creates .venv)
uv sync
# 4. Run a research query
uv run python main.py "What are the latest breakthroughs in quantum computing?"
# Verbose mode prints the full output of every agent
uv run python main.py --verbose "Explain the current state of nuclear fusion energy"
# Human-in-the-loop: pause for your approve/revise decision before finishing
uv run python main.py --human-review "How do I connect Langfuse to LangGraph?"Set the provider with LLM_PROVIDER in .env and supply the matching key:
| Variable | Required | Description |
|---|---|---|
LLM_PROVIDER |
No | groq (default), gemini, openai, or anthropic |
GROQ_API_KEY |
If groq |
Groq — free tier, no credit card |
GOOGLE_API_KEY |
If gemini |
Google AI Studio |
OPENAI_API_KEY |
If openai |
OpenAI |
ANTHROPIC_API_KEY |
If anthropic |
Anthropic |
LLM_MODEL |
No | Override the provider's default model |
LANGSMITH_TRACING |
No | Set to true (with LANGSMITH_API_KEY) to trace every step in LangSmith |
Web search uses DuckDuckGo and requires no API key.
Every LLM call and graph transition can be traced to LangSmith — the full supervisor → researcher → writer → validate → reviewer tree, token counts, latencies, and each node's inputs/outputs. It's auto-instrumented: no code wires it up, you just set env vars.
Setup (~2 min, free tier — no card):
- Create a key at smith.langchain.com → Settings → API Keys → Create API Key (
lsv2_...). - Add to your
.env:LANGSMITH_TRACING=true LANGSMITH_API_KEY=lsv2_your_key_here LANGSMITH_PROJECT=langgraph-multi-agent # optional; defaults to "default" - Run normally — the startup banner confirms it:
Traces then appear in the LangSmith UI under that project.
LangSmith tracing: ON (project='langgraph-multi-agent')
How it works: .env is loaded (load_dotenv() in agents/config.py) before the first LLM call, so LangChain/LangGraph pick up the vars and stream traces automatically. The only project code involved is the startup status line in main.py — the instrumentation is the framework's.
Notes:
langsmithships as a LangChain dependency, so nothing extra to install.- Leave
LANGSMITH_TRACINGunset/falseuntil the key is in — enabling it without a valid key just fails to upload (and adds slight latency). - Older
LANGCHAIN_TRACING_V2/LANGCHAIN_API_KEYnames also work; the startup check honors both.
$ uv run python main.py "How do I connect Langfuse to LangGraph?"
============================================================
Research query: How do I connect Langfuse to LangGraph?
LangSmith tracing: off (set LANGSMITH_TRACING=true + LANGSMITH_API_KEY to enable)
============================================================
--- [SUPERVISOR] --- [Supervisor] Routing to: researcher
--- [RESEARCHER] --- [Researcher] Retrieved 5 source(s).
--- [SUPERVISOR] --- [Supervisor] Routing to: writer
--- [WRITER] --- [Writer] Draft produced (2740 chars)
--- [VALIDATE] --- [Validate] grounded
--- [REVIEWER] --- [Reviewer] Verdict: REVISE
--- [WRITER] --- [Writer] Draft produced (5606 chars)
--- [VALIDATE] --- [Validate] grounded
--- [REVIEWER] --- [Reviewer] Verdict: ACCEPT
============================================================
FINAL REPORT
============================================================
## Overview
Langfuse is an open-source LLM-engineering platform that traces API calls,
manages prompts, and runs evaluations [^4][^5]. ...
## Sources
[1] Open Source Observability for LangGraph - Langfuse — https://langfuse.com/guides/cookbook/integration_langgraph
[4] Langfuse integrations - Docs by LangChain — https://docs.langchain.com/oss/python/integrations/providers/langfuse
[5] Get Started - Langfuse — https://langfuse.com/docs/observability/get-started
Every claim carries a
[^n]citation and the run ends with a## Sourceslist. If nothing can be grounded, the tool prints an INSUFFICIENT GROUNDING notice and exits non-zero instead of inventing an answer.
docker build -t research-assistant .
docker run --env-file .env research-assistant "Your research query here"The image uses a multi-stage build with uv for fast, reproducible dependency installation on a python:3.12-slim base.
langgraph-multi-agent/
├── agents/
│ ├── __init__.py
│ ├── config.py # Provider-agnostic LLM factory (Groq/Gemini/OpenAI/Anthropic)
│ ├── graph.py # AgentState, agent nodes, routing, and StateGraph assembly
│ └── tools.py # Custom tools: web_search (DuckDuckGo) + summarize
├── main.py # CLI entry point — builds and streams the graph
├── pyproject.toml # Project metadata, dependencies, tooling config
├── Dockerfile # Multi-stage container build
├── .env-template # Environment variable template
└── README.md
- Supervisor / orchestrator-worker pattern — a central node routes to specialist workers, keeping the workflow flexible and extensible (adding an agent = add a node + a routing branch).
- Conditional edges — the Supervisor uses
add_conditional_edgesso the next step is decided at runtime from state, rather than fixed at graph-build time. - Reducers —
messagesuses LangGraph'sadd_messagesreducer to append history; other fields overwrite by default. - Loop safety —
revision_count+MAX_REVISIONSguarantees termination of the writer↔reviewer loop. - Factory + Strategy —
get_llm()constructs the configured provider behind a single interface, with lazy imports so only the chosen SDK is required.
- Semantic grounding — the
validategate checks that citations resolve, not that the source text actually supports the claim; an entailment / LLM-as-judge check would close that gap. - Structured routing — replace prose-based verdict parsing with structured output (function calling / Pydantic schemas) for more reliable control flow.
- Durable persistence — swap the in-memory
MemorySaverfor a SQLite/Postgres checkpointer to pause/resume across restarts and support multi-turn sessions. - Concurrent research — fan out searches and page fetches concurrently (currently sequential).
- Bounded supervisor — add an explicit hop counter (today only the writer↔reviewer loop is capped by
MAX_REVISIONS).
MIT