cd ~ / projects

Multi-Agent Research Assistant — LangGraph Orchestration

personal · 2026

A multi-agent research system built on LangGraph that turns a single question into a reviewed markdown report. Four cooperating agents — Supervisor, Researcher, Writer, Reviewer — are nodes in a stateful StateGraph sharing a typed Pydantic v2 state object merged via a reducer. A supervisor/orchestrator-worker pattern routes dynamically through conditional edges (the next agent is chosen at runtime from shared state, not a hard-coded pipeline). A Writer–Reviewer refinement loop iterates with a revision cap (max 3) for guaranteed termination, and live web research runs through a keyless DuckDuckGo tool plus an LLM-backed summarizer. A provider-agnostic LLM layer (Groq Llama 3.3 70B, Google Gemini, OpenAI, Anthropic Claude) is switchable via one env var, with node-by-node streaming output and a multi-stage Docker build.

What it is

A research assistant that turns one question into a reviewed, citation-grounded markdown report. Four cooperating agents — Supervisor, Researcher, Writer, Reviewer — are nodes in a LangGraph StateGraph sharing a typed Pydantic state object; routing decisions are made at runtime from state, not from a hard-coded pipeline.

Architecture

The Supervisor sequences early work: no sources in state sends control to the Researcher; sources without a draft send it to the Writer. From there the flow is fixed: every draft passes through a deterministic validate node — plain Python, no LLM — that refuses any draft with no sources, no citations, or a citation pointing at a source that doesn't exist. Only grounded drafts reach the Reviewer, whose ACCEPT/REVISE verdict is persisted in state and routed deterministically, with revisions capped at three. If nothing can be grounded, the tool exits non-zero with an INSUFFICIENT GROUNDING refusal instead of inventing an answer.

agents/graph.py
graph.add_node("validate", validate)  # plain Python - no LLM in the gate
graph.add_conditional_edges("validate", gate, {"grounded": "reviewer", "refuse": END})
graph.add_conditional_edges("reviewer", after_review, {"revise": "writer", "accept": END})
# termination lives in code: delegating "notice ACCEPT" to the LLM once caused a near-infinite loop

Decisions worth defending

Citations use footnote markers because a regex can verify them deterministically. Loop termination was moved out of the model after the near-infinite-loop incident. An opt-in human-in-the-loop mode (LangGraph interrupt() with a checkpointer) lets a human extend the loop past the cap. And the LLM layer is a factory behind a single env var — Groq Llama 3.3 70B by default, or Gemini, GPT-4o, Claude Sonnet — so the whole system runs at zero cost but isn't married to a provider.

github