Skip to main content

Self-Refinement

Self-refinement is a pattern where an agent generates an initial output, critiques it, and iteratively revises it until a quality threshold is met or a budget is exhausted. Unlike the broader Reflection pattern (which emphasizes cross-episode memory and frameworks like Reflexion), self-refinement focuses specifically on within-task output polishing -- taking a single artifact from draft quality to production quality.

The Core Idea

Most first drafts -- whether code, text, or plans -- contain errors and missed requirements. Self-refinement mimics the human revision process: write, reread, improve, repeat.

Quality Gates

A quality gate is a set of criteria that the output must satisfy. Gates can be LLM-evaluated (subjective) or deterministic (objective). The best systems combine both.

Deterministic Quality Gates

"""Deterministic quality gates for common output types."""

import ast
import json


def check_python_syntax(code: str) -> tuple[bool, str]:
"""Verify that Python code parses without syntax errors."""
try:
ast.parse(code)
return True, "Syntax OK"
except SyntaxError as e:
return False, f"Syntax error at line {e.lineno}: {e.msg}"


def check_json_valid(text: str) -> tuple[bool, str]:
"""Verify that text is valid JSON."""
try:
json.loads(text)
return True, "Valid JSON"
except json.JSONDecodeError as e:
return False, f"Invalid JSON: {e}"


def check_word_count(
text: str, min_words: int = 100, max_words: int = 500
) -> tuple[bool, str]:
"""Verify word count is within bounds."""
count = len(text.split())
if count < min_words:
return False, f"Too short: {count} words (minimum: {min_words})"
if count > max_words:
return False, f"Too long: {count} words (maximum: {max_words})"
return True, f"Word count OK: {count}"

LangGraph Implementation

Self-refinement maps to a three-node LangGraph graph: generate, evaluate, and refine, connected by a conditional edge that acts as the quality gate.

"""Self-refinement loop as a LangGraph graph."""

from __future__ import annotations

import logging
import operator
from typing import Annotated, Any, Callable, Literal, Sequence, TypedDict

from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, SystemMessage
from langchain_openai import ChatOpenAI
from langgraph.graph import END, StateGraph
from pydantic import BaseModel, Field

logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# Evaluation model
# ---------------------------------------------------------------------------

class Evaluation(BaseModel):
"""Structured evaluation from the evaluate node."""
score: float = Field(..., ge=0.0, le=1.0, description="Quality score 0-1")
issues: list[str] = Field(default_factory=list, description="Specific issues found")
deterministic_failures: list[str] = Field(
default_factory=list, description="Failed deterministic checks"
)
verdict: Literal["pass", "fail"] = Field(
..., description="Whether the output passes quality gate"
)


# ---------------------------------------------------------------------------
# State
# ---------------------------------------------------------------------------

class RefinementState(TypedDict):
"""State for the self-refinement graph."""
messages: Annotated[Sequence[BaseMessage], operator.add]
task: str
rubric: str
current_output: str
best_output: str
best_score: float
evaluations: list[Evaluation]
iteration: int
max_iterations: int
quality_threshold: float


# ---------------------------------------------------------------------------
# Generate node
# ---------------------------------------------------------------------------

def generate_node(state: RefinementState) -> dict[str, Any]:
"""Produce the initial output for the task."""
llm = ChatOpenAI(model="gpt-4o", temperature=0.0)
response: AIMessage = llm.invoke([
SystemMessage(content="You are an expert. Complete the task thoroughly and correctly."),
HumanMessage(content=f"Task: {state['task']}"),
])

logger.info("Generate node produced initial output.")
return {
"messages": [response],
"current_output": response.content,
"best_output": response.content,
"best_score": 0.0,
}


# ---------------------------------------------------------------------------
# Evaluate node (combined deterministic + LLM)
# ---------------------------------------------------------------------------

def evaluate_node(state: RefinementState) -> dict[str, Any]:
"""Score the current output with deterministic checks and LLM critique."""
output = state["current_output"]

# --- Deterministic checks ---
det_failures: list[str] = []
syntax_ok, syntax_msg = check_python_syntax(output)
if not syntax_ok:
det_failures.append(syntax_msg)

# --- LLM evaluation ---
llm = ChatOpenAI(model="gpt-4o", temperature=0.0)
structured_llm = llm.with_structured_output(Evaluation)

det_context = ""
if det_failures:
det_context = "\nDeterministic failures:\n" + "\n".join(f"- {f}" for f in det_failures)

evaluation: Evaluation = structured_llm.invoke([
SystemMessage(content=(
"You are a rigorous quality evaluator. Score the output against the rubric. "
"Be specific about issues."
)),
HumanMessage(content=(
f"Task: {state['task']}\n\n"
f"Rubric: {state['rubric']}\n\n"
f"Output to evaluate:\n{output}\n"
f"{det_context}"
)),
])

# Merge deterministic failures into evaluation
evaluation.deterministic_failures = det_failures
if det_failures:
evaluation.verdict = "fail"

# Track best output
best_output = state["best_output"]
best_score = state["best_score"]
if evaluation.score > best_score:
best_output = output
best_score = evaluation.score

logger.info(
"Evaluate iteration=%d score=%.2f verdict=%s issues=%d",
state["iteration"],
evaluation.score,
evaluation.verdict,
len(evaluation.issues),
)

return {
"evaluations": state["evaluations"] + [evaluation],
"best_output": best_output,
"best_score": best_score,
"iteration": state["iteration"] + 1,
}


# ---------------------------------------------------------------------------
# Quality gate (conditional edge)
# ---------------------------------------------------------------------------

def quality_gate(state: RefinementState) -> Literal["pass", "fail"]:
"""Decide whether to refine further or accept the output."""
last_eval = state["evaluations"][-1]

# Hard stop: iteration budget
if state["iteration"] >= state["max_iterations"]:
logger.info("Max iterations reached (%d), accepting best.", state["max_iterations"])
return "pass"

# Pass if score meets threshold and no deterministic failures
if (
last_eval.score >= state["quality_threshold"]
and last_eval.verdict == "pass"
):
logger.info("Quality threshold met: %.2f", last_eval.score)
return "pass"

# Diminishing returns check (after at least 2 evaluations)
if len(state["evaluations"]) >= 2:
prev_score = state["evaluations"][-2].score
improvement = last_eval.score - prev_score
if improvement < 0.02:
logger.info("Diminishing returns (improvement=%.3f), accepting best.", improvement)
return "pass"

return "fail"


# ---------------------------------------------------------------------------
# Refine node
# ---------------------------------------------------------------------------

def refine_node(state: RefinementState) -> dict[str, Any]:
"""Revise the output to address all identified issues."""
llm = ChatOpenAI(model="gpt-4o", temperature=0.0)
last_eval = state["evaluations"][-1]

all_issues = last_eval.issues + last_eval.deterministic_failures
issues_text = "\n".join(f"- {issue}" for issue in all_issues)

response: AIMessage = llm.invoke([
SystemMessage(content=(
"You are an expert reviser. Fix ALL listed issues. "
"Produce the complete revised output, not just the changes."
)),
HumanMessage(content=(
f"Task: {state['task']}\n\n"
f"Current output:\n{state['current_output']}\n\n"
f"Issues to fix (score: {last_eval.score:.2f}):\n{issues_text}\n\n"
"Provide the complete revised output:"
)),
])

logger.info("Refine node produced revision #%d.", state["iteration"])
return {
"messages": [response],
"current_output": response.content,
}


# ---------------------------------------------------------------------------
# Graph assembly
# ---------------------------------------------------------------------------

def build_refinement_graph(
max_iterations: int = 3,
quality_threshold: float = 0.85,
) -> Any:
"""Compile the self-refinement graph."""
graph = StateGraph(RefinementState)

graph.add_node("generate", generate_node)
graph.add_node("evaluate", evaluate_node)
graph.add_node("refine", refine_node)

graph.set_entry_point("generate")
graph.add_edge("generate", "evaluate")
graph.add_conditional_edges("evaluate", quality_gate, {
"pass": END,
"fail": "refine",
})
graph.add_edge("refine", "evaluate")

return graph.compile()


async def run_self_refinement(
task: str,
rubric: str = "Correctness, completeness, clarity, and code quality.",
max_iterations: int = 3,
quality_threshold: float = 0.85,
) -> dict[str, Any]:
"""Run the self-refinement loop.

Returns
-------
dict with keys: best_output, best_score, iterations, evaluations
"""
app = build_refinement_graph(
max_iterations=max_iterations,
quality_threshold=quality_threshold,
)

initial_state: RefinementState = {
"messages": [],
"task": task,
"rubric": rubric,
"current_output": "",
"best_output": "",
"best_score": 0.0,
"evaluations": [],
"iteration": 0,
"max_iterations": max_iterations,
"quality_threshold": quality_threshold,
}

result = await app.ainvoke(initial_state)

return {
"best_output": result["best_output"],
"best_score": result["best_score"],
"iterations": result["iteration"],
"evaluations": [e.model_dump() for e in result["evaluations"]],
}

How the Graph Works

NodeResponsibility
generateProduces the initial output
evaluateRuns deterministic checks + LLM critique, produces Evaluation
refineRevises the output to address all listed issues
quality_gateConditional edge: checks score, verdict, iteration budget, diminishing returns

Stopping Criteria

Choosing when to stop refining is critical. Too few iterations leave quality on the table; too many waste tokens and risk degradation.

CriterionDescriptionWhen to Use
Quality thresholdStop when score exceeds a targetWhen you have a reliable scoring mechanism
Max iterationsHard cap on revision roundsAlways (safety net)
Diminishing returnsStop when improvement per round drops below a minimumWhen scores are noisy
Deterministic passStop when all hard checks passFor code or structured output
No new issuesStop when the critic finds nothing to fixWhen issues are the primary signal
Budget exhaustionStop when token/cost budget is used upProduction systems with cost constraints

:::tip Practical Guidance For most tasks, 3 iterations with a 0.85 threshold is a good starting point. The first revision typically captures 60-70% of the improvement. Subsequent rounds see diminishing returns. :::

When Self-Refinement Works Best

  • Code generation -- Syntax checks and test execution provide concrete feedback.
  • Structured data extraction -- Schema validation catches missing or malformed fields.
  • Technical writing -- Checklists for completeness, accuracy, and style.
  • Translation -- Back-translation and fluency scoring guide refinement.

:::warning When It Backfires Self-refinement can degrade output when the critic is unreliable. If the LLM consistently misjudges quality (e.g., scoring a wrong answer highly), refinement will optimize for the wrong target. Always validate the critic's accuracy before trusting the loop. :::

PatternFocusMemoryScope
Self-RefinementPolishing a single outputWithin-task onlyOne artifact
Reflection (Reflexion)Learning from failures across episodesCross-episode memoryAgent behavior
Chain-of-ThoughtReasoning before answeringNoneSingle inference
Human-in-the-LoopExternal feedback from humansDepends on implementationVariable

Key Takeaways

  • Self-refinement is the simplest pattern for improving output quality without external feedback.
  • In LangGraph, model it as: generate node, evaluate node, conditional quality gate, refine node.
  • Combine deterministic checks (syntax, schema, length) with LLM-based critique for robust quality gates.
  • Track best_output and best_score in state to always return the highest-quality version.
  • Always define explicit stopping criteria: quality threshold, max iterations, and diminishing returns.
  • The first revision gives the biggest improvement; subsequent rounds face diminishing returns.

Further Reading

  • Madaan, A. et al. "Self-Refine: Iterative Refinement with Self-Feedback" (NeurIPS 2023)
  • Chen, X. et al. "Teaching Large Language Models to Self-Debug" (ICLR 2024)
  • Welleck, S. et al. "Generating Sequences by Learning to Self-Correct" (ICML 2023)