LangGraph / Intermediate Track Module 5 / 10
LangGraph Intermediate ⏱ 28 min
DEV

Cycles & Reflection: Intermediate

Self-correction through loops

How to Use This Lesson

  • Start with the user problem, then map the pattern to architecture and failure modes.
  • If a code or design example is included, change one assumption and reason through the impact.
  • Use role callouts, checklists, and Q&A sections as implementation or interview prep notes.

This lesson focuses on Cycles & Reflection at the intermediate level. Use it to move from definition to implementation-ready explanation.

Concept

Reflection loops add a self-evaluation node after main generation. The evaluator critiques output and decides: good enough (exit) or needs revision (loop back). Common patterns: generate-critique-revise, plan-execute-evaluate, draft-review-refine. Each iteration costs tokens - design stopping criteria carefully. Use a separate judge LLM to avoid same-model self-bias.

Key Facts

  • Reflection: generate, critique with separate prompt, revise if needed
  • LLM-as-judge: separate model for evaluation reduces same-model self-bias
  • Max iterations guard: include an iteration_count with an int reducer and recursion_limit
  • Constitutional AI: evaluate against defined principles, rewrite if violated
  • Token cost: 3-iteration reflection costs 3-5x a single pass

Reference Implementation

from typing import TypedDict, Annotated, List

MAX_ITER = 3

def add_int(old: int, new: int) -> int:
    return old + new

def append_list(old: List[str], new: List[str]) -> List[str]:
    return old + new

class ReflectionState(TypedDict):
    task: str
    draft: str
    critiques: Annotated[List[str], append_list]
    iteration: Annotated[int, add_int]
    final: str

def generate(state: ReflectionState):
    if state.get("critiques"):
        prompt = f"Task: {state['task']}\nFix this: {state['critiques'][-1]}"
    else:
        prompt = f"Complete: {state['task']}"
    draft = f"Draft v{state.get('iteration', 0) + 1}"  # replace with llm call
    return {"draft": draft, "iteration": 1}

def critique(state: ReflectionState):
    evaluation = "PASS" if state["iteration"] >= 2 else "Needs more depth"
    return {"critiques": [evaluation]}

def should_continue(state: ReflectionState) -> str:
    if state["iteration"] >= MAX_ITER or "PASS" in state["critiques"][-1]:
        return "finalize"
    return "generate"

def finalize(state: ReflectionState):
    return {"final": state["draft"]}

# Also invoke with a hard runtime guard:
# app.invoke(input_state, {"recursion_limit": 10})

Interview Q&A

Q1. What is a reflection loop and when does it improve output quality?

A reflection loop is generate-evaluate-revise, repeated until quality is sufficient. It improves output for: long-form writing, code generation (compile-check-fix), complex reasoning (verify logic), and safety-critical content. It does not help much for simple factual retrieval where the first pass is already deterministic.

Q2. How do you avoid the sycophancy problem in self-reflection?

Use a separate LLM as judge with a different prompt than the generator. Same-model self-critique often validates its own output. Use a stricter judge prompt with specific evaluation criteria. Have the judge produce a numeric score not just pass/fail - route back if below threshold. Using a different model family for judging is most effective.

Q3. What is the Plan-Execute-Evaluate pattern?

A three-phase loop: Plan node (LLM breaks task into steps), Execute node (run each step with tools), Evaluate node (check if plan succeeded or needs replanning). Used in research agents, coding agents, and complex automation. LangGraph’s cycle support makes this natural - the evaluate node loops back to plan if needed.

Q4. What error protects you from accidental infinite loops?

LangGraph raises GraphRecursionError when execution exceeds the configured recursion_limit. Treat it as a production safety signal: log the state, show a recoverable error, and fix the routing or stopping criteria rather than simply increasing the limit.

Q5. Why should iteration be an int update rather than a list update?

The reducer and update type must match. An integer counter should receive 1 and merge with add_int. Returning [1] to an int field is a common runtime bug because the next reducer call tries to add an int and a list.

Practice Task

Explain when this LangGraph pattern is safer than a linear chain, then name one production failure it prevents.