From Linear Chains to Cyclic Graphs: The Essential Guide to Stateful AI Agents

10 min read Original article ↗

Press enter or click to view image in full size

Why your RAG pipeline isn’t an “Agent” yet. A deep dive into the paradigm shift from rigid LangChain sequences to dynamic, looping LangGraph workflows.

Anonymous

The “Grasping” Problem

Let’s be honest. We’ve built some impressive machinery in the first two articles of this series.

We gave our AI the power of conceptual understanding with
Vector Embeddings (Article 1).

Then, we built a high-speed
Hybrid Search pipeline with RAG (Article 2),

creating a “knowledge engine” that can fetch the perfect information from your private documents.

You have a Ferrari of knowledge retrieval. But right now, that Ferrari has a brick on the gas pedal, and the steering wheel is locked straight ahead.

Your current RAG system waits passively for a question, fetches an answer, and then immediately forgets everything that just happened. It is a smart chatbot, but it is not an Agent.

What is an Agent? An agent is an AI system that can reason, plan, execute actions, perceive the results of those actions, and then determine the next best step to take to achieve a goal.

You want an AI that can: “Write a research paper, then critique its own work for accuracy, then revise the draft based on that critique, and repeat until it’s perfect.”

This requires a system that can remember past mistakes and loop back to try again. This is the fundamental shift from linear Chains to cyclic Graphs.

Part 1: The Linear Trap (Why Chains Fail at Complex Tasks)

Standard LangChain sequences (using the modern LCEL syntax) are designed as Directed Acyclic Graphs (DAGs).

In plain English, they are one-way streets. Think of it like an assembly line. Data flows into Step A, moves to Step B, then to Step C, and falls off the end as a final product.

This works perfectly for simple, single-pass tasks like standard RAG: Question → Retrieve Documents → Generate Answer → Stop.

But what happens when you try to build a complex, iterative workflow on an assembly line? It breaks.

The Visual Proof: The Dead End

Let’s imagine a simple “Writer-Critic” workflow. The writer produces a draft, and the critic provides feedback. In a linear chain, the data flows in one direction and hits a barrier.

Press enter or click to view image in full size

The failure mode of linear chains for complex tasks. The workflow successfully generates critique, but there is no mechanism to send that critique back to the writer for a revision. The process terminates prematurely.

The Code Proof: Hitting the Wall

Let’s prove this failure in code. We will chain a Writer step and a Critic step together.

from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.7)

# --- Step 1: The Writer Chain ---
writer_prompt = ChatPromptTemplate.from_template("Write a short paragraph about: {topic}.")
writer_chain = writer_prompt | llm | StrOutputParser()

# --- Step 2: The Critic Chain ---
# The input to this chain is the *output* of the writer chain
critic_prompt = ChatPromptTemplate.from_template("Critique this draft: {draft}")
critic_chain = critic_prompt | llm | StrOutputParser()

# --- Combining them into a Linear Sequence ---
# The output of writer becomes input to critic.
linear_chain = ({"draft": writer_chain} | critic_chain)

# --- Run the sequence ---
final_output = linear_chain.invoke({"topic": "Artificial General Intelligence"})

print("--- Final Output of the Linear Chain ---")
print(final_output)
# RESULT: You get a list of criticisms, but no revised draft. The process ends.

To build true agents, we must abandon the assembly line. We need LangGraph.

Part 2: The Paradigm Shift (The War Room and the Whiteboard)

LangGraph is a library designed to solve the “one-way street” problem by introducing two critical concepts that standard chains lack: Persistent State and Cyclic Flows.

The best way to understand it is the “War Room” analogy.

Imagine a team of experts trying to solve a complex crisis. They are all gathered around a giant Whiteboard.

  • The State (The Whiteboard): This is the most important concept. It is a shared, persistent memory bank. It holds everything the team currently knows: the current draft, the latest research, critique notes, and the plan. Everyone in the room reads from the same whiteboard and writes their updates onto it.
  • The Nodes (The Experts): You have a “Writer Expert” and a “Critic Expert.” They are just Python functions. They take turns stepping up to the whiteboard, reading the current situation, performing their specialized task using an LLM, and then updating the whiteboard with their results.
  • The Edges (The Logic): These are the rules governing who goes to the whiteboard next.
    - Normal Edge: “After the Writer is done, the Critic goes next.”
    - Conditional Edge (The Router): “After the Critic is done, look at the whiteboard. If the critique says the draft is bad, send the Writer back up. If it says it’s good, we are finished.”

Press enter or click to view image in full size

The fundamental shift. Linear chains are rigid, passing data like a hot potato until it drops. Stateful graphs revolve around a persistent, shared memory (State), enabling complex, cyclic, and self-correcting workflows.

Part 3: Visualizing the “State” (The Missing Link)

Before examining the code, it is essential to understand how the “State” changes over time. This is the hardest part for beginners to grasp, but it’s the key to everything.

In Python terms, the State is just a dictionary that gets passed around. Let’s watch it evolve as our agent runs.

Figure 3: The heartbeat of a LangGraph agent. The State is a persistent dictionary. Nodes are functions that take the current state as input and return updates to it. The Critic node updates the critique key, which the Writer node will then read in the next cycle to improve the draft.

Part 4: The Deep Code Dive (Building a Reflection Agent)

Let’s build the “Reflection Agent” from Figure 3. It will draft, reflect, and revise in a loop.

Prerequisites: pip install langgraph langchain-openai

Step 1: Define the Whiteboard structure (The State)

First, we tell LangGraph what our whiteboard looks like. We use Python TypedDict to define the schema of our shared memory.

from typing import TypedDict, Optional

# This defines the structure of our shared memory.
# Every node will receive this dictionary and return updates to it.
class AgentState(TypedDict):
topic: str # The original user goal
draft: Optional[str] # The current evolving draft
critique: Optional[str] # The latest feedback generated by the critic
revision_number: int # A counter to track how many loops we've done# This defines the structure of our shared memory.

# Every node will receive this dictionary and return updates to it.
class AgentState(TypedDict):
topic: str # The original user goal
draft: Optional[str] # The current evolving draft
critique: Optional[str] # The latest feedback generated by the critic
revision_number: int # A counter to track how many loops we've done

Step 2: Define the Experts (The Nodes)

Nodes are the workers. They are simple Python functions.

Crucial Point: Notice what the nodes take as input and what they return. They take the entire state dict, do their job, and return only the keys they want to update. LangGraph handles merging these updates back into the main state.

from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.7)

# --- The Writer Node ---
def writer_node(state: AgentState):
"""Node 1: Reads the state and writes or revises the draft."""
current_rev = state.get("revision_number", 0)
print(f"--- Executing Writer Node (Generating Revision {current_rev + 1}) ---")

# Check the whiteboard: Is there a critique waiting for me?
if state.get("critique"):
# Yes: This is a revision cycle.
prompt_text = """You are an expert writer. Revise your previous draft based on the critique.
Original Draft: {draft}
Critique: {critique}
Return ONLY the revised draft."""
else:
# No: This is the first draft.
prompt_text = "Write a short, one-paragraph summary about: {topic}."

prompt = ChatPromptTemplate.from_template(prompt_text)
chain = prompt | llm | StrOutputParser()

# Run chain using data right off the whiteboard
new_draft = chain.invoke({
"topic": state["topic"],
"draft": state.get("draft"),
"critique": state.get("critique")
})

# Return ONLY the updates. LangGraph will merge these into the main state.
return {"draft": new_draft, "revision_number": current_rev + 1}

# --- The Critic Node ---
def critic_node(state: AgentState):
"""Node 2: Reads the draft from the state and writes a critique."""
print("--- Executing Critic Node ---")
prompt = ChatPromptTemplate.from_template(
"Provide 3 short bullet points of critique for this draft:\n\nDraft:\n{draft}"
)
chain = prompt | llm | StrOutputParser()
critique = chain.invoke({"draft": state["draft"]})

# Update the whiteboard with the new critique
return {"critique": critique}

Step 3: Define the Logic (The Conditional Edge)

We need a “router.” This is a function that looks at the state and decides where to go next. It doesn’t change the state; it just returns the name of the next node.

from langgraph.graph import END

def should_continue(state: AgentState):
"""Decides if we should loop back to the writer or finish."""
# Look at the revision number on the whiteboard.
# Stop after 3 revisions so we don't loop forever and burn API credits.
if state["revision_number"] >= 3:
print("--- Max revisions reached. Finishing. ---")
return END # 'END' is a special node that stops the graph.

# Otherwise, loop back to the writer to fix the draft.
return "writer"def should_continue(state: AgentState):
"""Decides if we should loop back to the writer or finish."""
# Look at the revision number on the whiteboard.
# Stop after 3 revisions so we don't loop forever and burn API credits.
if state["revision_number"] >= 3:
print("--- Max revisions reached. Finishing. ---")
return END # 'END' is a special node that stops the graph.

# Otherwise, loop back to the writer to fix the draft.
return "writer"

Step 4: Build and Run the Graph

Now we wire it all together using StateGraph.

from langgraph.graph import StateGraph

# 1. Initialize the graph, telling it what our State structure looks like.
workflow = StateGraph(AgentState)

# 2. Add the nodes (the experts) to the graph.
workflow.add_node("writer", writer_node)
workflow.add_node("critic", critic_node)

# 3. Define the flow (the edges).

# Start here.
workflow.set_entry_point("writer")

# After the writer runs, ALWAYS go to the critic next.
workflow.add_edge("writer", "critic")

# After the critic runs, call the 'should_continue' function to decide.
workflow.add_conditional_edges(
"critic", # Where did we just come from?
should_continue, # What function decides next?
{
# Map the function's return values to actual node names.
"writer": "writer", # If function returns "writer", go to the writer node.
END: END # If function returns END, stop the graph.
}
)

# 4. Compile into a runnable application.
app = workflow.compile()

# --- RUN THE AGENT ---
initial_state = {"topic": "The importance of iterative design in engineering.", "revision_number": 0}
print("Starting Agent Workflow...\n")

# We use .stream() to watch the steps happen in real-time.
for output in app.stream(initial_state):
pass # The print statements inside the nodes will show progress.

# Get the final, completed state from the graph.
final_state = app.get_state(app.get_latest_run_id()).values
print("\n=== FINAL POLISHED DRAFT ===")
print(final_state["draft"])

Output Analysis: When you run this code, watch your console. You will see the cycle happen automatically:
Writer (Rev 1)CriticWriter (Rev 2)CriticWriter (Rev 3)Finish.

The final output is a polished draft that has been improved twice based on specific feedback. The shared “Whiteboard” (State) allowed the Writer to see the future Critic’s notes and use them as new input.

Conclusion & Connection to RAG

Moving from chains to graphs is a fundamental shift in how we design AI systems. By introducing Shared State and Cyclic Flows, we unlock the ability to build agents that can plan, execute, verify, and self-correct.

So, how does this connect to the RAG system from Article 2?

Think of it this way: RAG is the engine, and LangGraph is the driver.

The Hybrid Search RAG pipeline we built earlier is no longer the entire application. Instead, it becomes a powerful Tool used by one of the Nodes in your graph. A “Researcher Node” can now decide to call that RAG tool whenever it needs information, paste the results onto the shared whiteboard, and let other nodes use that context to perform their tasks.

We now have the knowledge engine (RAG) and the autonomous driver (LangGraph).