agent-pard0x/react_vs_custom.md
2025-06-25 15:48:07 +02:00

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:

  1. Your use case (log analysis + shell commands) fits perfectly
  2. Current implementation is clean and working
  3. Maintenance overhead is minimal
  4. 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.