Agentic AI — Control Flow
How agents decide what to do next. Learn the ReAct framework, agent state management, complex conditional control flow, and building stateful agents with LangGraph.
In this lecture
19.1 The ReAct Framework
- Think — analyse the situation and decide what action to take.
- Act — execute the chosen action (search, calculate, call an API).
- Observe — review the result and determine the next step.
19.2 State Management of AI Agents
State management lets the agent: prevent forgetting previous context, support multi-turn conversations, track progress through complex workflows, and validate inputs.
Types of state (recap from Lecture 15)
- Ephemeral — temporary, single-computation values; deleted after use.
- Persistent — survives across steps/sessions (conversation history, preferences); stored via databases/checkpointers.
- Shared — a unified object multiple nodes/agents read and write — crucial for multi-agent collaboration.
19.3 Complex Control Flow
Simple chatbots are linear — one straight path. Real agents need complex control flow:
- Branching — choose different paths based on conditions (if price > budget, ask the user; else book).
- Loops / cycles — repeat steps (the ReAct loop; retry a failed tool call).
- Conditional routing — dynamically decide the next node from the current state.
- Human-in-the-loop (HITL) — pause the workflow for human approval before a critical action.
19.4 LangGraph: Graphs, Nodes, Edges, State
LangGraph has four core components:
1. State — the shared diary
A typed Python dictionary (TypedDict) passed between every step. Each node reads from it and writes to it, carrying context forward. TypedDict gives type safety and prevents key errors. The add_messages reducer appends new messages instead of overwriting the history.
2. Nodes — the workers
Python functions that perform work. Each node receives the current state, performs an operation (call an LLM, run a tool, compute), and returns updates to the state. Keep each node to a single responsibility.
3. Edges — the logic gates
- Standard edge — a fixed path: Node A → Node B.
- Conditional edge — a dynamic path; a routing function inspects the state and decides which node runs next. Like a GPS that reroutes based on traffic.
4. The StateGraph — the builder
The StateGraph assembles nodes and edges into an executable workflow. Three essential methods: add_node() (register a worker), add_edge() / add_conditional_edges() (connect them), and set_entry_point() (define where execution starts). Finally .compile() turns it into a runnable app.
19.5 Building an Agent Graph
from typing import TypedDict, Annotated
from langgraph.graph.message import add_messages
# State = a typed dictionary shared across all nodes
class AgentState(TypedDict):
messages: Annotated[list, add_messages] # add_messages APPENDS
flight_price: int # other fields OVERWRITE
decision: str
# Node: receives state, returns state updates
def check_price(state: AgentState):
if state["flight_price"] > 500:
return {"decision": "ask_user"}
return {"decision": "book"}
# Routing function for the conditional edge
def route(state: AgentState):
return "ask" if state["decision"] == "ask_user" else "book"
from langgraph.graph import StateGraph, START, END
builder = StateGraph(AgentState)
# 1. Register nodes (workers)
builder.add_node("find_flight", find_flight)
builder.add_node("check_price", check_price)
builder.add_node("book_flight", book_flight)
builder.add_node("ask_user", ask_user)
# 2. Connect with edges
builder.add_edge(START, "find_flight")
builder.add_edge("find_flight", "check_price")
builder.add_conditional_edges("check_price", route,
{"book": "book_flight", "ask": "ask_user"})
builder.add_edge("book_flight", END)
# 3. Compile into a runnable app
app = builder.compile()
result = app.invoke({"flight_price": 0, "decision": "", "messages": []})
from langgraph.prebuilt import ToolNode
from langgraph.graph import StateGraph, START, END
def assistant_node(state): # the "Think" node
return {"messages": [llm_with_tools.invoke(state["messages"])]}
tool_node = ToolNode([multiply]) # the "Act" node
def should_continue(state): # router: loop or stop?
last = state["messages"][-1]
return "tools" if last.tool_calls else END
builder = StateGraph(State)
builder.add_node("assistant", assistant_node)
builder.add_node("tools", tool_node)
builder.add_edge(START, "assistant")
builder.add_conditional_edges("assistant", should_continue)
builder.add_edge("tools", "assistant") # loop back -> ReAct cycle
graph = builder.compile()
The edge tools → assistant creates the loop — the agent keeps reasoning and acting until should_continue returns END. That loop is the ReAct cycle implemented as a graph.
MemorySaver) when compiling the graph to save state snapshots. The agent can then recover from crashes, resume multi-turn conversations after a restart, and avoid recomputing completed work.
ReAct and LangGraph components are heavily tested.
The ReAct framework cycles through which three steps?
ReAct = Reason + Act: Think (decide), Act (execute), Observe (review the result) — then think again. (Retrieve→Augment→Generate is RAG.)
The main benefit of the ReAct loop is that the agent can:
Each Observe feeds back into Think, so when an action fails the agent reconsiders and tries a different approach — it self-corrects.
In an AI agent, "state" is best described as:
State is the constantly-updated "order pad" that stores messages, intermediate results and progress so the agent stays coherent.
In LangGraph, a node is:
A node is a worker function: it takes the current state, does an operation, and returns updates. Edges connect nodes.
A conditional edge in LangGraph:
A conditional edge runs a routing function that inspects the state and dynamically picks which node executes next — like a GPS rerouting.
Annotating the messages field with add_messages ensures that new messages are:
add_messages is a reducer that appends new messages, preserving the full conversation history instead of replacing it.
Human-in-the-Loop (HITL) is used to:
HITL inserts a human checkpoint — the workflow pauses for approval before irreversible actions (sending an email, making a purchase).
Why is a graph-based approach better than linear code for AI agents?
Graphs allow loops (ReAct), conditional routing based on state, and explicit, predictable execution that is easy to debug and scale.
Define a LangGraph State TypedDict with a messages list (appended) and a task_status string.
from typing import TypedDict, Annotated
from langgraph.graph.message import add_messages
class AgentState(TypedDict):
messages: Annotated[list, add_messages] # appended each step
task_status: str # overwritten each step
The Annotated[..., add_messages] reducer makes messages append; plain fields like task_status are overwritten on update.
Build a minimal LangGraph with a single node chat connected from START to END, and compile it.
from langgraph.graph import StateGraph, START, END
from langchain_core.messages import AIMessage
def chat_node(state):
text = state["messages"][-1].content
return {"messages": [AIMessage(content=f"I heard: {text}")]}
graph = StateGraph(AgentState)
graph.add_node("chat", chat_node)
graph.add_edge(START, "chat")
graph.add_edge("chat", END)
app = graph.compile()
add_node registers the worker, two add_edge calls wire START → chat → END, and .compile() produces a runnable app.
Write a routing function and add a conditional edge so that, after a check node, the graph goes to "approve" if state["amount"] > 1000, otherwise to "auto".
def route(state):
if state["amount"] > 1000:
return "approve" # needs human approval
return "auto" # auto-process
builder.add_conditional_edges(
"check", route,
{"approve": "approve_node", "auto": "auto_node"}
)
The routing function inspects the state and returns a key; the mapping dictionary sends the flow to the matching node.
Name LangGraph's four core components and state the role of each in one line.
State — a typed dictionary (the shared "diary") passed between steps, holding all context. Nodes — Python worker functions that receive state and return updates. Edges — connections defining flow; conditional edges route dynamically based on state. StateGraph — the builder that registers nodes, connects edges and compiles everything into a runnable app.
.compile(). add_messages appends history; checkpointing enables recovery.