9.5 KiB
ReAct Agent vs Custom StateGraph: Architectural Decision Guide
This document explores the two main approaches for building LangGraph agents: using the prebuilt create_react_agent
vs implementing a custom StateGraph
.
TL;DR Recommendation
Use create_react_agent
for most use cases. Only migrate to custom StateGraph
when you hit specific limitations of the ReAct pattern.
Option 1: create_react_agent
(Current Implementation)
What it is
# Simple 5-line agent creation
llm = init_chat_model("openai:gpt-4o-mini")
tools = [shell_tool, analyze_log_file]
agent = create_react_agent(llm, tools, prompt=system_prompt)
Under the Hood
create_react_agent
uses a predefined StateGraph
with this structure:
START → agent → tools → agent → END
↑________________↓
agent
node: LLM reasoning (decides what to do)tools
node: Tool execution (acting)- Conditional loop: Continues until final response
Advantages ✅
Simplicity & Speed
- Minimal code to get started
- Battle-tested ReAct pattern
- Automatic reasoning/acting cycles
Maintenance
- Automatic updates with LangGraph improvements
- Less code to debug and maintain
- Well-documented pattern
Perfect for Standard Use Cases
- Tool-based interactions
- Conversational interfaces
- Analysis workflows
- System administration tasks
Limitations ⚠️
- Fixed ReAct pattern only
- Limited state management
- No custom routing logic
- No parallel tool execution
- No complex workflow orchestration
Option 2: Custom StateGraph Implementation
What it looks like
from typing import TypedDict, Annotated, Literal
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langchain_core.messages import BaseMessage
class AgentState(TypedDict):
messages: Annotated[list[BaseMessage], add_messages]
current_task: str # "log_analysis", "shell_command", "general"
log_context: dict # Remember previous analyses
safety_mode: bool # Control dangerous commands
def classify_request(state: AgentState) -> AgentState:
"""Classify user request type"""
last_message = state["messages"][-1].content.lower()
if any(word in last_message for word in ["log", "analyze", "error", "pattern"]):
state["current_task"] = "log_analysis"
elif any(word in last_message for word in ["command", "shell", "run", "execute"]):
state["current_task"] = "shell_command"
else:
state["current_task"] = "general"
return state
def route_request(state: AgentState) -> Literal["log_analyzer", "shell_executor", "general_chat"]:
"""Route to appropriate node based on request type"""
return {
"log_analysis": "log_analyzer",
"shell_command": "shell_executor",
"general": "general_chat"
}[state["current_task"]]
def analyze_logs_node(state: AgentState) -> AgentState:
"""Specialized node for log analysis"""
llm = init_chat_model("openai:gpt-4o-mini")
# Custom logic for log analysis
# - Parallel file processing
# - Context from previous analyses
# - Specialized prompting
prompt = f"""You are a log analysis expert.
Previous context: {state.get("log_context", {})}
Use analyze_log_file tool for the requested analysis.
"""
response = llm.invoke([HumanMessage(content=prompt)] + state["messages"][-3:])
state["messages"].append(response)
# Update context for future analyses
state["log_context"]["last_analysis"] = "completed"
return state
def execute_shell_node(state: AgentState) -> AgentState:
"""Specialized node for shell commands with safety checks"""
llm = init_chat_model("openai:gpt-4o-mini")
# Safety validation before execution
dangerous_commands = ["rm -rf", "sudo rm", "format", "dd if="]
last_message = state["messages"][-1].content.lower()
if any(cmd in last_message for cmd in dangerous_commands):
state["messages"].append(
AIMessage(content="⚠️ Potentially dangerous command detected. Please confirm.")
)
state["safety_mode"] = True
return state
# Normal execution with ShellTool
# Custom logic for command validation and execution
return state
def general_chat_node(state: AgentState) -> AgentState:
"""Handle general conversation"""
llm = init_chat_model("openai:gpt-4o-mini")
prompt = """You are a helpful system administration assistant.
Provide guidance and suggestions for system debugging tasks.
"""
response = llm.invoke([HumanMessage(content=prompt)] + state["messages"][-5:])
state["messages"].append(response)
return state
def create_advanced_agent():
"""Create custom agent with StateGraph"""
# Define workflow
workflow = StateGraph(AgentState)
# Add nodes
workflow.add_node("classifier", classify_request)
workflow.add_node("log_analyzer", analyze_logs_node)
workflow.add_node("shell_executor", execute_shell_node)
workflow.add_node("general_chat", general_chat_node)
# Define edges
workflow.add_edge(START, "classifier")
workflow.add_conditional_edges(
"classifier",
route_request,
{
"log_analyzer": "log_analyzer",
"shell_executor": "shell_executor",
"general_chat": "general_chat"
}
)
# All terminal nodes lead to END
workflow.add_edge("log_analyzer", END)
workflow.add_edge("shell_executor", END)
workflow.add_edge("general_chat", END)
return workflow.compile()
Advantages ✅
Complete Control
- Custom business logic
- Complex state management
- Advanced routing and validation
- Parallel processing capabilities
Specialized Workflows
- Different handling per task type
- Memory between interactions
- Safety checks and validation
- Custom error handling
Performance Optimization
- Optimized tool selection
- Reduced unnecessary LLM calls
- Parallel execution where possible
Disadvantages ❌
Complexity
- 50+ lines vs 5 lines
- More potential bugs
- Custom maintenance required
Development Time
- Slower initial development
- More testing needed
- Complex debugging
Comparison Matrix
Aspect | create_react_agent |
Custom StateGraph |
---|---|---|
Lines of Code | ~5 | ~50+ |
Development Time | Minutes | Hours/Days |
Flexibility | ReAct pattern only | Complete freedom |
Maintenance | Automatic | Manual |
Performance | Good, optimized | Depends on implementation |
Debugging | Limited visibility | Full control |
State Management | Basic messages | Rich custom state |
Routing Logic | Tool-based only | Custom conditional |
Parallel Execution | No | Yes |
Safety Checks | Tool-level only | Custom validation |
Use Cases Coverage | 80% | 100% |
When to Use Each Approach
Stick with create_react_agent
when:
✅ Tool-based interactions (your current use case) ✅ Standard conversational AI ✅ Rapid prototyping ✅ Simple reasoning/acting cycles ✅ Maintenance is a priority ✅ Team has limited LangGraph experience
Migrate to Custom StateGraph
when:
🔄 Complex business logic required 🔄 Multi-step workflows with different paths 🔄 Advanced state management needed 🔄 Parallel processing requirements 🔄 Custom validation/safety logic 🔄 Performance optimization critical 🔄 Specialized routing based on context
Migration Strategy
If you decide to eventually migrate to custom StateGraph:
Phase 1: Enhance Current Implementation
# Add more sophisticated tools to your current setup
def create_enhanced_react_agent():
tools = [
shell_tool,
analyze_log_file,
safety_validator_tool, # New: safety checks
parallel_log_analyzer, # New: batch processing
context_manager_tool # New: conversation context
]
return create_react_agent(llm, tools, enhanced_prompt)
Phase 2: Hybrid Approach
# Use create_react_agent for some tasks, custom StateGraph for others
def create_hybrid_agent():
# Route complex workflows to custom graph
# Keep simple interactions with ReAct agent
pass
Phase 3: Full Custom Implementation
- Implement complete StateGraph when requirements demand it
Recommendation for Your Project
Keep create_react_agent
for now because:
- ✅ Your use case (log analysis + shell commands) fits perfectly
- ✅ Current implementation is clean and working
- ✅ Maintenance overhead is minimal
- ✅ Team can focus on improving tools rather than framework
Consider custom StateGraph later if you need:
- Advanced workflow orchestration
- Complex state management between analyses
- Parallel processing of multiple log files
- Sophisticated safety validation
- Performance optimization for large-scale deployments
Conclusion
Your current create_react_agent
implementation is excellent for an MVP and likely covers 80% of system administration use cases. The ReAct pattern provides a solid foundation for tool-based AI interactions.
Only migrate to custom StateGraph when you have specific requirements that the ReAct pattern cannot handle efficiently. Focus on enhancing your tools (log_analyzer.py
, additional custom tools) rather than changing the underlying agent framework.
The best architecture is the one that solves your current problems without overengineering for hypothetical future needs.