I Built an AI Debate Arena on Bedrock — 59 Models Claimed to Work, Only 54 Actually Did
I wanted two AI models to argue with each other — pick a side, take turns, land actual counterpunches — and have a third model judge the winner. Not a chatbot demo, an actual arena: pick two fighters from whatever's available on Amazon Bedrock, pick a topic, watch them go. It turned into AI Debate Battle, a Next.js app on Vercel backed by Amazon Bedrock AgentCore Runtime, and it taught me two things I didn't expect: list-foundation-models will happily list models you can't call, and getting an LLM to sound like a person arguing on a stage — instead of a student padding out a five-paragraph essay — needs a completely different kind of prompt than getting it to sound "helpful."
Gotcha #1: "listed" isn't "invokable"
Bedrock's list-foundation-models API in us-east-1 returned 59 text-generation, streaming-capable candidates across 14 providers — Anthropic, Amazon, Meta, Mistral, DeepSeek, Qwen, Z.AI, MiniMax, Moonshot AI, NVIDIA, Google, Writer, OpenAI's open-weight releases, TwelveLabs. That list is not the same thing as "models this account can actually call." I found that out by writing a script that calls Converse on every single candidate with a 5-token "hi" and records what comes back:
def probe(test_id):
r = subprocess.run(
["aws", "bedrock-runtime", "converse", "--region", "us-east-1",
"--model-id", test_id,
"--messages", '[{"role":"user","content":[{"text":"hi"}]}]',
"--inference-config", '{"maxTokens":5}'],
capture_output=True, text=True, timeout=60)
return test_id, r.returncode == 0
5 of the 59 came back AccessDeniedException or ValidationException — newer flagship variants that are listed but not yet provisioned for this account, and one multimodal-only model that doesn't actually implement the text Converse contract Bedrock's own catalog says it does. The other 54 responded cleanly and became the roster: everything from Claude Opus 4.6 and Claude Haiku 4.5 down to Llama 3 8B Instruct, DeepSeek V3.2, Qwen3 Coder Next, GLM 5, MiniMax M2.5, Nova Micro. For most single-model apps this doesn't bite you — you pick one model, test it, ship it. It bites you the moment you try to build anything that lets a user choose from a catalog: the catalog and the capability list are two different things, and the only source of truth is an actual Converse call.
Architecture: Strands on AgentCore, with a Bedrock fallback
The debate logic runs as Strands Agents deployed to Amazon Bedrock AgentCore Runtime, invoked from Next.js API routes on Vercel:
Two entrypoints in one Strands agent handle the whole app: action: "turn" streams one debater's argument token-by-token back over SSE, action: "judge" returns the scoring verdict as JSON. The prompt itself — persona, stance, round number, the opponent's last line — is built in TypeScript and passed into the agent as a payload, not hardcoded in the agent's own system prompt. That split matters: I iterated on the prompt (see the next section) a dozen times without ever redeploying the Runtime.
@app.entrypoint
async def invoke(payload):
if payload["action"] == "turn":
agent = Agent(
model=BedrockModel(model_id=payload["modelId"], region_name="us-east-1"),
system_prompt=payload["system"],
messages=payload.get("messages", []),
)
async for event in agent.stream_async(prompt):
if "data" in event:
yield event["data"]
Deployment used direct_code_deploy — no Dockerfile, no ECR push, just agentcore configure and agentcore deploy against the raw Python file. If the Runtime call fails for any reason (cold start, transient error, AGENTCORE_RUNTIME_ARN unset), the Next.js layer catches it and calls bedrock-runtime Converse directly instead — same prompt, no user-visible difference. Wiring a fallback path for the very code path you're demoing felt like overkill until I actually needed it once during testing, at which point it silently did its job and I only found out from the CloudWatch logs.
Gotcha #2: every model defaults to five-paragraph-essay mode
The first version of the debate prompt was reasonable-sounding: "argue this side, rebut the opponent's weakest point, be assertive." Every single model — Claude, Llama, Nova, Mistral, didn't matter — produced the same shape of answer: "X is superior. First, structural advantage. Second, practical benefit. Third, experiential quality. In conclusion, X is objectively better." Grammatically flawless, structurally identical, and it read like nobody was actually in the room.
The fix wasn't more instructions about tone — it was replacing "write an argument" with "you are a specific person, mid-sentence, on a stage." I gave each fighter a persona tied to its display name (hot-blooded, dry-and-surgical, contrarian-and-sarcastic, etc.) and banned the essay scaffolding outright:
- React to what {opponent} just said before anything else — quote or
paraphrase their weakest claim and go after it. Address them by name.
- Conceding a small point before flipping it is a power move
("fine, that part is true — and that's exactly why you lose").
- Vary your rhythm. A one-line jab that lands beats a paragraph.
- Banned: "firstly", "secondly", "in conclusion", bullet points,
both-sides hedging. Commit fully to your side.
Same models, same topic style, genuinely different output. Here's Claude Haiku 4.5, playing a persona called "Cold Analyst," arguing against giving AI agents payment authority — a real response pulled from the production API, not edited:
Right, so Blazing Debater wants us to believe a spending cap solves this. Let me ask you something — who sets the cap? A human, presumably. And what happens when an agent encounters a situation that's just slightly outside it? Does it wait? Then you've gained nothing. Does it spend anyway? Then your cap was theater.
But here's the real problem you're glossing over: speed isn't the point of automation — correct automation is. [...] An agent with payment authority is a system optimized for speed over judgment. And the moment it miscalculates, misreads a market signal, or gets fed bad data, it doesn't ask for forgiveness — it's already spent the money.
No "firstly." No hedging. It picks up the opponent's actual argument, restates it back at them, and drives a wedge into it. That's a prompt-engineering lesson that generalized well beyond debate apps for me: if you want an LLM to sound like a person instead of an essay generator, describe the person, not the essay.
The judge, scoring the same match
A third Strands agent (Claude Sonnet 4.6, fixed regardless of who's fighting) reads the full transcript and returns strict JSON — winner, three sub-scores per side out of 10 (logic, persuasion, rebuttal), a reasoning blurb, and a verbatim best-quote pull. Scoring the exchange above:
{
"winner": "con",
"scores": {
"pro": {"logic": 4, "persuasion": 5, "rebuttal": 0},
"con": {"logic": 8, "persuasion": 8, "rebuttal": 8}
},
"reasoning": "Pro opened with a breezy one-liner that never engaged
with risk, oversight, or failure modes... Con came out swinging with
the spending-cap paradox... That 'theater' line is where the match
turned.",
"bestQuote": "An agent with payment authority is a system optimized
for speed over judgment."
}
The judge's own instruction had to go through the same fix as the debaters: "write a formal evaluation" produced report language, "narrate the verdict like a ringside commentator naming the moment the match turned" produced the sentence above. Same underlying pattern — role beats register.
Stack it into a tournament (4 fighters, two semifinals, a final) and you get a full bracket run end to end with no human in the loop except picking the fighters and the topic — a mundane multi-agent orchestration problem I'd normally reach for a framework to solve, but the fan-out here is small and static enough that a plain for loop over Strands invocations did the job.
Takeaways
-
Probe, don't trust the catalog.
list-foundation-modelsandlist-inference-profilestell you what exists; only a realConversecall tells you what your account can invoke. 5 of 59 candidates failed silently until tested. - Keep the prompt out of the agent. Building the system prompt in the calling application and passing it as a payload to a generic AgentCore/Strands entrypoint meant dozens of prompt-engineering iterations with zero Runtime redeploys.
- A fallback path pays for itself the first time it's needed, even in a side project — wire AgentCore as primary and direct Bedrock Converse as the safety net from day one, not after the first outage.
- "Argue persuasively" is a worse instruction than "you are this specific person." Every model defaults to essay-shape output unless you give it a character and explicitly ban the scaffolding.
Full source — the model-probing script, the Strands/AgentCore agent, the Next.js app, and the persona prompts — is up on GitHub: yama3133/ai-debate-battle. The app itself is live at ai-debate-battle-six.vercel.app if you want to pick two models and watch them fight.

Top comments (0)