Debate and peer review
The com.tnsai.coordination.debate package has two executors that improve an answer by setting LLMs against each other. DebateExecutor has a proponent and an opponent argue a topic for a few rounds, then asks a judge for a verdict. PeerReviewExecutor has a producer write a draft and a reviewer approve it or send it back for revision.
Both take LLMClient instances, not agents, and call chat(prompt) with fixed prompts. They are not related to the @Debate messaging protocol in Protocols or the DEBATE group strategy in Group topologies.
Debate
execute(topic) runs the rounds and then the judge:
- In each round the proponent argues in favor and the opponent argues against. In the first round the proponent opens and the opponent answers it. In later rounds the proponent sees the opponent's last argument, and the opponent sees the proponent's newest argument and its own previous one.
- If a side's call throws, its argument for that round is the text
Error: Unable to generate argument.(orcounter-argument.for the opponent), and the debate goes on. - After the last round the judge gets the whole transcript and is asked for five numbered sections: VERDICT, REASONING, PRO SUMMARY, CON SUMMARY and CONFIDENCE.
| Builder method | Default | Meaning |
|---|---|---|
proponent(client) | required | Argues in favor |
opponent(client) | required | Argues against |
judge(client) | required | Reads the transcript and decides |
maxRounds(n) | 3 | Number of rounds; must be at least 1 |
timeout(duration) | 10 minutes | Checked before each round |
format(format) | STRUCTURED | STRUCTURED or FREE_FORM; stored but not used, so both produce the same prompts |
The timeout only stops new rounds from starting. A round that has started finishes, and the judge always runs.
package com.example.tnsai.docs;
import com.tnsai.coordination.debate.DebateExecutor;
import com.tnsai.coordination.debate.DebateResult;
import com.tnsai.coordination.debate.DebateRound;
import com.tnsai.coordination.debate.PeerReviewExecutor;
import com.tnsai.coordination.debate.PeerReviewExecutor.PeerReviewIteration;
import com.tnsai.coordination.debate.PeerReviewExecutor.PeerReviewResult;
import com.tnsai.llm.LLMClient;
import java.time.Duration;
/** Compilation contract for multi-agent/debate.md. */
public final class DebateExample {
private DebateExample() {
}
static DebateResult debate(LLMClient pro, LLMClient con, LLMClient judge) {
DebateExecutor debate = DebateExecutor.builder()
.proponent(pro)
.opponent(con)
.judge(judge)
.maxRounds(3)
.timeout(Duration.ofMinutes(5)) // checked before each round
.build();
DebateResult result = debate.execute("Should this service move to microservices?");
for (DebateRound round : result.rounds()) {
System.out.println("Round " + round.roundNumber());
System.out.println(" Pro: " + round.proponentArgument());
System.out.println(" Con: " + round.opponentArgument());
}
System.out.println("Verdict: " + result.verdict());
System.out.println("Confidence: " + result.confidence()
+ (result.isHighConfidence() ? " (high)" : ""));
return result;
}
static String review(LLMClient writer, LLMClient editor) {
PeerReviewResult result = PeerReviewExecutor.builder()
.producer(writer)
.reviewer(editor)
.maxIterations(3)
.build()
.execute("Write release notes for version 2.0");
for (PeerReviewIteration iteration : result.iterations()) {
System.out.println(iteration.iterationNumber() + ": approved=" + iteration.approved());
}
// Without approval, finalOutput is the last draft, which was not approved.
return result.approved() ? result.finalOutput() : null;
}
}The result
DebateResult is a record:
| Field | Meaning |
|---|---|
verdict | The judge's VERDICT section, or the whole judge answer if that section is missing |
confidence | 0–1, from the first "Confidence: NN%" in the judge answer; 0.5 when none is found |
rounds | One DebateRound per round: roundNumber, proponentArgument, opponentArgument, duration |
roundsUsed | Rounds that ran, fewer than maxRounds after a timeout |
proponentSummary, opponentSummary | The judge's PRO SUMMARY and CON SUMMARY sections; empty if missing |
duration | Total time |
isHighConfidence() is true when confidence is at least 0.7. If the judge's call throws, the verdict starts with Unable to reach verdict due to error:, the confidence is 0 and the summaries are empty.
Peer review
execute(task) loops for up to maxIterations iterations:
- The producer writes a draft. In the first iteration it gets the task. In later iterations it gets its previous draft and the reviewer's feedback, but not the task text.
- The reviewer gets the task and the draft and is asked for APPROVED (yes or no), FEEDBACK and STRENGTHS.
- The draft counts as approved when the reviewer's answer contains
approved: yesorapproved:yes, ignoring case. Any other wording, such asApproved: **yes**, counts as a rejection. - An approved draft ends the loop. Otherwise the feedback goes into the next iteration.
| Builder method | Default | Meaning |
|---|---|---|
producer(client) | required | Writes and revises the draft |
reviewer(client) | required | Approves or gives feedback |
maxIterations(n) | 3 | Drafts to try; must be at least 1 |
timeout(duration) | 10 minutes | Checked before each iteration |
If the reviewer's call throws, the draft is not approved and the next revision gets Review unavailable: … as its feedback. If the producer's call throws, the loop stops.
PeerReviewResult has finalOutput, approved, iterations, iterationsUsed and duration. Each PeerReviewIteration has iterationNumber, output, feedback, approved and duration. When approved is false, finalOutput is the last draft, which was not approved, or an empty string if the producer never answered. Check approved before you use it.