TechByteByByte

Loops: Generate, Review, Revise

Build a genuine, working revision loop from scratch, watch its state evolve across real iterations, and add the bounded retry count every real loop needs — grounded in AutoGPT's real, documented runaway-cost failure.

#LangGraph#Loops#State#Iteration

Every module so far has built graphs that move forward, node after node, toward END. Real workflows frequently need something genuinely different: a step that might need to happen again, based on how it went the first time. This module builds that properly, and takes the real, honest danger of getting it wrong seriously.

The shape, drawn first

flowchart TD
    START --> generate
    generate --> review
    review -->|good enough| END
    review -->|needs revision| revise
    revise --> review

Notice revise loops back to review, not back to generate — a genuinely deliberate choice. Once something exists, checking whether a revision improved it is a separate, real step from creating the first draft.

Example 1: building the loop

from typing import TypedDict
from langgraph.graph import StateGraph, START, END
from langchain.chat_models import init_chat_model

class State(TypedDict):
    topic: str
    draft: str
    is_good: bool

model = init_chat_model("openai:gpt-4o-mini")

def generate(state: State) -> dict:
    response = model.invoke(f"Write one short sentence about {state['topic']}.")
    return {"draft": response.content}

def review(state: State) -> dict:
    return {"is_good": len(state["draft"]) > 40}

def revise(state: State) -> dict:
    response = model.invoke(f"Make this sentence longer and more detailed: {state['draft']}")
    return {"draft": response.content}

def route_after_review(state: State) -> str:
    return "END" if state["is_good"] else "revise"

builder = StateGraph(State)
builder.add_node("generate", generate)
builder.add_node("review", review)
builder.add_node("revise", revise)

builder.add_edge(START, "generate")
builder.add_edge("generate", "review")
builder.add_conditional_edges("review", route_after_review, {"END": END, "revise": "revise"})
builder.add_edge("revise", "review")

graph = builder.compile()
result = graph.invoke({"topic": "the ocean", "draft": "", "is_good": False})
print(result["draft"])

Notice add_conditional_edges("review", route_after_review, {"END": END, "revise": "revise"}) — a dictionary mapping, rather than a plain list, letting a routing function’s return value (“END”) map cleanly onto the real END object without the routing function needing to import it directly.

Example 2: watching state evolve across real iterations

Let’s actually trace what happens, iteration by iteration, rather than only seeing the final result.

from typing import TypedDict
from langgraph.graph import StateGraph, START, END

class State(TypedDict):
    draft: str
    is_good: bool
    iteration: int

def generate(state: State) -> dict:
    return {"draft": "Short.", "iteration": 1}

def review(state: State) -> dict:
    print(f"Iteration {state['iteration']}: draft = {state['draft']!r} ({len(state['draft'])} chars)")
    return {"is_good": len(state["draft"]) > 30}

def revise(state: State) -> dict:
    return {"draft": state["draft"] + " Adding more detail each time.", "iteration": state["iteration"] + 1}

def route_after_review(state: State) -> str:
    return "END" if state["is_good"] else "revise"

builder = StateGraph(State)
builder.add_node("generate", generate)
builder.add_node("review", review)
builder.add_node("revise", revise)
builder.add_edge(START, "generate")
builder.add_edge("generate", "review")
builder.add_conditional_edges("review", route_after_review, {"END": END, "revise": "revise"})
builder.add_edge("revise", "review")

graph = builder.compile()
result = graph.invoke({"draft": "", "is_good": False, "iteration": 0})
print("Final:", result["draft"])

Run this and read the printed iterations directly — this is genuinely what “state evolving across a loop” looks like in practice: the same draft field, growing a little more with each real pass through revise, until review finally judges it good enough.

Example 3: the real, honest danger of an unbounded loop

Recall Module 6’s GraphRecursionError — this is exactly the scenario that exception exists to catch, and it’s worth taking seriously with a real, documented example, not just a warning.

In March 2023, AutoGPT — one of the very first widely used autonomous agent projects — went viral, and just as quickly became known for a real, well-documented failure mode: it would genuinely get stuck in repetitive loops, re-attempting variations of the same failed step without ever converging, and every one of those repeated attempts consumed real, paid API calls. Users reported running up real, unexpected costs from exactly this pattern — a loop with no deliberate, bounded exit condition, running far longer than anyone intended. This is not a hypothetical risk this module is inventing to justify itself; it’s a documented, real cost real users actually paid, from exactly the class of bug this module’s next example directly prevents.

Example 4: a genuine, bounded retry count

from typing import TypedDict
from langgraph.graph import StateGraph, START, END

class State(TypedDict):
    draft: str
    is_good: bool
    retry_count: int

def generate(state: State) -> dict:
    return {"draft": "Short.", "retry_count": 0}

def review(state: State) -> dict:
    return {"is_good": len(state["draft"]) > 100}  # deliberately hard to satisfy

def revise(state: State) -> dict:
    return {"draft": state["draft"] + " more.", "retry_count": state["retry_count"] + 1}

def route_after_review(state: State) -> str:
    if state["is_good"]:
        return "END"
    if state["retry_count"] >= 3:
        return "escalate"
    return "revise"

def escalate(state: State) -> dict:
    return {"draft": f"[Needs human review after {state['retry_count']} attempts] {state['draft']}"}

builder = StateGraph(State)
builder.add_node("generate", generate)
builder.add_node("review", review)
builder.add_node("revise", revise)
builder.add_node("escalate", escalate)
builder.add_edge(START, "generate")
builder.add_edge("generate", "review")
builder.add_conditional_edges("review", route_after_review, {"END": END, "revise": "revise", "escalate": "escalate"})
builder.add_edge("revise", "review")
builder.add_edge("escalate", END)

graph = builder.compile()
result = graph.invoke({"draft": "", "is_good": False, "retry_count": 0})
print(result["draft"])

Notice retry_count is checked inside route_after_review, as a genuine, deliberate exit condition — not relying on LangGraph’s default recursion limit to eventually catch it. This is the real, meaningful difference between a loop that happens to be protected by a blunt, generic safety net, and one that was actually designed with a real, considered stopping point. When the count is exceeded, the workflow doesn’t crash — it routes to escalate, a genuine, graceful fallback, exactly the pattern real production systems need.

Common mistakes worth avoiding

Relying entirely on the default recursion limit instead of a deliberate retry_count. Recall Module 6 — the default limit exists to catch accidental infinite loops. A genuinely intended, bounded loop should track and check its own count explicitly, as Example 4 did, producing a real, graceful fallback rather than a generic crash.

Looping back to generate instead of review after a revision. Recall this module’s opening diagram — revise should refine the existing draft and hand it back to review, not restart the entire process from scratch, discarding real, already-useful work.

Forgetting that each loop iteration costs a real, additional model call. Recall AutoGPT’s real, documented cost story — every pass through revise and review in a real workflow using a real model is a genuine, billed API call. A loop with a generous retry count, multiplied across many real users, is a genuine, real operating cost worth deliberately sizing, not an abstract concern.

What you should take away from this module

  • A real revision loop routes revise back to review, not back to generate — refining existing work, not restarting it.
  • Real loops need a deliberate, explicit stopping condition — a retry_count, checked inside the routing function — not just reliance on the default recursion limit.
  • AutoGPT’s real, documented 2023 failure — repetitive, unbounded loops consuming real, unexpected cost — is exactly the risk a bounded retry count and a graceful escalation path exist to prevent.
  • A properly bounded loop escalates gracefully when it can’t converge, rather than crashing or looping indefinitely.

Where this goes next

The next module builds a real ReAct-style tool-calling loop directly from graph primitives — the exact mechanism behind every agent you’ve used, now constructed by hand, one real piece at a time, before the following module introduces the prebuilt shortcut.

Author
TechByteByByte Editorial Team
Reviewed by
TechByteByByte Admin
Published
Last reviewed