local-llm-debate GitHub ↗
local · multi-agent · zero api keys

Two local LLMs walk into a debate.
Only one leaves with the answer.

Agent A and Agent B independently answer your question, critique each other, revise, and repeat until they converge or run out of rounds — then an impartial judge picks a winner. A live terminal UI shows every step as it happens.

~/repos/Local_LLM_Debate — llm-debate
Local LLM Debate — initial terminal prompt asking for a question
Inference
100% local via Ollama
Models
qwen2.5:3b + qwen2.5:1.5b
Cloud calls
None, except optional web search
what it does

Every panel you see is a real decision

The terminal UI isn't decorative — each region reflects one thing the engine is actually doing right now, streamed over an event bus as the debate runs.

router

Resource routing

An LLM decides per-question whether to use web search, local documents, a Python tool, or nothing — and shows its confidence and reasoning.

resources

Retrieval, visible

Whatever gets pulled in — search snippets, PDF chunks, tool output — is shown verbatim, not hidden inside the prompt.

agent a / b

Independent debaters

Two agents answer separately, each with their own resources, so neither can just copy the other's reasoning.

critique

Cross-examination

Each agent critiques the other's answer for weaknesses and hallucination risk before either gets to revise.

judge

Impartial verdict

A separate judge call picks a winner with reasoning and a confidence score — it never sees which agent is which.

runtime

Convergence, not a timer

Rounds stop when answers converge or hallucination risk drops low — not just after a fixed count.

under the hood

Engine and UI never import each other

The debate engine only knows how to emit() events onto a thread-safe queue. The terminal UI only knows how to consume them. Swap the UI for a web frontend, a logger, or nothing, and the debate logic doesn't change.

Component architecture

LangGraph runs the debate on a background thread; Rich renders the live view on the main thread. They only ever talk through the event queue.

flowchart LR
    subgraph Engine["Debate Engine (LangGraph)"]
        G[graph.py] --> A1[agents/debater_a.py]
        G --> A2[agents/debater_b.py]
        G --> J[agents/judge.py]
        G --> C[agents/debate_controller.py]
        A1 --> RM[tools/resource_manager.py]
        A2 --> RM
        RM --> WEB[tools/search.py]
        RM --> RAG[rag/retriever.py]
        RM --> PY[tools/python_tool.py]
        RM --> ROUTE[tools/resource_router.py]
    end

    subgraph Bridge["Event Bus"]
        EM[ui/emitter.py] --> EB[(ui/event_bus.py<br/>thread-safe queue)]
    end

    subgraph UI["Terminal UI (Rich)"]
        EB --> R[ui/renderer.py]
        R --> ST[ui/state.py]
        R --> L[ui/layout.py]
        L --> P[ui/panels/*]
    end

    Engine -. emit&#40;Event&#41; .-> EM
    A1 -.-> EM
    A2 -.-> EM
    J -.-> EM
    C -.-> EM
    RM -.-> EM

    Ollama[(Ollama<br/>qwen2.5:3b / 1.5b)] -.-> A1
    Ollama -.-> A2
    Ollama -.-> J
    Ollama -.-> ROUTE
        

The debate graph

Both debaters answer in parallel, critique each other in parallel, revise in parallel, then synchronize before the controller decides: loop again, or hand off to the judge.

flowchart TD
    START([START]) --> DA[debater_a]
    START --> DB[debater_b]

    DA --> CB["critique_b<br/><i>B critiques A's answer</i>"]
    DB --> CA["critique_a<br/><i>A critiques B's answer</i>"]

    CA --> RA[revise_a]
    CB --> RB[revise_b]

    RA --> SYNC["sync<br/><i>barrier</i>"]
    RB --> SYNC

    SYNC --> CTRL{controller}

    CTRL -->|continue| RS[round_start] --> CA
    RS --> CB
    CTRL -->|judge| JUDGE[judge] --> END([END])
        

One question, end to end

What actually fires, in order, from the moment you press Enter.

sequenceDiagram
    participant U as You
    participant M as main.py
    participant G as LangGraph
    participant R as Resource Router
    participant O as Ollama
    participant UI as Terminal UI

    U->>M: type question, press Enter
    M->>UI: QuestionEvent
    M->>G: graph.invoke() (background thread)

    par Agent A
        G->>R: route_resources(question)
        R->>O: routing decision (web? rag? tool?)
        R-->>UI: RouterEvent, ResourceEvent
        G->>O: debater_a prompt
        O-->>UI: AgentEvent (status=Answered)
    and Agent B
        G->>R: route_resources(question)
        G->>O: debater_b prompt
        O-->>UI: AgentEvent (status=Answered)
    end

    par Critique
        G->>O: critique_a (A critiques B)
        O-->>UI: CritiqueEvent
    and
        G->>O: critique_b (B critiques A)
        O-->>UI: CritiqueEvent
    end

    par Revise
        G->>O: revise_a
        O-->>UI: AgentEvent (status=Revised)
    and
        G->>O: revise_b
        O-->>UI: AgentEvent (status=Revised)
    end

    G-->>UI: RuntimeEvent (round, elapsed, stop_reason)

    alt stop condition met
        G->>O: judge prompt
        O-->>UI: JudgeEvent (winner, reasoning, confidence)
    else continue
        Note over G: loop back to critique phase
    end

    UI-->>U: live-updating panels throughout
        
debate in progress
Local LLM Debate — live debate with router, resources, agents, critique, and judge panels
controller

When does it actually stop?

Checked every round, in this order, by tools/stopping.py:

  1. Both agents' latest answers are identical → stop (answers_converged)

  2. Both agents' hallucination risk is ≤ 2 → stop (low_hallucination_risk)

  3. round_number >= max_rounds → stop (max_rounds)

get started

Setup and run

Everything runs on your machine. The only external dependency is Ollama, which handles model download and inference.

Requirements

  • Python 3.10+
  • Ollama installed and running
  • A terminal with truecolor support — iTerm2, Windows Terminal, GNOME Terminal, Kitty, Alacritty

Setup — macOS / Linux

bash
$ git clone https://github.com/SreenathKarthick11/Local_LLM_Debate.git
$ cd Local_LLM_Debate
$ ./install.sh
# checks for Ollama, pulls qwen2.5:3b + qwen2.5:1.5b,
# creates a venv, installs the package

Run

bash
$ source .venv/bin/activate
$ llm-debate

Prefer no venv activation?

bash — pipx
# installs into an isolated env, links the command globally
$ pipx install .
using the app

From launch to verdict

  1. Upload a PDF — optional. A picker lists PDFs found in your current folder, ~/Downloads, ~/Documents, and ~/Desktop. Type a number, part of a filename, or a full path — or press Enter to skip.

  2. Type your question, press Enter.

  3. Watch the debate unfold live across the router, resources, both agents, critiques, judge, and runtime panels.

  4. Navigate freely. Click a scrollable panel to focus it, then j/ and k/ or the mouse wheel to scroll. Ctrl+C exits and cleans up at any point.

Panels at a glance

PanelShows
Routerwhich resources were used (web / RAG / tool) and why
Resourcesthe actual retrieved web text, document chunks, or tool output
Agent A / Agent Beach debater's current status, answer, and confidence
Critique A / Critique Bweaknesses and hallucination risk found in the opponent's answer
Judgethe winner, confidence, and reasoning
Runtimeround number, elapsed time, stop reason

Explicit routing commands

Force a specific resource regardless of what the router would choose, by including these anywhere in your question:

CommandForces
@webWeb search
@fileLocal document retrieval (RAG)
@pythonThe Python calculator tool

Example: What's the average of these numbers? @python

good to know

Notes on the local model setup

qwen2.5:3b handles debating and judging; qwen2.5:1.5b handles the cheaper, more frequent critique and routing calls — this split keeps latency reasonable on consumer hardware.

Structured output (AgentResponse, CritiqueResponse, JudgeResponse, ResourceRoute) is enforced via .with_structured_output(...) in llm.py, so parsing is schema-guaranteed rather than regex-scraped from free text.

The embedding model (BAAI/bge-small-en-v1.5) downloads automatically on first RAG use — this requires internet access once, even though inference afterward is fully local.