Lesson 08 | Building a Standard ReAct Agent Architecture
Implementing the classic "Reason-Act-Observe" (ReAct) loop using a cyclic edge.
Welcome back, fellow geeks, to our LangGraph Multi-Agent Masterclass. It's your old friend here.
Over the past 7 lessons, we've laid a solid foundation for our "AI Content Agency": we've defined state machines, mastered basic node routing, and even taught our LLMs to generate simple structured outputs. However, if you look closely at our previous architectures, you'll notice a fatal flaw—they are too "linear" and too "obedient."
Imagine if the Researcher in our agency was a purely linear thinker: if you asked them to look up "new products from the 2024 Apple Event," and they didn't have this knowledge in their internal weights, what would they do? Previously, they would just start making things up (hallucinating).
How does a truly senior researcher work? They would first search Google (Act), look at the titles of the search results (Observe), and think to themselves, "Hmm, this link looks promising, I should click it" (Reason). Then, they would use a browser tool to read the webpage (Act). If the content isn't enough, they'd try a different keyword... They repeat this loop until they feel they've gathered enough information, and only then will they report back to you.
This is the tough nut we are going to crack today: the ReAct (Reason + Act) Architecture.
Today, we will leverage LangGraph's Cyclic Edges to inject genuine "reasoning and exploration" capabilities into our AI agency, completely refactoring our Researcher agent. Grab your coffee, and let's dive in!
🎯 Lesson Objectives
- Master the underlying logic of ReAct: Understand how the "Reason-Act-Observe" loop breaks through the capability ceiling of LLMs.
- Master LangGraph cyclic graph construction: Learn to use Conditional Edges to implement unpredictable, dynamic routing.
- Hands-on Researcher refactoring: Build a super researcher for our AI Content Agency that can autonomously call tools and decide when to conclude an investigation.
- State and context management: Understand how
MessagesStateaccumulates clues like memory within an infinite loop.
📖 Theory Breakdown
1. What is ReAct?
ReAct is a paradigm proposed by Princeton University and Google in 2022 (in the paper ReAct: Synergizing Reasoning and Acting in Language Models). Before this, LLMs either purely reasoned (Chain of Thought) or purely acted (directly calling APIs).
ReAct combines the two: it allows the LLM to "think out loud" before taking action, and to "assess the situation" after seeing the results of that action.
For our Researcher, its inner monologue looks like this:
- Thought: The boss asked me to check the latest features of LangGraph. I need to use a search engine first.
- Action: Call
search_tool("LangGraph new features 2024") - Observation: The tool returned 5 search results.
- Thought: The 2nd result looks the most relevant; I need to read its detailed content.
- Action: Call
scrape_web_tool("url_to_doc") - Observation: ...webpage text...
- Thought: I have enough information now. I can start summarizing and finish the task.
2. How to map ReAct in LangGraph?
In LangGraph, we don't need to hardcode this loop. We only need to define two core nodes and one conditional edge:
- Agent Node (LLM Node): Responsible for reasoning and deciding whether to call a tool.
- Tool Node: Responsible for executing the tool and returning the results.
- Conditional Edge: Connects to the Agent Node. If the LLM decides to call a tool, it routes to the Tool Node; if the LLM directly outputs the final answer, it routes to
END.
The beauty of this architecture is: the number of loops is decided by the LLM itself. If it feels it has investigated enough, the graph ends; if not, the graph continues to loop.
3. Core Architecture Diagram (Mermaid)
Below is the ReAct workflow diagram we are going to build for our Researcher today. Pay close attention to the loop circuit in the diagram:
graph TD
classDef start_end fill:#f96,stroke:#333,stroke-width:2px;
classDef agent fill:#69b3a2,stroke:#333,stroke-width:2px;
classDef tool fill:#ff9999,stroke:#333,stroke-width:2px;
classDef condition fill:#e1d5e7,stroke:#333,stroke-width:2px;
START((START)):::start_end
END((END)):::start_end
subgraph ReAct_Loop [Core ReAct Loop (Researcher)]
Agent[🤖 Agent Node
Reason & Decide Next Step]:::agent
Condition{💡 Routing Decision
Has Tool Call?}:::condition
Tools[🛠️ Tool Node
Execute Tool & Record Observation]:::tool
end
START --> Agent
Agent --> Condition
Condition -- "Yes (Go Act)" --> Tools
Tools -- "Return Observation" --> Agent
Condition -- "No (Task Complete)" --> ENDInstructor's Sharp Take: Do you see that line pointing from Tools back to Agent? This is exactly where LangGraph overpowers traditional LangChain Chains. Chains are unidirectional, whereas Graphs are Turing-complete. With loops, an Agent truly comes to "life."
💻 Hands-on Code Walkthrough
Now, let's return to the "AI Content Agency" project. Our Planner has just issued a task: "Investigate the most popular AI coding assistant tools currently on the market and provide a brief comparison report."
Our Researcher needs to take the order.
1. Preparation and Tool Definition
First, we need to equip our Researcher with "hands and eyes" (Tools). For demonstration purposes, we'll use mocked search and web scraping tools.
import json
from typing import TypedDict, Annotated, Sequence
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage, ToolMessage
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
# ==========================================
# 1. Define Tools - The Researcher's Arsenal
# ==========================================
@tool
def search_web(query: str) -> str:
"""Use this tool when you need to search the internet for the latest information."""
print(f" [Tool 执行] 🔍 正在搜索: {query}")
# Mock search engine results
if "AI 编程助手" in query:
return json.dumps([
{"title": "Cursor: 彻底改变编程方式", "url": "https://cursor.sh/about"},
{"title": "GitHub Copilot 最新特性", "url": "https://github.com/copilot"}
])
return "未找到相关信息。"
@tool
def scrape_web(url: str) -> str:
"""Use this tool when you need to read the detailed content of a specific webpage."""
print(f" [Tool 执行] 📄 正在抓取网页: {url}")
# Mock web scraping
if "cursor" in url.lower():
return "Cursor 是一款基于 VS Code 分支的 AI 编辑器,深度集成了 Claude 3.5 Sonnet,支持多文件上下文重构。"
elif "copilot" in url.lower():
return "GitHub Copilot 集成了 OpenAI 的模型,优势在于与 GitHub 生态的无缝对接以及企业级安全合规。"
return "网页内容无法读取。"
# Tool list
tools = [search_web, scrape_web]
2. Defining State and Agent Node
In ReAct, State is incredibly important. We need to record the entire history of "Reason-Act-Observe." The add_messages reducer provided by LangGraph is the perfect solution.
# ==========================================
# 2. Define State
# ==========================================
class AgentState(TypedDict):
# Using add_messages ensures messages are appended, not overwritten
# This is the secret to ReAct remembering previous search results!
messages: Annotated[Sequence[BaseMessage], add_messages]
# ==========================================
# 3. Define LLM and Node Binding
# ==========================================
# Initialize LLM (Assuming you have configured OPENAI_API_KEY)
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
# Crucial step: Bind tools to the LLM
# This is equivalent to telling the LLM: "You can call these functions anytime you need to."
llm_with_tools = llm.bind_tools(tools)
def researcher_agent_node(state: AgentState):
"""
Agent Node: Responsible for reading the current context (including previous observations),
reasoning, and deciding whether to output the final answer or continue calling tools.
"""
print("\n[Agent 思考中...] 🧠 正在分析当前情报...")
messages = state["messages"]
# Inject system prompt to give the Researcher its persona
sys_msg = SystemMessage(content="""
你是 AI Content Agency 的高级研究员 (Senior Researcher)。
你的目标是通过工具收集准确的信息。
请遵循以下原则:
1. 不要瞎编!必须基于工具返回的事实。
2. 如果信息不够,请继续搜索或读取网页。
3. 当你收集到足够的信息时,请直接输出最终的调查报告。
""")
# Invoke the LLM
response = llm_with_tools.invoke([sys_msg] + messages)
# Return the new message (it will be appended to the state)
return {"messages": [response]}
3. Building Tool Node and Conditional Routing
This is the core code of today's lesson. We need to process the tool_calls returned by the LLM, execute them, and stuff the results back to the LLM as ToolMessages.
(Note: LangGraph officially provides a ready-to-use ToolNode, but to ensure you thoroughly understand the underlying logic, we are writing a minimalist custom tool execution node.)
# ==========================================
# 4. Define Tool Execution Node (Tool Node)
# ==========================================
def tool_execution_node(state: AgentState):
"""
Tool Node: Specifically responsible for intercepting the LLM's tool_calls,
executing the actual Python functions, and wrapping the results as ToolMessages to return.
"""
messages = state["messages"]
# Get the latest message from the LLM
last_message = messages[-1]
tool_responses = []
# Iterate through all the tools the LLM requested to call
for tool_call in last_message.tool_calls:
tool_name = tool_call["name"]
tool_args = tool_call["args"]
# Find the corresponding tool function and execute it
tool_instance = next(t for t in tools if t.name == tool_name)
try:
result = tool_instance.invoke(tool_args)
except Exception as e:
result = f"工具执行出错: {str(e)}"
# Must be wrapped as a ToolMessage, including the tool_call_id
# This way the LLM knows which specific call this result corresponds to
tool_responses.append(
ToolMessage(
content=str(result),
name=tool_name,
tool_call_id=tool_call["id"]
)
)
return {"messages": tool_responses}
# ==========================================
# 5. Define Conditional Routing (Conditional Edge)
# ==========================================
def should_continue(state: AgentState) -> str:
"""
Routing logic: Decides whether the graph continues looping or ends.
"""
messages = state["messages"]
last_message = messages[-1]
# If the message returned by the LLM contains tool_calls, it means it wants to act
if last_message.tool_calls:
print(" [路由决策] 🔀 发现工具调用,前往 Tools 节点。")
return "continue"
# Otherwise, it means the LLM thinks the task is done and outputted plain text
print(" [路由决策] 🏁 任务完成,准备输出报告。")
return "end"
4. Assembling and Running our Agency Researcher
Let's wire the nodes and edges together!
# ==========================================
# 6. Build LangGraph
# ==========================================
workflow = StateGraph(AgentState)
# Add nodes
workflow.add_node("agent", researcher_agent_node)
workflow.add_node("tools", tool_execution_node)
# Set entry point: Always enter the agent node first to reason
workflow.add_edge(START, "agent")
# Add conditional edges: From the agent, decide where to go based on should_continue's return value
workflow.add_conditional_edges(
"agent",
should_continue,
{
"continue": "tools", # If it returns 'continue', go to the tools node
"end": END # If it returns 'end', finish the graph
}
)
# Add cyclic edge: After tools finish executing, it MUST return to the agent node for "observation and next step reasoning"
workflow.add_edge("tools", "agent")
# Compile the graph
researcher_app = workflow.compile()
# ==========================================
# 7. Mock Run (Demo)
# ==========================================
if __name__ == "__main__":
from langchain_core.messages import SystemMessage # Complete the missing import from above
print("=== AI Content Agency: Researcher 启动 ===")
# Task issued by the Planner
initial_task = "请调查目前市面上最火的 AI 编程助手工具(Cursor和Copilot),并给出一份简短的对比报告。"
inputs = {"messages": [HumanMessage(content=initial_task)]}
# The stream method allows us to see the execution process step-by-step
for output in researcher_app.stream(inputs, stream_mode="updates"):
# Print the currently executing node
for node_name, state_update in output.items():
pass # Detailed logs are already printed inside the nodes
# Print the final result
final_state = researcher_app.get_state(inputs) # Get the final state
# (Note: The get_state usage above might require a thread_id in some versions.
# The simplest way to get the final message is directly from the last stream output)
print("\n==================================")
print("📊 Researcher 最终报告:")
print("==================================")
# Extract the content of the last message in the state
print(output['agent']['messages'][-1].content)
🧠 Mental Walkthrough of the Execution (The LLM's Inner Monologue)
When you run this code, you will see the console frantically outputting:
[Agent 思考中...](Agent Thinking...) -> The LLM decides it needs to look up information.[路由决策] 🔀 发现工具调用...(Routing Decision: Tool call found...) -> Triggers the conditional edge.[Tool 执行] 🔍 正在搜索...(Tool Execution: Searching...) -> Executessearch_web.- Returns to Agent,
[Agent 思考中...]-> Sees the search results only have URLs, decides to dig deeper. [路由决策] 🔀 发现工具调用...[Tool 执行] 📄 正在抓取...(Tool Execution: Scraping...) -> Executesscrape_web.- Returns to Agent,
[Agent 思考中...]-> Information gathering is complete, starts writing the report. [路由决策] 🏁 任务完成...(Routing Decision: Task complete...) -> Routes to END.
This is true intelligence! It is no longer a rigid process, but an Agent with autonomous exploration capabilities.
💣 Gotchas and Troubleshooting Guide
As your mentor, I can't just teach you how to run a Demo; I also have to tell you how this architecture will completely wreck you in a real production environment.
💣 Pitfall 1: Infinite Loops
Symptom: The LLM gets stuck in a loop. For example, it searches for something and can't find it, so it changes the keyword and searches again, still can't find it, and keeps searching... Your API bill will explode overnight. How to avoid: In LangGraph, you must set a recursion limit when invoking the graph. The default is 25, but it's highly recommended to set it explicitly.
# Limit the graph to a maximum of 10 steps
researcher_app.invoke(inputs, {"recursion_limit": 10})
If the limit is reached, LangGraph will throw a GraphRecursionError, which you can catch at the outer layer to implement graceful degradation.
💣 Pitfall 2: Tool Errors Crashing the Graph
Symptom: scrape_web encounters an anti-bot block and throws a TimeoutError, causing the entire Graph to crash and exit.
How to avoid:
Never let a tool's Error bubble up to the graph level! Just like the try...except block we wrote in tool_execution_node, you should convert the error message into a string and wrap it in a ToolMessage to return to the LLM.
LLMs are very smart. If it sees a ToolMessage saying "Error: 403 Forbidden", it will figure out a workaround (like trying a different website, or stating in the report that "the website is inaccessible"). Turning exceptions into observations is the essence of the ReAct architecture.
💣 Pitfall 3: Context Window Explosion
Symptom: The Researcher checks 10 webpages, each with 5,000 words. The messages list gets longer and longer, eventually exceeding GPT-4's 128k context limit, throwing a TokenLimitExceeded error.
How to avoid:
In real-world projects, we cannot add_messages infinitely. We need to introduce State Trimming or a Summarize Node within the loop. When len(messages) exceeds a certain threshold, it triggers an internal smaller LLM to compress the previous useless webpage content into a summary.
(Spoiler alert: We will dive deep into this advanced technique in Lesson 12: Long Context and Memory Management!)
📝 Lesson Summary
Today, we completed a cognitive leap from "linear execution" to "dynamic loops":
- The ReAct architecture isn't some mysterious magic; it is simply LLM Node + Tool Node + Cyclic Conditional Edge.
- We successfully built the
Researcherrole for our "AI Content Agency." It is no longer a nerd that only recites training data, but a true researcher that can use search engines, read webpages, and autonomously decide when to deliver the results. - We mastered the accumulation mechanism of the
messagesstate in LangGraph, which is the key to the LLM maintaining memory across multiple loops.
Next Time: Our Researcher is capable now, but it's fighting alone. In our Agency, how does the Planner (Manager) hand tasks over to the Researcher, and how does the Researcher pass the report to the Writer (Lead Author) once done? In Lesson 09, Multi-Agent Collaboration: SubGraphs and Networked Organizations, we will take the Researcher we wrote today and plug it in as a sub-node within our massive agency network.
Geeks, get today's code running! Try adding a "weather lookup" tool to your Researcher and see if it can tell you whether to bring an umbrella tomorrow. See you in the next lesson!