Build an AI Debate Arena with LangGraph
Build a multi-agent debate system with LangGraph using cyclic graph workflows.
Introduction
30 Second Summary
Debates are one of the oldest ways humans sharpen ideas. Two people argue, a judge listens, and eventually someone wins. What if you could set up that same dynamic between AI agents and watch them go at it on any topic you choose?
In this project, you will build an AI Debate Arena where two AI agents argue for and against a topic in rounds, while a judge agent watches and declares a winner.
What You'll Build
A Python script that orchestrates a multi-agent debate using LangGraph, where two AI debaters powered by Google Gemini face off and a judge decides who wins.
By the end of this project, you'll have:
- A LangGraph StateGraph with three nodes (two debaters and a judge) wired into a loop that runs automatically.
- A working conditional routing system where the judge dynamically decides whether to keep debating or declare a winner.
- A reusable debate runner you can try with any topic to see how AI agents argue different subjects.
- Secret Mission: Add an audience heckler agent that roasts the weaker debater between rounds.
Are there any prerequisites?
None. This project includes full setup instructions for Python, VS Code, and a free Gemini API key. No prior experience with AI agents or LangGraph is required.
Set Up Your Python Environment and Get a Gemini API Key
Before you can build your AI debate arena, you need a few tools in place.
In this step, you'll install Python and VS Code, create a project folder with a virtual environment, install the LangGraph packages, and grab a free API key from Google.
In this step, get ready to:
- Install Python 3.10+ and VS Code.
- Create a project folder with a virtual environment and install the required packages.
- Get a free Gemini API key and set it as an environment variable.
Install Python and VS Code
Python is the programming language you'll use to build the debate arena. VS Code is the code editor where you'll write your script.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or Command Prompt (Windows) and press Enter to open it.
- Check if Python is installed by running this command:
python3 --version
✔️ I see a version number
You should see something like Python 3.10.x or higher. You're all set with Python.
ⓧ Command not found
You need to install Python 3.10 or higher.
- Go to python.org/downloads in your browser.
- Click the Download Python button to get the latest version.
- Run the installer. On Windows, check the box that says Add Python to PATH before clicking Install Now.
- Close and reopen your terminal, then run python3 --version again to confirm it works.
Now make sure VS Code is installed.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Visual Studio Code and press Enter.
✔️ VS Code opened
You're ready to go. Keep VS Code open for later.
ⓧ It's not installed yet
- Go to code.visualstudio.com in your browser.
- Click the download button for your operating system.
- Run the installer and follow the prompts.
- Open VS Code once installation finishes.
Create your project folder and install packages
A virtual environment keeps this project's packages separate from everything else on your computer. This way, installing packages for the debate arena won't interfere with other Python projects.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir debate-arena
cd debate-arena
- Confirm the folder was created by running:
ls
You should see an empty folder (no output from ls since the folder is new). That's expected.
- Create a virtual environment by running this command:
python3 -m venv venv
What does this command do?
This creates a folder called venv inside your project. It contains a self-contained copy of Python and a place to install packages without affecting your system.
- Activate the virtual environment:
macOS
source venv/bin/activate
Windows
venv\Scripts\activate
You should see (venv) appear at the beginning of your terminal prompt. This confirms the virtual environment is active.
Don't see (venv) in your prompt?
On Windows, you may need to allow script execution first. Run Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned in PowerShell, then try activating again.
Make sure you're inside the debate-arena folder. Run ls and confirm you see a venv folder listed.
help me activate my Python virtual environment
- Install the two packages your project needs by running:
pip install langgraph langchain-google-genai
What are these packages?
langgraph is the framework you'll use to build the debate graph. It lets you wire AI agents together with explicit control over how data flows between them.
langchain-google-genai connects your code to Google's Gemini AI model. This is what powers each debater's arguments.
The install will download several dependencies. Wait until you see the Successfully installed message at the end.
Seeing a network error or timeout?
Check your internet connection and try running the pip install command again. Temporary network issues are the most common cause.
If you see a permissions error, make sure (venv) is showing in your prompt. Installing without an active virtual environment can cause permission problems.
help me fix my pip install error
Get a free Gemini API key
Your debate agents need access to an AI model to generate arguments. Google AI Studio gives you a free API key for the Gemini model with no credit card required.
- Open aistudio.google.com in your browser.
- Sign in with your Google account.
- Click Get API Key in the left sidebar.
- Click Create API Key.
- Select the default project (or create a new one if prompted).
- Copy the API key that appears. You'll need it in a moment.
What is an API key?
An API key is like a password that lets your code talk to Google's Gemini model. Every time your debate agents generate an argument, your code sends this key to prove it has permission to use the AI.
Now set the API key as an environment variable so your Python code can find it.
- Back in your terminal (with (venv) still active), set the environment variable by running:
macOS
export GOOGLE_API_KEY=your-api-key-here
Windows
set GOOGLE_API_KEY=your-api-key-here
- Replace your-api-key-here with the actual API key you copied from Google AI Studio.
This variable only lasts for this terminal session
If you close and reopen your terminal, you'll need to run the export (or set) command again. This is fine for a learning project.
- Verify the variable is set by running:
macOS
echo $GOOGLE_API_KEY
Windows
echo %GOOGLE_API_KEY%
You should see your API key printed back to the terminal. If you see a blank line, re-run the export command and double-check you pasted the key correctly.
- Open your debate-arena folder in VS Code by running:
code .
code command not found?
Open VS Code manually, then click File in the top menu bar, then Open Folder. Navigate to your Desktop and select the debate-arena folder.
help me open my project folder in VS Code
Your environment is ready. Next up, you'll create the first AI debater and watch it argue a case.
Build Your First Debater Node
Your environment is ready with all the packages installed. Now it's time to write the first piece of your debate arena.
In LangGraph, each agent is a "node." A node is a function that receives the current state of the graph, does some work, and returns updated state. You'll create your first node: the debater who argues FOR any topic you give it.
In this step, get ready to:
- Define a shared state structure that all agents will read from and write to.
- Write a debater node that calls Gemini to argue FOR any topic.
- Test your debater to confirm it generates arguments.
Create your debate file and configure the AI model
Every LangGraph project starts with a Python file that imports the graph primitives and sets up the AI model. You'll create debate.py as the single file for your entire debate arena.
- In VS Code, click File in the top menu bar.
- Click New File.
- Name the file debate.py and save it inside your debate-arena folder.
- Add the imports and configuration by pasting this code into debate.py:
import os
from typing import TypedDict, Literal, Annotated
import operator
from langgraph.graph import StateGraph, START, END
from langchain_google_genai import ChatGoogleGenerativeAI
# --- Configuration ---
os.environ["GOOGLE_API_KEY"] = "your-api-key-here"
llm = ChatGoogleGenerativeAI(model="gemini-2.5-flash", temperature=1.0)
- Replace your-api-key-here with your actual Gemini API key from the previous step.
What does this code do?
- The first three lines import Python's built-in types you'll use to define your state structure.
- StateGraph, START, and END are LangGraph's graph-building primitives. You'll use them in a later step to wire your agents together.
- ChatGoogleGenerativeAI is the LangChain wrapper for Gemini. Setting temperature=1.0 makes the debater's responses more creative and varied.
You won't see any output yet. The next chunk defines the state that these imports power.
Now you need a shared data structure that every node in your graph can read from and write to. In LangGraph, this is called the state. You'll define it as a Python TypedDict so every field has a clear type.
- Add the state definition below the configuration section in debate.py:
# --- State Definition ---
class DebateState(TypedDict):
topic: str
debate_history: Annotated[list[str], operator.add]
current_round: int
max_rounds: int
winner: str
What does each field hold?
- topic stores the debate question (e.g., "Pineapple belongs on pizza").
- debate_history is a list of all arguments made so far. The Annotated[list[str], operator.add] syntax tells LangGraph to append new items to the list instead of replacing it.
- current_round and max_rounds track how many rounds have passed and when to stop.
- winner stays empty until the judge declares a side has won.
Why use a reducer on debate_history?
Without the operator.add reducer, each node would overwrite the entire list when it returns a new argument. The reducer tells LangGraph to merge results by appending, so every argument accumulates across rounds.
- Save debate.py.
- Confirm your imports and state definition are valid by running this in your terminal:
python3 debate.py
You should see no output and no errors. A clean exit means Python parsed everything successfully.
Seeing an import error?
- Make sure your virtual environment is activated. You should see (venv) or similar at the start of your terminal prompt.
- Check that you installed both packages: langgraph and langchain-google-genai. Re-run pip install langgraph langchain-google-genai if needed.
- Double-check that the import line says langchain_google_genai with underscores, not hyphens.
Help me fix my import error in debate.py
Write the debater_for node function
A LangGraph node is a plain Python function. It receives the current state as input and returns a dictionary with the fields it wants to update. Your first node will prompt Gemini to argue FOR the debate topic.
- Add the debater_for function below the DebateState class in debate.py:
# --- Node Functions ---
def debater_for(state: DebateState) -> dict:
"""Argues FOR the topic."""
topic = state["topic"]
history = state["debate_history"]
current_round = state["current_round"]
if history:
# If there are previous arguments, counter them
previous_args = "\n".join(history)
prompt = (
f"You are a passionate debater arguing FOR the topic: '{topic}'.\n"
f"This is round {current_round}. Here is the debate so far:\n"
f"{previous_args}\n\n"
f"Counter the opposition's latest argument and make your strongest case FOR the topic. "
f"Keep your response to 2-3 sentences. Be witty and persuasive."
)
else:
# Opening round - no previous arguments to counter
prompt = (
f"You are a passionate debater arguing FOR the topic: '{topic}'.\n"
f"This is the opening round. Make your strongest opening argument FOR the topic. "
f"Keep your response to 2-3 sentences. Be witty and persuasive."
)
# Call Gemini and format the argument
response = llm.invoke(prompt)
argument = f"[Round {current_round} - FOR]: {response.content}"
print(f"\n\U0001f7e2 {argument}")
# Return updated state - debate_history uses the append reducer
return {"debate_history": [argument]}
What does this function do?
- It reads the current topic, debate_history, and current_round from state.
- If this is the opening round (no history), it generates an opening argument. Otherwise, it reads previous arguments and counters them.
- It calls llm.invoke(prompt) to send the prompt to Gemini and get a response.
- It returns {"debate_history": [argument]}. Because of the operator.add reducer, this appends the new argument to the existing list instead of replacing it.
- Save debate.py.
Test your debater node
Before wiring up the full graph, you should confirm your debater node actually calls Gemini and produces an argument. You'll add a temporary test block to call the function directly.
- Add this test code at the very bottom of debate.py:
# --- Quick Test (delete after testing) ---
if __name__ == "__main__":
test_state = {
"topic": "Pineapple belongs on pizza",
"debate_history": [],
"current_round": 1,
"max_rounds": 3,
"winner": "",
}
result = debater_for(test_state)
print(f"\nReturned state update: {result}")
What does this test do?
This creates a fake initial state with a topic and empty history, then calls debater_for directly. If everything works, you'll see Gemini's FOR argument printed to the terminal.
- Save debate.py and run it:
python3 debate.py
You should see a green circle emoji followed by a FOR argument about pineapple on pizza. The exact text will be different every time because Gemini generates a fresh response.
Seeing an API error?
- If you see Invalid API key, double-check that you replaced your-api-key-here with your actual Gemini API key in the code.
- If you see a quota exceeded error, wait a minute and try again. The free tier has rate limits.
- If you see Permission denied, make sure the Gemini API is enabled for your API key at aistudio.google.com.
Help me fix my Gemini API error in debate.py
- Once you see a successful FOR argument, delete the entire # --- Quick Test (delete after testing) --- block from the bottom of your file.
- Save debate.py.
✔️ Awesome, I've got everything!
Great. Double check you've saved your file after removing the test block.
ⓧ I'd like to double check the full code
Here is the complete debate.py so far. Make sure your file matches this exactly:
import os
from typing import TypedDict, Literal, Annotated
import operator
from langgraph.graph import StateGraph, START, END
from langchain_google_genai import ChatGoogleGenerativeAI
# --- Configuration ---
os.environ["GOOGLE_API_KEY"] = "your-api-key-here"
llm = ChatGoogleGenerativeAI(model="gemini-2.5-flash", temperature=1.0)
# --- State Definition ---
class DebateState(TypedDict):
topic: str
debate_history: Annotated[list[str], operator.add]
current_round: int
max_rounds: int
winner: str
# --- Node Functions ---
def debater_for(state: DebateState) -> dict:
"""Argues FOR the topic."""
topic = state["topic"]
history = state["debate_history"]
current_round = state["current_round"]
if history:
previous_args = "\n".join(history)
prompt = (
f"You are a passionate debater arguing FOR the topic: '{topic}'.\n"
f"This is round {current_round}. Here is the debate so far:\n"
f"{previous_args}\n\n"
f"Counter the opposition's latest argument and make your strongest case FOR the topic. "
f"Keep your response to 2-3 sentences. Be witty and persuasive."
)
else:
prompt = (
f"You are a passionate debater arguing FOR the topic: '{topic}'.\n"
f"This is the opening round. Make your strongest opening argument FOR the topic. "
f"Keep your response to 2-3 sentences. Be witty and persuasive."
)
response = llm.invoke(prompt)
argument = f"[Round {current_round} - FOR]: {response.content}"
print(f"\n\U0001f7e2 {argument}")
return {"debate_history": [argument]}
Your first debater node is alive and arguing. Next up, you'll give it an opponent and a judge to keep score.
Add the Opposition and the Judge
Your FOR debater can make its opening argument. A debate with only one side is just a speech, though.
In this step, you'll build the other two agents that make the debate work. The opposition debater reads the FOR side's arguments from state and crafts counterarguments. The judge evaluates both sides and decides whether to keep the debate going or declare a winner.
In this step, get ready to:
- Write the debater_against node function.
- Write the judge node function.
- Test both nodes individually to confirm they produce output.
Write the opposition debater
The opposition debater follows the same pattern as your FOR debater, with one key difference. It always has access to the debate history because it never goes first. It reads the previous arguments from state and directly counters them.
- In debate.py, find the end of the debater_for function (the return {"debate_history": [argument]} line).
- Add the debater_against function below it by copying this code:
def debater_against(state: DebateState) -> dict:
"""Argues AGAINST the topic."""
topic = state["topic"]
history = state["debate_history"]
current_round = state["current_round"]
previous_args = "\n".join(history)
prompt = (
f"You are a passionate debater arguing AGAINST the topic: '{topic}'.\n"
f"This is round {current_round}. Here is the debate so far:\n"
f"{previous_args}\n\n"
f"Counter the FOR side's latest argument and make your strongest case AGAINST the topic. "
f"Keep your response to 2-3 sentences. Be witty and persuasive."
)
response = llm.invoke(prompt)
argument = f"[Round {current_round} - AGAINST]: {response.content}"
print(f"\n🔴 {argument}")
return {"debate_history": [argument]}
What does this code do?
This function mirrors debater_for but argues the opposite position.
- previous_args = "\n".join(history) joins all previous arguments into a single string so the AI can read the full debate context.
- The prompt tells the AI to directly counter the FOR side's latest argument, making the debate feel like a real back-and-forth.
- It returns {"debate_history": [argument]} which appends to the list thanks to the operator.add reducer you defined in your state.
- Save debate.py.
- You won't see output yet. You'll test this function after writing the judge.
Write the judge node
The judge is the most important node in your graph. It reads all arguments, evaluates both sides, and makes a decision: either declare a winner or say "CONTINUE" to keep the debate going. This decision is what will power the conditional routing in your graph later.
- Add the judge function directly below debater_against by copying this code:
def judge(state: DebateState) -> dict:
"""Judges the debate and decides whether to continue or declare a winner."""
topic = state["topic"]
history = state["debate_history"]
current_round = state["current_round"]
max_rounds = state["max_rounds"]
previous_args = "\n".join(history)
prompt = (
f"You are an impartial debate judge evaluating a debate on: '{topic}'.\n"
f"Here is the debate so far:\n{previous_args}\n\n"
f"This is the end of round {current_round} of {max_rounds}.\n"
f"If one side has clearly won OR this is the final round, respond with ONLY:\n"
f"WINNER: FOR\n"
f"or\n"
f"WINNER: AGAINST\n"
f"followed by a one-sentence explanation.\n\n"
f"If the debate is still close and more rounds remain, respond with ONLY:\n"
f"CONTINUE\n"
f"followed by a one-sentence comment on the round."
)
response = llm.invoke(prompt)
judge_text = response.content.strip()
print(f"\n\u2696\ufe0f [Judge]: {judge_text}")
if "WINNER: FOR" in judge_text.upper():
return {"winner": "FOR", "current_round": current_round + 1}
elif "WINNER: AGAINST" in judge_text.upper():
return {"winner": "AGAINST", "current_round": current_round + 1}
else:
return {"winner": "", "current_round": current_round + 1}
What does this code do?
The judge has two responsibilities: evaluate the debate and signal what should happen next.
- The prompt forces the AI to respond in a structured format. It must say either WINNER: FOR, WINNER: AGAINST, or CONTINUE.
- The if/elif/else block parses the AI's response. If it finds a winner keyword, it sets the winner field in state.
- If no winner is found, it returns an empty winner string and increments current_round. The next step will use this state to decide whether the graph loops or stops.
- Save debate.py.
- You won't see output yet. The next substep tests both functions.
Seeing a syntax error?
- Make sure there are two blank lines between the end of debater_for and the start of debater_against, and two blank lines between debater_against and judge. Python expects this spacing between top-level functions.
- Check that all f-string quotes and parentheses are matched. A missing closing quote or parenthesis will break the entire function.
Help me fix a syntax error in my debate.py judge or debater_against function.
✔️ Awesome, I've got everything!
Double check you've saved debate.py before moving on.
ⓧ I'd like to double check the full code
import os
from typing import TypedDict, Literal, Annotated
import operator
from langgraph.graph import StateGraph, START, END
from langchain_google_genai import ChatGoogleGenerativeAI
# --- Configuration ---
os.environ["GOOGLE_API_KEY"] = "your-api-key-here"
llm = ChatGoogleGenerativeAI(model="gemini-2.5-flash", temperature=1.0)
# --- State Definition ---
class DebateState(TypedDict):
topic: str
debate_history: Annotated[list[str], operator.add]
current_round: int
max_rounds: int
winner: str
# --- Node Functions ---
def debater_for(state: DebateState) -> dict:
"""Argues FOR the topic."""
topic = state["topic"]
history = state["debate_history"]
current_round = state["current_round"]
if history:
previous_args = "\n".join(history)
prompt = (
f"You are a passionate debater arguing FOR the topic: '{topic}'.\n"
f"This is round {current_round}. Here is the debate so far:\n"
f"{previous_args}\n\n"
f"Counter the opposition's latest argument and make your strongest case FOR the topic. "
f"Keep your response to 2-3 sentences. Be witty and persuasive."
)
else:
prompt = (
f"You are a passionate debater arguing FOR the topic: '{topic}'.\n"
f"This is the opening round. Make your strongest opening argument FOR the topic. "
f"Keep your response to 2-3 sentences. Be witty and persuasive."
)
response = llm.invoke(prompt)
argument = f"[Round {current_round} - FOR]: {response.content}"
print(f"\n🟢 {argument}")
return {"debate_history": [argument]}
def debater_against(state: DebateState) -> dict:
"""Argues AGAINST the topic."""
topic = state["topic"]
history = state["debate_history"]
current_round = state["current_round"]
previous_args = "\n".join(history)
prompt = (
f"You are a passionate debater arguing AGAINST the topic: '{topic}'.\n"
f"This is round {current_round}. Here is the debate so far:\n"
f"{previous_args}\n\n"
f"Counter the FOR side's latest argument and make your strongest case AGAINST the topic. "
f"Keep your response to 2-3 sentences. Be witty and persuasive."
)
response = llm.invoke(prompt)
argument = f"[Round {current_round} - AGAINST]: {response.content}"
print(f"\n🔴 {argument}")
return {"debate_history": [argument]}
def judge(state: DebateState) -> dict:
"""Judges the debate and decides whether to continue or declare a winner."""
topic = state["topic"]
history = state["debate_history"]
current_round = state["current_round"]
max_rounds = state["max_rounds"]
previous_args = "\n".join(history)
prompt = (
f"You are an impartial debate judge evaluating a debate on: '{topic}'.\n"
f"Here is the debate so far:\n{previous_args}\n\n"
f"This is the end of round {current_round} of {max_rounds}.\n"
f"If one side has clearly won OR this is the final round, respond with ONLY:\n"
f"WINNER: FOR\n"
f"or\n"
f"WINNER: AGAINST\n"
f"followed by a one-sentence explanation.\n\n"
f"If the debate is still close and more rounds remain, respond with ONLY:\n"
f"CONTINUE\n"
f"followed by a one-sentence comment on the round."
)
response = llm.invoke(prompt)
judge_text = response.content.strip()
print(f"\n\u2696\ufe0f [Judge]: {judge_text}")
if "WINNER: FOR" in judge_text.upper():
return {"winner": "FOR", "current_round": current_round + 1}
elif "WINNER: AGAINST" in judge_text.upper():
return {"winner": "AGAINST", "current_round": current_round + 1}
else:
return {"winner": "", "current_round": current_round + 1}
Test both nodes
Both functions are written, but you haven't confirmed they actually call Gemini and produce output. A quick temporary test will verify everything works before you wire the full graph.
- Add this temporary test code at the very bottom of debate.py:
# --- Quick Test (remove after testing) ---
test_state = {
"topic": "Pineapple belongs on pizza",
"debate_history": ["[Round 1 - FOR]: Pineapple adds a sweet-savory contrast that elevates every bite."],
"current_round": 1,
"max_rounds": 3,
"winner": "",
}
print("Testing debater_against...")
debater_against(test_state)
print("\nTesting judge...")
judge(test_state)
What does this test do?
You're creating a fake state dictionary that simulates what would exist after the FOR debater has spoken. This lets you test debater_against and judge in isolation without needing the full graph wired up.
- Save debate.py.
- Run the script by entering this command in your terminal:
python3 debate.py
- You should see two printed responses. The AGAINST debater prints a red circle followed by its counterargument. The judge prints a scale emoji followed by either CONTINUE or WINNER with an explanation.
Seeing an API error?
- If you see Invalid API key, double check that your GOOGLE_API_KEY value in the file matches what you copied from Google AI Studio.
- If you see Resource exhausted, you've hit the free tier rate limit. Wait 60 seconds and try again.
- If you see NameError: name 'DebateState' is not defined, make sure your new functions are placed below the DebateState class definition in the file.
Help me debug an error when running my debate.py test.
- Once you see output from both functions, delete the entire test block (everything from # --- Quick Test (remove after testing) --- to the end of the file).
- Save debate.py.
All three agents are ready. Next up, you'll wire them into a LangGraph StateGraph and watch the full debate unfold automatically.
Wire the Graph and Run the Debate
You have three working AI agents: two debaters and a judge. Right now they exist as standalone functions with no connection between them.
In this step, you will wire them into a LangGraph StateGraph that controls who speaks when. The graph handles the flow: FOR debater speaks, AGAINST debater responds, judge evaluates, and a conditional edge decides whether to loop back for another round or end the debate.
In this step, get ready to:
- Write a conditional edge function that decides when the debate ends.
- Build and compile the full StateGraph with nodes, edges, and conditional routing.
- Run a live AI debate and watch the agents argue in real time.
Write the conditional edge function
A conditional edge is what makes your graph dynamic. Instead of always going to the same next node, the graph calls a function that decides where to route based on the current state.
Your debate needs to end in two cases: either the judge declares a winner, or the maximum number of rounds is reached. Otherwise, the debate loops back for another round.
- In debate.py, scroll down to the bottom of your judge function.
- Add the following code below the judge function:
# --- Conditional Edge ---
def should_continue(state: DebateState) -> Literal["debater_for", "__end__"]:
"""Decide whether the debate continues or ends."""
if state["winner"]:
return "__end__"
if state["current_round"] > state["max_rounds"]:
return "__end__"
return "debater_for"
What does this code do?
This function is the brain of your graph's routing logic. LangGraph calls it after the judge node finishes to decide what happens next.
- If state["winner"] has a value (the judge picked a side), the function returns "__end__" which tells LangGraph to stop the graph.
- If current_round exceeds max_rounds, the debate also ends.
- Otherwise it returns "debater_for", which sends the graph back to the FOR debater for another round. This creates the loop.
- Save debate.py.
You won't see a change yet. The next chunk uses this function when building the graph.
Build and compile the StateGraph
Now you will assemble all the pieces into a single graph. You register each node, define the edges between them, and attach your conditional routing function.
- Add the following code below the should_continue function in debate.py:
# --- Build the Graph ---
builder = StateGraph(DebateState)
builder.add_node("debater_for", debater_for)
builder.add_node("debater_against", debater_against)
builder.add_node("judge", judge)
builder.add_edge(START, "debater_for")
builder.add_edge("debater_for", "debater_against")
builder.add_edge("debater_against", "judge")
builder.add_conditional_edges("judge", should_continue)
graph = builder.compile()
What does this code do?
This is where you define the flow of your multi-agent system.
- StateGraph(DebateState) creates a new graph that uses your DebateState TypedDict as the shared state between all nodes.
- add_node registers each function as a named node in the graph.
- add_edge creates fixed connections: START goes to the FOR debater, who goes to the AGAINST debater, who goes to the judge.
- add_conditional_edges attaches your should_continue function to the judge node. After the judge runs, LangGraph calls should_continue to decide the next destination.
- builder.compile() finalizes the graph into a runnable object you can invoke.
Why is this a loop and not a chain?
A chain moves forward in one direction and stops. Your debate graph has a cycle: judge can route back to debater_for, which goes to debater_against, which goes back to judge again.
This cyclic workflow is what makes LangGraph different from simple LangChain chains. You get explicit control over when to loop and when to stop.
- Save debate.py.
You won't see a change yet. The next chunk adds the code that actually invokes the graph.
Run the debate
Your graph is built and compiled. Now you need to invoke it with an initial state containing a debate topic and watch the agents go at it.
- Add the following code at the very bottom of debate.py:
# --- Run the Debate ---
if __name__ == "__main__":
print("=" * 60)
print("\U0001f3a4 AI DEBATE ARENA")
print("=" * 60)
topic = "Pineapple belongs on pizza"
print(f"\n\U0001f4cb Topic: {topic}")
print("-" * 60)
result = graph.invoke({
"topic": topic,
"debate_history": [],
"current_round": 1,
"max_rounds": 3,
"winner": "",
})
print("\n" + "=" * 60)
if result["winner"]:
print(f"\U0001f3c6 WINNER: {result['winner']}")
else:
print("\U0001f91d DEBATE ENDED (max rounds reached)")
print("=" * 60)
What does this code do?
- if __name__ == "__main__": ensures this block only runs when you execute the file directly (not when importing it).
- graph.invoke({...}) starts the graph with an initial state. The graph runs through all nodes and loops until should_continue returns "__end__".
- The initial state sets the topic, an empty debate history, round 1, a maximum of 3 rounds, and no winner yet.
- After the graph finishes, the code checks if a winner was declared and prints the result.
- Save debate.py.
- Run the debate by executing this command in your terminal:
python3 debate.py
You should see a full debate play out in your terminal, round by round. Each debater's argument prints as it is generated, followed by the judge's evaluation.
Seeing an error?
- If you see Invalid API key, double-check that you replaced your-api-key-here with your actual Gemini API key in the os.environ line.
- If you see ModuleNotFoundError, make sure your virtual environment is activated. You should see (venv) at the start of your terminal prompt.
- If the debate seems stuck or takes a long time, the Gemini free tier has rate limits. Wait a minute and try again.
Still stuck? Help me debug my debate arena script
- Try running the script a second time. Because the temperature is set to 1.0, you will get completely different arguments and possibly a different winner each time.
- Experiment with changing the topic variable to something else, like "Cats are better than dogs" or "Remote work is more productive than office work".
- Try changing max_rounds to 5 to see a longer debate unfold.
✔️ Awesome, I've got everything!
Great. Double check you have saved debate.py and that your debate runs successfully.
ⓧ I'd like to double check the full code
Here is the complete debate.py file. Compare your code against this:
import os
from typing import TypedDict, Literal, Annotated
import operator
from langgraph.graph import StateGraph, START, END
from langchain_google_genai import ChatGoogleGenerativeAI
# --- Configuration ---
os.environ["GOOGLE_API_KEY"] = "your-api-key-here"
llm = ChatGoogleGenerativeAI(model="gemini-2.5-flash", temperature=1.0)
# --- State Definition ---
class DebateState(TypedDict):
topic: str
debate_history: Annotated[list[str], operator.add]
current_round: int
max_rounds: int
winner: str
# --- Node Functions ---
def debater_for(state: DebateState) -> dict:
"""Argues FOR the topic."""
topic = state["topic"]
history = state["debate_history"]
current_round = state["current_round"]
if history:
previous_args = "\n".join(history)
prompt = (
f"You are a passionate debater arguing FOR the topic: '{topic}'.\n"
f"This is round {current_round}. Here is the debate so far:\n"
f"{previous_args}\n\n"
f"Counter the opposition's latest argument and make your strongest case FOR the topic. "
f"Keep your response to 2-3 sentences. Be witty and persuasive."
)
else:
prompt = (
f"You are a passionate debater arguing FOR the topic: '{topic}'.\n"
f"This is the opening round. Make your strongest opening argument FOR the topic. "
f"Keep your response to 2-3 sentences. Be witty and persuasive."
)
response = llm.invoke(prompt)
argument = f"[Round {current_round} - FOR]: {response.content}"
print(f"\n\U0001f7e2 {argument}")
return {"debate_history": [argument]}
def debater_against(state: DebateState) -> dict:
"""Argues AGAINST the topic."""
topic = state["topic"]
history = state["debate_history"]
current_round = state["current_round"]
previous_args = "\n".join(history)
prompt = (
f"You are a passionate debater arguing AGAINST the topic: '{topic}'.\n"
f"This is round {current_round}. Here is the debate so far:\n"
f"{previous_args}\n\n"
f"Counter the FOR side's latest argument and make your strongest case AGAINST the topic. "
f"Keep your response to 2-3 sentences. Be witty and persuasive."
)
response = llm.invoke(prompt)
argument = f"[Round {current_round} - AGAINST]: {response.content}"
print(f"\n\U0001f534 {argument}")
return {"debate_history": [argument]}
def judge(state: DebateState) -> dict:
"""Judges the debate and decides whether to continue or declare a winner."""
topic = state["topic"]
history = state["debate_history"]
current_round = state["current_round"]
max_rounds = state["max_rounds"]
previous_args = "\n".join(history)
prompt = (
f"You are an impartial debate judge evaluating a debate on: '{topic}'.\n"
f"Here is the debate so far:\n{previous_args}\n\n"
f"This is the end of round {current_round} of {max_rounds}.\n"
f"If one side has clearly won OR this is the final round, respond with ONLY:\n"
f"WINNER: FOR\n"
f"or\n"
f"WINNER: AGAINST\n"
f"followed by a one-sentence explanation.\n\n"
f"If the debate is still close and more rounds remain, respond with ONLY:\n"
f"CONTINUE\n"
f"followed by a one-sentence comment on the round."
)
response = llm.invoke(prompt)
judge_text = response.content.strip()
print(f"\n\u2696\ufe0f [Judge]: {judge_text}")
if "WINNER: FOR" in judge_text.upper():
return {"winner": "FOR", "current_round": current_round + 1}
elif "WINNER: AGAINST" in judge_text.upper():
return {"winner": "AGAINST", "current_round": current_round + 1}
else:
return {"winner": "", "current_round": current_round + 1}
# --- Conditional Edge ---
def should_continue(state: DebateState) -> Literal["debater_for", "__end__"]:
"""Decide whether the debate continues or ends."""
if state["winner"]:
return "__end__"
if state["current_round"] > state["max_rounds"]:
return "__end__"
return "debater_for"
# --- Build the Graph ---
builder = StateGraph(DebateState)
builder.add_node("debater_for", debater_for)
builder.add_node("debater_against", debater_against)
builder.add_node("judge", judge)
builder.add_edge(START, "debater_for")
builder.add_edge("debater_for", "debater_against")
builder.add_edge("debater_against", "judge")
builder.add_conditional_edges("judge", should_continue)
graph = builder.compile()
# --- Run the Debate ---
if __name__ == "__main__":
print("=" * 60)
print("\U0001f3a4 AI DEBATE ARENA")
print("=" * 60)
topic = "Pineapple belongs on pizza"
print(f"\n\U0001f4cb Topic: {topic}")
print("-" * 60)
result = graph.invoke({
"topic": topic,
"debate_history": [],
"current_round": 1,
"max_rounds": 3,
"winner": "",
})
print("\n" + "=" * 60)
if result["winner"]:
print(f"\U0001f3c6 WINNER: {result['winner']}")
else:
print("\U0001f91d DEBATE ENDED (max rounds reached)")
print("=" * 60)
You just built a fully functional multi-agent debate system. Three AI agents collaborate through a graph, passing state between them and looping until the judge calls it. Next up, the Secret Mission takes your arena to the next level.
Secret mission
Add an Audience Heckler Agent
Your debate arena works, but it feels like a quiet courtroom. Real debates have audiences that react. In this secret mission, you'll add a fourth agent that heckles the weaker debater between rounds.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally with a free Gemini API key, so there are no ongoing costs. Decide whether to keep your resources as they are, pause them to come back later, or delete them entirely.
Resources you used:
- Python virtual environment (venv folder inside your project directory).
- The debate.py file with your debate arena code.
- Installed pip packages (langgraph and langchain-google-genai).
- Gemini API key from Google AI Studio.
Keep everything running
No action needed. Choose this if you want to keep experimenting with different debate topics or extend the project further.
- Your project folder, virtual environment, and API key all stay as they are.
- You can rerun the debate any time with python debate.py.
Pause - I'll come back to this later
Deactivate the virtual environment to free up your terminal session. Your files and packages stay on disk for when you return.
- Run deactivate in your terminal to exit the virtual environment.
- When you come back, reactivate with source venv/bin/activate (macOS/Linux) or venv\Scripts\activate (Windows).
Delete - I don't want to use this again
Remove all project files and optionally revoke your API key.
- Delete the entire project folder from your Desktop.
- If you no longer need the Gemini API key, go to Google AI Studio and delete the key from your project settings.
- No cloud resources need to be shut down since everything ran locally.
That's a Wrap!
That's a Wrap!
Nice work! You just built a multi-agent AI debate arena powered by LangGraph. Two AI debaters squared off, a judge called the shots, and you controlled the entire flow with a graph.
What you learned:
- Built a StateGraph from scratch with three collaborating AI agent nodes, shared state, and a reducer pattern for accumulating debate history.
- Implemented conditional routing that dynamically decides whether to loop back for another round or end the debate when a winner is declared.
- Connected to Gemini via langchain-google-genai to power each agent with its own persona and prompt strategy.
- Secret Mission: Added an audience heckler agent that roasts weak arguments between rounds.
Ready to quiz yourself?