Chapter 83 · Agent loops and control
Reflection, and whether it is worth the tokens
Asking a model to check its own work sounds obviously good and mostly is not. Where it measurably helps, where it is a coin flip dressed as rigour, and the cheaper checks that beat it.
10 min read
Reflection — having the model review its own output and revise — is the most-recommended and least-measured technique in this area. This chapter measures it.
The summary: it helps on a narrow class of problem, it costs roughly double, and on most of what a retrieval assistant does it is a coin flip dressed as rigour.
Three things called reflection
| Form | What it does | Cost |
|---|---|---|
| Self-critique | "Review your answer and improve it" | +1 call |
| Verification | "Is each claim supported by a source?" | +1 call |
| Self-consistency | Sample n answers, take the majority | ×n calls |
They have very different properties and conflating them is why the technique has a mixed reputation.
Self-critique: mostly not worth it
// The obvious implementation.
const draft = await generate(question, chunks)
const critique = await callModel(prompt('critique', { question, draft: draft.text, sources }))
const final = await callModel(prompt('revise', { question, draft: draft.text, critique }))Atlas, 120 cases:
| Single pass | With self-critique | |
|---|---|---|
| Correct | 0.91 | 0.91 |
| Grounded | 0.98 | 0.98 |
| Citations valid | 0.99 | 0.97 |
| Correct abstention | 0.97 | 0.88 |
| Length | 48 words | 81 words |
| Cost | 1.0× | 2.9× |
Three of five metrics got worse. The two that fell are the interesting ones.
Abstention fell nine points. A model asked to improve a refusal finds something to say. "I could not find this" critiques poorly — it looks unhelpful — so revision turns it into a hedged partial answer from adjacent sources.
Citation validity fell. Revision rewrites sentences, and rewritten sentences lose their [n] markers or keep a marker whose claim has changed.
Self-critique optimises for how the answer reads, because that is what a critic can see. On a task where the virtues are groundedness and knowing when to decline, reading better is not the goal.
⚠️ This is a finding about this task. For open-ended generation where fluency and completeness are the goal, self-critique earns its cost — the measurement above does not transfer.
Verification: narrow, specific, and it works
The difference is the question asked. "Improve this" invites change; "is this claim in this source?" invites a verdict.
// src/generate/verify.ts — a yes/no per claim, not a rewrite.
export async function verifyClaims(answer: Answer, chunks: Retrieved[]) {
const claims = splitClaims(answer.text) // sentences carrying a citation
const verdicts = await Promise.all(claims.map(async (claim) => {
const source = chunks[claim.citation - 1]
const verdict = await callModel({
model: models.routing, // ⚠️ the cheap model is enough
messages: [{ role: 'user', content: ENTAILMENT_PROMPT
.replace('{{source}}', source.content).replace('{{claim}}', claim.text) }],
maxTokens: 5,
})
return { claim, verdict: verdict.text.trim().toLowerCase() }
}))
// ⚠️ Unsupported claims are REMOVED, not rewritten. A rewrite is
// self-critique with extra steps and brings back its failure modes.
const unsupported = verdicts.filter((v) => v.verdict === 'no')
return { unsupported, text: removeClaims(answer.text, unsupported) }
}| Single pass | With verification | |
|---|---|---|
| Correct | 0.91 | 0.91 |
| Grounded | 0.98 | 1.00 |
| Citations valid | 0.99 | 1.00 |
| Correct abstention | 0.97 | 0.97 |
| Cost | 1.0× | 1.3× |
Groundedness to 1.00 for 30% more, and nothing got worse — because removal cannot introduce an error, and the cheap model is sufficient for a yes/no against one passage.
Atlas runs this on every answer carrying a figure and samples it elsewhere, which is the chapter 62 sampling made cheap enough to be routine on the subset that matters.
Self-consistency: for planning, not for answers
// Sample several, take the majority. Expensive, and it works where the
// task has a discrete right answer.
const plans = await Promise.all([1, 2, 3].map(() =>
producePlan(question, { temperature: 0.7 })))
const chosen = majorityBy(plans, (p) => p.steps.map((s) => s.tool).join('>'))| Task | Single | n=3 majority | Cost |
|---|---|---|---|
| Final answer | 0.91 | 0.91 | 3× |
| Tool selection | 0.99 | 0.99 | 3× |
| Sequential plan | 0.86 | 0.93 | 3× |
| Ambiguity detection | 0.81 | 0.90 | 3× |
It buys nothing on the two things that are already reliable and seven to nine points on the two that are not. Applied to the 6% of traffic that plans, three times the cost of a plan call is a small absolute number for a large gain on the hardest cases.
// So: self-consistency on planning only, and only for the hard route.
const plan = route.complexity === 'high'
? majorityPlan(await samplePlans(question, 3))
: await producePlan(question)The cheaper checks that beat all three
Before paying for any reflection, the deterministic checks from earlier sections cover most of the same ground for free.
| Check | Catches | Cost | Chapter |
|---|---|---|---|
| Citation range | Fabricated references | 0 | 57 |
| Inline/declared agreement | Citation drift | 0 | 57 |
| Figure in cited source | Invented numbers | 0 | 57 |
Consistency: found vs citations | Ungrounded answers | 0 | 57 |
| Premise check | A plan past its point | 0 | 82 |
| Arithmetic computed in code | Wrong comparisons | 0 | 47 |
// Measured on Atlas: the free checks caught 71% of what a full
// self-critique pass would have, at zero cost and with no risk of
// rewriting a correct answer into a worse one.Spend the deterministic checks first. Reflection is for what cannot be checked mechanically, and most of what people reach for it to fix can be.
When reflection pays
Three conditions, and all three have to hold.
export function shouldReflect(run: Run, answer: Answer): boolean {
// 1. The output is hard to check deterministically.
if (deterministicChecksCover(answer)) return false
// 2. The cost of being wrong is high. Chapter 71's reversibility.
if (!run.involvesIrreversibleAction) return false
// 3. The task has a discrete right answer, so a majority means something.
if (!run.hasDiscreteAnswer) return false
return true
}For Atlas that is a narrow set: plans containing a write action. Those get self-consistency on the plan and verification on the final answer, and nothing gets self-critique.
Measuring it honestly
The trap is measuring reflection on the cases it was designed for.
// ⚠️ Run the FULL case set, not the hard subset. A technique that helps
// 7 hard cases and harms 12 easy ones is a net loss, and measuring only
// the 7 reports a win.
const full = await scoreEndToEnd(allCases, { reflection: 'self_critique' })
const hard = await scoreEndToEnd(hardCases, { reflection: 'self_critique' })
console.log({ full: full.correct, hard: hard.correct })
// Atlas: hard 0.86 -> 0.89 (+0.03), full 0.91 -> 0.91 (0.00)
// The gain on hard cases was exactly offset by losses elsewhere.That measurement is the one that settled the question on Atlas, and it would have reported a success if only the hard subset had been run.
The failure mode: the critique that agreed with itself
Atlas's first verification implementation used the same model, in the same conversation, to check its own claims.
Agreement with human raters: 0.62. Barely better than chance on a binary task, and biased in one direction — the model confirmed its own claims 94% of the time, including claims that were not in the source.
// ❌ Same model, same conversation. It is not verifying; it is
// continuing, and the continuation of "here is my answer" is
// agreement.
messages.push({ role: 'user', content: 'Is each claim supported?' })
const verdict = await callModel({ messages })
// ✅ A fresh context, a different model, and only the source and the
// claim — no draft, no reasoning, nothing to be consistent with.
const verdict = await callModel({
model: models.verifier, // ⚠️ a different family
messages: [{ role: 'user', content: ENTAILMENT_PROMPT
.replace('{{source}}', source).replace('{{claim}}', claim) }],
})Agreement with human raters after the change: 0.91.
Two things did the work: a different model family, and removing everything except the source and the claim. The second mattered as much as the first — the same model, given only the pair with no draft in context, agreed with humans at 0.87.
// And the calibration that must happen before trusting any of it,
// per chapter 106.
// 40 claims, two human raters, compared to the verifier.
// ⚠️ Below ~0.85 agreement the verifier is adding noise. Do not ship a
// reflection step whose judge has not been calibrated.A model asked to check its own work in its own context will agree with itself. Reflection needs a fresh context and preferably a different model, or it is a confidence-boosting ritual with a token cost.
Key points
- Three different things are called reflection and they behave differently.
- Self-critique made three of five Atlas metrics worse: abstention fell nine points and citation validity fell, because revision rewrites refusals into hedged answers.
- Self-critique optimises for how an answer reads, which is not the goal on a grounded-answer task.
- Verification works: groundedness to 1.00 for 30% more cost, because removal cannot introduce an error and the cheap model suffices.
- Self-consistency buys nothing on reliable tasks and seven to nine points on planning and ambiguity detection. Apply it to planning only.
- The free deterministic checks caught 71% of what self-critique would have, at zero cost.
- Reflection pays only when the output is hard to check mechanically, the cost of being wrong is high, and the answer is discrete.
- Measure on the full case set. A gain on hard cases exactly offset by losses elsewhere reports as a win on the subset.
- A model checking its own work in its own context agrees with itself: 0.62 against humans, rising to 0.91 with a fresh context and a different model.