TL;DR
LLM self-correction is useful when feedback identifies a real defect and a later check confirms its repair. A model saying “I fixed it” is not that check. Separate candidate generation from acceptance, retain assessed versions, reject regressions, and measure how often corrections repair errors versus damage correct answers.
Contents
- What self-correction does and does not establish
- What the research supports
- Design feedback that can change the outcome
- A revision loop with regression checks in Go
- Measure repairs and regressions separately
- Control cost and operational risk
- FAQ
- Sources and further reading
What self-correction does and does not establish
Self-correction is a feedback-and-revision process applied to an earlier model answer. The revision may improve the answer, leave it unchanged, or make it worse. Calling the process “correction” does not establish its outcome.
In an application, separate four jobs:
- Detect: establish that a requirement failed.
- Locate: identify the relevant field, claim, or behavior.
- Repair: propose a change that addresses the failure.
- Accept: check the changed artifact against the full contract.
A JSON parser can detect malformed output but cannot establish whether an invoice amount matches source records. A failed test can locate incorrect behavior but may not identify its cause. An eloquent critique may provide neither. The application should preserve these distinctions instead of compressing everything into a model-generated confidence number.
Reasoning models may learn behaviors resembling reconsideration within a trajectory. Applications can also implement revision around ordinary LLMs. Neither requires exposing a full chain of thought to the user. Evaluate observable outputs and check results.
Selection is a different operation. Best-of-N chooses among complete candidates; beam search expands and prunes partial candidates; sequential revision edits an existing candidate. Generating several answers and ranking them is not automatically self-correction or beam search.
What the research supports
Research supports several feedback mechanisms and also documents important failures. It does not establish a universal improvement per revision round or a model ranking attributable solely to self-correction.
| Work | Mechanism | What to take into engineering |
|---|---|---|
| Self-Refine | A model generates, critiques, and refines its own output without extra weight training | A feedback loop can be built around an existing model |
| Reflexion | Linguistic feedback stored in episodic agent memory informs later trials | Persist useful failure evidence rather than blindly repeating attempts |
| CRITIC | Tool interaction supplies feedback for output revision | External evidence can help challenge model errors |
| Large Language Models Cannot Self-Correct Reasoning Yet | Studies intrinsic correction without external feedback | Some tested settings fail to improve and can degrade |
Self-Refine's reported aggregate improvement across its tasks is not a promise that every iteration helps. Reflexion permits both external and internally simulated feedback; defining it as “external feedback only” misses part of the method. Its benchmark result describes the paper's agent protocol, not a raw single-call score transferable to any wrapper.
The intrinsic-correction study concerns the models and settings it tested. It is evidence against assuming reliable self-correction, not proof that all future systems are incapable of it.
The DeepSeek-R1 report distinguishes R1-Zero, trained with reinforcement learning without a preliminary supervised fine-tuning stage, from the full R1 training pipeline with cold-start data and multiple stages. It is therefore inaccurate to describe every R1 capability as the result of the same “pure RL” setup. Visible reasoning text also does not prove a particular verifier architecture or explain how much of a benchmark score comes from correction.
For API-facing systems, the OpenAI reasoning guide describes reasoning controls and token accounting. It does not provide a basis for inventing hidden tree structures or per-round correction behavior. Keep application mechanisms explicit and treat undisclosed internals as unknown.
Design feedback that can change the outcome
Feedback is actionable when it states the failed requirement, observed evidence, and what must remain true. “Be more accurate” is not a useful repair instruction.
For an invoice summary, a useful report is: “The source has subtotal 10,000 and tax 800 minor units; total 10,700 does not equal 10,800. Currency must remain CNY.” The source values belong to the task contract. Letting a model change them to make its own total pass would invalidate the check.
| Task | Useful feedback | Inadequate substitute |
|---|---|---|
| Structured extraction | Source span and field mismatch | Schema pass alone |
| Code generation | Failing input, expected behavior, actual behavior | “There might be bugs” |
| Grounded answer | Unsupported claim and relevant source passage | A fluent alternative answer |
| Planning | Violated capacity or authorization constraint | A higher self-reported confidence |
Separate hard constraints from preferences. A correct currency, authorized destination, or required field should not be traded away for a better style score. A model judge may assist with readability, but it should not override failed mandatory checks.
Store feedback with the candidate version, task version, verifier version, and source snapshot. Reusing a check after any of those change can create false confidence. A text diff helps inspect changes; change size is not a correctness metric. One changed minus sign can break an answer, while a complete rewrite can repair it.
A revision loop with regression checks in Go
This Go 1.23 example implements a conservative acceptance policy for an invoice summary. It retains the best assessed candidate, refuses to lose a previously passing check, and verifies every revision before use. The scripted repair function demonstrates the controller; it does not invoke or benchmark a model.
The source subtotal, tax, and currency are trusted inputs. The generated artifact contains only the proposed total and currency. The verifier uses integer minor units, avoiding floating-point rounding and refusing invalid or overflowing source amounts.
Save as main.go and run go run main.go.
package main
import (
"context"
"fmt"
"math"
"time"
)
type Source struct {
Currency string
Subtotal int64
Tax int64
}
type Draft struct {
Currency string
Total int64
}
type Check struct {
Name string
Pass bool
}
type Outcome struct {
Draft Draft
Checks []Check
Accepted bool
Revisions int
Rejected int
Stop string
}
type Repair func(context.Context, Draft, []string) (Draft, error)
func assess(ctx context.Context, source Source, draft Draft) ([]Check, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
if source.Currency == "" || source.Subtotal < 0 || source.Tax < 0 ||
source.Subtotal > math.MaxInt64-source.Tax {
return nil, fmt.Errorf("invalid source")
}
return []Check{
{"currency_matches_source", draft.Currency == source.Currency},
{"total_nonnegative", draft.Total >= 0},
{"total_matches_source", draft.Total == source.Subtotal+source.Tax},
}, nil
}
func failures(checks []Check) []string {
var names []string
for _, check := range checks {
if !check.Pass {
names = append(names, check.Name)
}
}
return names
}
func improves(previous, next []Check) bool {
if len(previous) != len(next) {
return false
}
gained := false
for i, check := range previous {
if check.Name != next[i].Name || check.Pass && !next[i].Pass {
return false
}
gained = gained || !check.Pass && next[i].Pass
}
return gained
}
func revise(ctx context.Context, source Source, initial Draft, limit int, repair Repair) (Outcome, error) {
result := Outcome{Draft: initial}
if limit < 0 || repair == nil {
return result, fmt.Errorf("invalid configuration")
}
checks, err := assess(ctx, source, initial)
if err != nil {
return result, err
}
result.Checks = checks
seen := map[Draft]bool{initial: true}
for {
if err := ctx.Err(); err != nil {
return result, err
}
failed := failures(result.Checks)
if len(failed) == 0 {
result.Accepted, result.Stop = true, "checks_passed"
return result, nil
}
if result.Revisions == limit {
result.Stop = "revision_limit"
return result, nil
}
result.Revisions++
next, err := repair(ctx, result.Draft, failed)
if err != nil {
return result, fmt.Errorf("repair failed: %w", err)
}
nextChecks, err := assess(ctx, source, next)
if err != nil {
return result, err
}
if seen[next] {
result.Stop = "repeated_candidate"
return result, nil
}
seen[next] = true
if !improves(result.Checks, nextChecks) {
result.Rejected++
continue
}
result.Draft, result.Checks = next, nextChecks
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
source := Source{"CNY", 10000, 800}
proposals := []Draft{{"USD", 10800}, {"CNY", 10800}}
index := 0
repair := func(ctx context.Context, current Draft, failed []string) (Draft, error) {
if err := ctx.Err(); err != nil {
return Draft{}, err
}
if len(failed) == 0 || index >= len(proposals) {
return Draft{}, fmt.Errorf("no scripted repair")
}
next := proposals[index]
index++
return next, nil
}
result, err := revise(ctx, source, Draft{"CNY", 10700}, 2, repair)
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Printf("accepted=%t revisions=%d rejected=%d currency=%s total=%d stop=%s\n",
result.Accepted, result.Revisions, result.Rejected,
result.Draft.Currency, result.Draft.Total, result.Stop)
}
Expected output:
accepted=true revisions=2 rejected=1 currency=CNY total=10800 stop=checks_passed
The first proposal fixes the total but changes the currency to USD. It is rejected because a previously passing requirement regresses. The second proposal passes all checks and is accepted after assessment, even though it is the last permitted revision.
With a revision limit of one, the result remains CNY 10,700 with Accepted=false and Stop=revision_limit. It is the retained assessed draft, not an approved invoice. With a limit of zero, the initial candidate is still checked. Repeated candidates stop the loop; repair failures return an error and leave the retained candidate's evidence intact.
This deliberately strict policy can reject intermediate steps needed for a larger rewrite. If the task requires such exploration, keep a separate branch of experimental candidates while retaining the last acceptable artifact. Do not weaken mandatory checks merely to let a search continue.
In a live integration, replace the scripted function with a model call that receives the fixed task facts and concrete failed checks. Parse its output into a Draft, validate completion status, and reserve generation and verification budgets. A callback must honor its context: cancellation in a Go controller cannot forcibly stop an arbitrary blocking function.
Measure repairs and regressions separately
The essential metric is the change in correctness across the same tasks, with evaluation labels independent of the repair loop.
For a fixed set of N tasks:
- Repair count: initially wrong, finally right.
- Regression count: initially right, finally wrong.
- Net accuracy change:
(repair count - regression count) / N. - Repair rate: repair count divided by initially wrong tasks.
- Regression rate: regression count divided by initially right tasks.
For illustration, suppose 100 answers initially contain 60 correct answers. Revision repairs 12 of the 40 errors but breaks 5 of the 60 correct answers. Final accuracy is 67%, the gain is 7 percentage points, the repair rate is 30%, and the regression rate is about 8.3%. These are arithmetic examples, not measured model results.
Reporting only the 12 repaired answers would hide the five regressions. Reporting only final accuracy would hide why the loop improved or failed.
Use a comparison that can distinguish useful feedback from extra computation:
| Baseline or variant | What it tests |
|---|---|
| Initial answer only | Starting quality |
| Fresh retry with comparable budget | Value of another attempt |
| Revision without specific evidence | Effect of generic self-critique |
| Revision with concrete feedback | Added value of error evidence |
| Full acceptance controller | Ability to keep improvements and reject damage |
Log final-answer quality, coverage, false acceptance, false rejection, latency, and total cost. If the system abstains, report that separately rather than silently excluding those tasks. For each iteration, retain candidate IDs and evidence so that a final result can be attributed to its actual check.
Keep development tests used for repair separate from held-out evaluation tests. Repeatedly adapting a candidate to the same test suite can overfit that suite. Changing the judge model or temperature can diversify judgments but does not establish statistical independence or remove shared blind spots.
Control cost and operational risk
A bounded correction loop needs both resource limits and limits on what feedback is allowed to authorize. More iterations create more chances to repair and more chances to fail.
Budget the initial generation, critiques, revisions, tool executions, and final verification together. Reserve verification capacity before generating the last revision; otherwise the controller may run out of resources precisely when it needs to check what it intends to return. The test-time compute guide covers shared reservations and usage reconciliation.
For AI Agents, separate artifact revision from external side effects. Rewriting a draft is different from submitting an order twice. If an execution timed out after a possible commit, reconcile its outcome before retrying it.
Tool output and retrieved text are evidence, not authority to broaden permissions. A failing test cannot authorize reading secrets. Generated code needs an isolated execution environment; launching a subprocess with a timeout does not create a sandbox.
Reflexion-style memory also needs scope. Store a specific failure, the evidence behind it, and the circumstances where the lesson applies. An incorrect reflection copied across tasks can make failures persistent. Use task or tenant boundaries, expiration rules where appropriate, and a way to invalidate lessons after the verifier or source changes.
FAQ
What is LLM self-correction?
Self-correction is a process in which a model revises an earlier answer using feedback about possible errors. It can occur inside a reasoning trajectory or through application-level generation, checking, and revision. Successful correction requires evidence that the revised answer is better.
How do Self-Refine and Reflexion differ?
Self-Refine uses a model to generate, critique, and refine an output without additional weight training. Reflexion stores linguistic feedback in episodic memory to inform later attempts; that feedback may be external or internally simulated.
Can self-correction make an answer worse?
Yes. The model can accept misleading feedback or replace a correct answer with an incorrect one. Measure wrong-to-right repairs and right-to-wrong regressions separately, and recheck each candidate before accepting it.
Is Best-of-N the same as beam search or revision?
No. Best-of-N scores complete candidates, beam search repeatedly expands and prunes partial candidates, and revision changes an existing candidate using feedback. A system can combine them, but their costs and failure modes differ.
What should a correction loop return when its budget ends?
Return an explicit unresolved status with the best assessed candidate and its actual check results. Never attach an earlier candidate's verification to a newly generated revision, and never silently label a failed candidate as accepted.
Sources and further reading
- Self-Refine — iterative generation, self-feedback, and refinement.
- Reflexion — linguistic feedback and episodic memory.
- CRITIC — tool-interactive feedback for self-correction.
- Large Language Models Cannot Self-Correct Reasoning Yet — limitations of intrinsic correction in the tested settings.
- DeepSeek-R1 report — R1-Zero and R1 training distinctions.
- OpenAI reasoning guide — API-facing controls and accounting boundaries.