Are you building AI agents that seem smart in a demo but fall apart with real-world complexity? You ask them to perform a multi-step task, and they either hallucinate an answer or get stuck in a loop. The problem isn't the LLM; it's the architecture. Simple, linear chains are dead. To build resilient, autonomous systems that can handle ambiguity and course-correct, you need to master the ReAct (Reason + Act) pattern.
This guide breaks down the architectural shift from linear thinking to cyclical reasoning. We'll explore the core concepts and walk through a self-contained TypeScript implementation of a Customer Support Agent that can dynamically query a database.
The Core Concept: From Linear Chains to Cyclical Reasoning
In the early days of LLM applications, agents were largely linear. They would receive a query, reason about it once, execute a single tool, and produce a final answer. This works for simple tasks, but it breaks down when faced with complex, multi-step problems that require iterative planning and information synthesis.
The ReAct pattern is the architectural solution. It transforms an agent from a simple, one-shot decision-maker into a persistent, reflective process. The core idea is to create a cyclical loop where the agent continuously alternates between internal reasoning and external action. This mirrors human problem-solving: you think, you plan, you look something up, you process the new information, and then you think again.
The Anatomy of a ReAct Loop: Thought, Action, Observation
The ReAct loop cycles through three fundamental states:
- Thought (Reasoning): The agent's internal monologue. It analyzes the current state, reviews history, and formulates a plan. βThe user asked for their subscription status. I don't know it, so I need to query the user database.β
- Action (Execution): Based on its reasoning, the agent executes a tool. This is the "hands-on" part where it interacts with the outside world (e.g.,
call database API). - Observation (Feedback): The result of the action is fed back into the agent's context. This is the new information that informs the next "Thought."
These three steps form the fundamental unit of the ReAct loop, repeated until the agent has gathered enough information to confidently provide a final answer.
The Cyclical Graph Structure: Implementing the Loop in LangGraph
In frameworks like LangGraph, this cyclical pattern is implemented as a graph structure rather than a while loop. This provides clarity, robust state management, and resilience.
- Nodes:
-
agent: The LLM call that generates the next step. -
tools: Functions that perform external actions. -
should_continue: A decision node that checks if the agent is done or needs to take another action.
-
- Edges:
- The critical cyclical edge routes from the
toolsnode back to theagentnode, feeding the observation into the next reasoning step. - The
should_continuenode routes to anENDstate if a final answer is generated.
- The critical cyclical edge routes from the
This graph-based approach makes the agent's workflow explicit and auditable.
Analogy: The Assembly Line vs. The Expert Workshop
- The Linear Agent is an Assembly Line Robot: It performs one perfect, pre-defined task. If a part is missing, it fails. It cannot adapt.
- The ReAct Agent is an Expert Workshop: An artisan building a chair doesn't just start cutting. They plan, cut, inspect (observe), adjust their plan (reason), and cut again. This iterative, reflective process allows them to handle ambiguity and achieve a complex goal.
Why ReAct? Handling Complexity and Ambiguity
The ReAct pattern excels at solving ambiguous, multi-step problems. Consider the query: "What are the key differences between the latest MacBook Pro and the previous generation, and which would you recommend for a video editor on a budget?"
A linear agent fails. A ReAct agent breaks it down:
- Thought: "This is a two-part question. First, find specs for the latest model. Second, find specs for the previous model. Then, compare them based on video editing and budget."
- Action: Search web for "latest MacBook Pro specs".
- Observation: (Receives specs).
- Thought: "Now I need the previous generation specs."
- Action: Search web for "MacBook Pro M2 specs".
- Observation: (Receives specs).
- Thought: "I have both. The key difference is the M3 chip's performance gain. For a budget-conscious video editor, the M2 model is now cheaper and still powerful. I can formulate a recommendation."
Self-Contained TypeScript Example: Customer Support Agent
This example simulates a backend API route using pure TypeScript. It uses a mock database and a mock LLM to demonstrate the ReAct loop without external dependencies.
// ==========================================
// 1. TYPE DEFINITIONS & INTERFACES
// ==========================================
/**
* Represents the state of our agent at any point in the loop.
* This is the "Memory" of the agent.
*/
interface AgentState {
input: string;
conversationHistory: string[];
context: Record<string, any>; // Stores retrieved data (e.g., user subscription)
shouldContinue: boolean;
}
/**
* Defines the structure of a tool the agent can use.
*/
interface Tool {
name: string;
description: string;
execute: (args: any) => Promise<any>;
}
// ==========================================
// 2. MOCK TOOLS (The "Act" Phase)
// ==========================================
/**
* Simulates an internal SaaS API to check user subscription status.
*/
const checkSubscriptionTool: Tool = {
name: "check_subscription",
description: "Use this to check the user's current subscription plan and status.",
execute: async (args: { userId: string }) => {
console.log(`[Tool Execution] Checking subscription for user: ${args.userId}...`);
// Simulate database latency
await new Promise(resolve => setTimeout(resolve, 100));
// Mock Data
if (args.userId === "user_123") {
return { plan: "Pro", status: "Active", expiresAt: "2024-12-31" };
}
return { plan: "Free", status: "Expired", expiresAt: "2023-01-01" };
}
};
const tools = [checkSubscriptionTool];
// ==========================================
// 3. MOCK LLM (The "Reason" Phase)
// ==========================================
/**
* Simulates an LLM (like GPT-4) that decides which tool to use.
* In production, this calls an actual LLM API with a structured output schema.
*
* @returns A string representing the LLM's decision: "TOOL:tool_name:args" or "FINAL:answer".
*/
async function mockLLMReasoning(state: AgentState): Promise<string> {
console.log("\n[LLM Reasoning] Analyzing state...");
if (Object.keys(state.context).length === 0) {
// We have no info yet. Let's ask the tool for data.
console.log("[LLM Reasoning] No context found. Deciding to call 'check_subscription'.");
return `TOOL:check_subscription:{"userId": "user_123"}`;
}
// We have info. Let's formulate an answer.
if (state.context.plan === "Pro") {
console.log("[LLM Reasoning] User is Pro. Formulating final answer.");
return `FINAL:The user has an active Pro subscription valid until ${state.context.expiresAt}.`;
} else {
console.log("[LLM Reasoning] User is not Pro. Formulating final answer.");
return `FINAL:The user's subscription is ${state.context.status}. Please upgrade to Pro.`;
}
}
// ==========================================
// 4. THE REACT AGENT (The Orchestrator)
// ==========================================
/**
* The core ReAct loop implementation.
*/
class ReActAgent {
private state: AgentState;
constructor(initialInput: string) {
this.state = {
input: initialInput,
conversationHistory: [initialInput],
context: {},
shouldContinue: true
};
}
async run(): Promise<string> {
console.log("π Starting ReAct Agent Loop...");
// Loop guard to prevent infinite execution
let steps = 0;
const MAX_STEPS = 5;
while (this.state.shouldContinue && steps < MAX_STEPS) {
steps++;
console.log(`\n--- Step ${steps} ---`);
// 1. REASON: Ask LLM what to do
const llmDecision = await mockLLMReasoning(this.state);
// 2. ACT & OBSERVE: Parse decision and execute
if (llmDecision.startsWith("TOOL:")) {
const [_, toolName, argsStr] = llmDecision.split(":");
const tool = tools.find(t => t.name === toolName);
if (tool) {
const args = JSON.parse(argsStr);
const result = await tool.execute(args);
// Update State with Observation
this.state.context = result;
this.state.conversationHistory.push(`Tool Result: ${JSON.stringify(result)}`);
} else {
throw new Error(`Tool ${toolName} not found.`);
}
}
else if (llmDecision.startsWith("FINAL:")) {
const answer = llmDecision.split(":")[1];
this.state.conversationHistory.push(`Final Answer: ${answer}`);
this.state.shouldContinue = false;
console.log("β
Task Complete. Returning final answer.");
return answer;
}
}
return "Error: Agent hit maximum step limit.";
}
}
// ==========================================
// 5. EXECUTION (Simulating a Web App Request)
// ==========================================
async function main() {
const userQuery = "What is the status of my subscription?";
const agent = new ReActAgent(userQuery);
const result = await agent.run();
console.log("\n========================================");
console.log("Final Output to Client:", result);
console.log("========================================");
}
main().catch(console.error);
Code Breakdown
- State Management: The
AgentStateinterface acts as the agent's memory. In a production SaaS app, this would likely be stored in Redis or a database session, allowing the agent to be stateless between requests while maintaining context within a session. - The Loop: The
whileloop in theReActAgentclass is the engine. It drives the cyclical nature of Reasoning and Acting. - Safety: The
MAX_STEPSguard is critical. Without it, a buggy LLM response could trap your agent in an infinite loop, racking up API costs. - Separation of Concerns: The
mockLLMReasoningfunction is separate from theReActAgent. In a real application, you'd swap this out for a call to an LLM provider (like OpenAI) using a library like Zod for structured output validation.
Common Pitfalls to Avoid
When implementing ReAct patterns in production, watch out for these issues:
- Infinite Loops: The agent gets stuck repeating the same thought or failed action. Solution: Always implement a
MAX_STEPScounter. - State Bloat: Storing the entire conversation history in the context window forever. Solution: Summarize old interactions or use a sliding window of recent messages.
- Poor Tool Descriptions: The LLM doesn't know when or how to use a tool. Solution: Be extremely descriptive in your tool definitions, including examples of valid inputs.
Conclusion
The ReAct pattern is more than a technical implementation; it's a paradigm shift. By moving from linear chains to cyclical, reflective loops, you give your agents the ability to handle the ambiguity and complexity of real-world tasks. Whether you're building a customer support bot or a data analysis engine, the principles of Reason, Act, and Observe are the foundation of robust, scalable AI systems.
The concepts and code demonstrated here are drawn directly from the comprehensive roadmap laid out in the book Autonomous Agents. Building Multi-Agent Systems and Workflows with LangGraph.js Amazon Link of the AI with JavaScript & TypeScript Series.
The ebook is also on Leanpub.com: https://leanpub.com/JSTypescriptAutonomousAgents.
Top comments (0)