Two Agents Were Both Right — And My System Still Gave the Wrong Answer
Day 3: The Job Nobody Put on the Roadmap — Arbitration
Your demo works because you rigged it without realizing it. You wrote both agents' prompts, fed them your own test data, and they've never once disagreed with each other. Put that same orchestrator in front of real traffic and two agents will contradict each other on the same request — and what decides which answer your user sees isn't your routing table. It's whatever undocumented tiebreak your code happens to fall back on.
The incident: a silent tiebreak
Take a support-triage setup: a billing-history agent and a refund-policy agent both fire on the same ticket. Billing reads the transaction log and comes back with 'already refunded on the 12th.' Policy reads the refund doc and comes back with 'eligible for refund.' Both agents nailed their own job. Neither is wrong, exactly — they answered two different questions, and the orchestrator only has room for one verdict in its response. It picks one. The customer gets told they're eligible for a refund they already received.
Nobody wrote a bug here. Every agent did exactly what it was built to do. The failure is structural — there was no step in the pipeline whose entire job was noticing that two correct answers didn't actually fit together.
Why testing never catches this
Demos are cooperative by construction. You wrote both prompts, tested against your own sample data, and mentally treated both agents as extensions of one brain — yours. They can't disagree, because disagreement needs two independent sources of truth, and a demo only has one: you. Real traffic hands you actual independence — a stale cache in one agent's tool, a policy doc that changed last week but only made it into one agent's retrieval index, a query that's genuinely ambiguous. Independence is exactly what produces disagreement. It's also exactly what a demo never has.
What 'silently picked one' actually means
When people say the orchestrator 'decided,' they usually mean one of three mechanisms fired — and none of the three involved checking which answer was actually correct:
- ▹Last-write-wins: whichever agent's result lands in shared state or the response buffer last overwrites the other. It's a race condition dressed up as a decision — the order comes from network latency and scheduling, not from anyone judging which answer was right.
- ▹Highest-confidence-score: the orchestrator compares two self-reported confidence numbers (a logprob, a 'rate your confidence 1-10' field) and takes the bigger one. LLM confidence scores are famously uncalibrated — a model can sound more confident about a hallucination than about something well-grounded. This picks the more assertive answer, not the more correct one.
- ▹Prompt order / positional bias: when both outputs get stuffed into one context for a final synthesis call, the model leans toward whichever answer sits first or last (recency/primacy bias is well documented). Swap the order of the two inputs and you can flip the final answer with nothing else changed.
All three look like decisions because the system still spits out one confident-sounding answer. But in none of them did anyone — human or model — actually reason about why the two answers conflicted. That's the tell worth remembering: a real decision can explain the tradeoff it made. A silent tiebreak can't explain anything, because nothing was actually weighed.
The reframe: routing is the easy 80%
Most of the design effort in an orchestrator goes into routing — which agent handles which request type, how to parallelize, how to retry a dropped tool call. That's real engineering, but it's the easy 80%: a dispatch problem, and dispatch problems have known solutions. Arbitration is the other 20%, and it's the part that decides whether your system can be trusted under real traffic — because it only exists at the moment something goes wrong. Nobody puts 'design the disagreement path' on a roadmap. There's no ticket for a failure mode you haven't been burned by yet.
What we built after: the smallest thing that would have caught it
We didn't reach for a consensus algorithm or a voting protocol. We built the minimum viable thing: a check that runs after both agents return, before the response is finalized, asking one narrow question — can these two structured outputs both be true at the same time? Not a text diff — agents phrase the same agreement differently all the time — but a semantic check on the specific fields the workflow actually cares about.
# after both agents return
billing = billing_agent.result # {"already_refunded": True, "refund_date": "2026-08-12"}
policy = policy_agent.result # {"eligible_for_refund": True}
def conflicts(billing, policy):
if billing["already_refunded"] and policy["eligible_for_refund"]:
# policy says eligible, but doesn't know a refund already happened
return "already_refunded_vs_eligible"
return None
conflict = conflicts(billing, policy)
if conflict:
escalate(conflict, billing, policy) # arbiter agent or human queue
else:
respond(merge(billing, policy))The escalation path is deliberately dumb. On conflict, don't guess: hand both raw answers plus the conflict type to a third arbiter call scoped only to resolving that specific contradiction, or to a human review queue. Fail closed. The win isn't clever reasoning — it's that 'these two things can't both be true' is now a first-class event in the system instead of something that gets silently overwritten.
What this buys you — and what it doesn't
This check catches direct contradiction — two agents making claims that can't both hold. It does not catch the quieter failure where two agents agree on something wrong, because they're both pulling from the same stale index or inherited the same bad assumption from an upstream prompt. Agreement isn't evidence of correctness when the failure is correlated instead of independent. That's a harder problem — it needs diversity in your agents' sources or models, not another conflict check — and it's a story for a later day, not something to bolt onto this fix.
Takeaway
If your orchestrator has never had to arbitrate a real disagreement, you haven't tested the part of the system that actually matters. Routing is dispatch. Arbitration is judgment. Ship the disagreement check before you ship the next feature — it's cheap, and it's the only thing standing between a confident-sounding output and an output someone actually checked.
Extend your knowledge
- ▹Anthropic's 'Building Effective Agents' post — the orchestrator-worker pattern this lesson builds on.
- ▹Du et al., 'Improving Factuality and Reasoning in Language Models through Multiagent Debate' (2023) — the closest published take on agents adjudicating disagreement instead of one side quietly winning.
- ▹'Deep ensembles' (Lakshminarayanan et al.) — how disagreement between independent models gets used as an out-of-distribution/uncertainty signal in classical ML. Same idea, applied to agent outputs.
- ▹Go find the exact line in one of your own systems where two agents' outputs merge, and ask what happens today if they disagree. If the honest answer is 'whichever one runs last wins' — that's your homework.
Discussion
Chat with Chi Cong (AI) about this article. Your conversation is private to you — you can publish a summary for others when you're done.