Understanding ReAct: Building Resilient AI Agents with LangChain

Nam

Nam Hoang / Feb 24, 2026

8 min read

In the early evolution of Large Language Models (LLMs), developers generally utilized two distinct interaction patterns: Reasoning and Acting. Reasoning, often implemented via Chain-of-Thought prompting, allows a model to "think aloud," which enhances its performance in logic and arithmetic. However, reasoning in isolation is a closed loop; the model is limited to its training data and frequently hallucinates when asked about real-time events.

Conversely, Acting allows models to interact with external tools like search engines, databases, or calculators. While this provides access to fresh data, acting without reasoning is "blind." Without a cognitive framework to decompose complex goals or handle unexpected tool outputs, models often fail to complete multi-step tasks. To solve this, the ReAct framework was developed to combine these two strengths into a single, cohesive loop.

I. The ReAct Breakthrough: Synergy of Thought and Action

Introduced by Yao et al. (2022), ReAct (Reason + Act) proposes a system where the model interleaves reasoning traces with task-specific actions. This synergy allows the model to induce and update plans based on observations, handle exceptions when tools fail, and retrieve factual knowledge to support its internal logic.

A standard ReAct trajectory follows a specific, repeating rhythm:

  1. Thought: The model describes its current state and identifies the next necessary step.
  2. Action: The model executes a specific tool call (e.g., a web search).
  3. Observation: The environment returns the result of that action.

This cycle continues iteratively until the model's internal reasoning concludes that a final answer has been reached.

II. Architecture of LangChain’s createAgent

While the original ReAct research focused on text-based trajectories, LangChain’s createAgent implementation transforms this concept into an industrial State Machine. By utilizing a StateGraph, LangChain orchestrates the flow between the LLM and its tools, ensuring the agent remains predictable and robust.

One of the first steps in this architecture is the construction of the agent's persona via the System Prompt. LangChain uses normalizeSystemPrompt to ensure that core instructions remain at the top of the context window, regardless of whether the model provider expects a simple string or a structured message.

Source: libs/langchain/src/agents/utils.ts

export function normalizeSystemPrompt(
  systemPrompt?: string | SystemMessage,
): SystemMessage {
  if (systemPrompt == null) return new SystemMessage('');
  if (typeof systemPrompt === 'string') {
    return new SystemMessage({
      content: [{ type: 'text', text: systemPrompt }],
    });
  }
  return systemPrompt;
}

III. Tool Binding and the Interleaving Router

For an agent to act, it must understand the "shape" of the tools available to it. The AgentNode (or model_request) dynamically attaches JavaScript tools to the model before every call. Using the #bindTools method, the agent generates the required JSON schema for providers like OpenAI or Anthropic, often forcing structured output to ensure the model provides data in a machine-readable format.

Source: libs/langchain/src/agents/nodes/AgentNode.ts

async #bindTools(model: LanguageModelLike, preparedOptions: ModelRequest | undefined, ...): Promise<Runnable> {
  const allTools = [
    ...(preparedOptions?.tools ?? this.#options.toolClasses),
    ...structuredTools.map((toolStrategy) => toolStrategy.tool),
  ];
  return await bindTools(model, allTools, {
    ...options,
    tool_choice: preparedOptions?.toolChoice || (structuredTools.length > 0 ? "any" : undefined),
  });
}

The core of the framework is the Conditional Router. After the model speaks, the router inspects the AIMessage. If the model requested tool_calls, the agent loops back to the ToolNode. If it is a standard text response, the agent stops.

Source: libs/langchain/src/agents/ReactAgent.ts

#createModelRouter(exitNode: string | typeof END = END) {
  return (state: Record<string, unknown>) => {
    const builtInState = state as unknown as BuiltInState;
    const lastMessage = builtInState.messages.at(-1);

    if (!AIMessage.isInstance(lastMessage) || !lastMessage.tool_calls || lastMessage.tool_calls.length === 0) {
      return exitNode; // Final Answer reached: STOP
    }
    // Tools requested: LOOP to ToolNode
    return lastMessage.tool_calls.map(
      (toolCall) => new Send(TOOLS_NODE_NAME, { ...state, lg_tool_call: toolCall })
    );
  };
}

IV. Planning and Error Handling

Modern ReAct agents often incorporate a "Planning" step, such as the todoListMiddleware. This instructs the model to maintain a structured task list, preventing it from losing track of sub-goals in complex sessions.

Furthermore, LangChain treats tool failures as Observations rather than application crashes. The ToolNode captures errors and returns them as a ToolMessage, allowing the LLM to reason about the failure and attempt a correction in the next turn.

Source: libs/langchain/src/agents/nodes/ToolNode.ts

function defaultHandleToolErrors(
  error: unknown,
  toolCall: ToolCall,
): ToolMessage | undefined {
  return new ToolMessage({
    content: `${error}\n Please fix your mistakes.`,
    tool_call_id: toolCall.id!,
    name: toolCall.name,
  });
}

V. Industrializing ReAct with Middleware

A raw ReAct loop can be fragile in production due to token limits and data safety concerns. LangChain addresses this through a Middleware Architecture—supervisory layers that intercept the agent’s lifecycle.

  1. The Janitor (Summarization): As the conversation history grows, the summarizationMiddleware condensation logic prevents the context window from overflowing by summarizing old messages while preserving essential context.
  2. The Bodyguard (PII): The piiMiddleware scans input for sensitive data (like emails or credit card numbers) and applies strategies to block, redact, or mask the information before it reaches the model.
  3. The Judge (Human-in-the-Loop): For destructive actions, the humanInTheLoopMiddleware uses an interrupt() function to pause the state and wait for a human to approve or reject a tool call.
  4. The Safety Net (Retries): The modelRetryMiddleware implements exponential backoff with jitter, ensuring that temporary network or rate-limit issues do not terminate the agent's process.

Example of PII Redaction Logic:

export function applyStrategy(
  content: string,
  matches: PIIMatch[],
  strategy: PIIStrategy,
  piiType: string,
): string {
  switch (strategy) {
    case 'block':
      throw new PIIDetectionError(piiType, matches);
    case 'redact':
      return applyRedactStrategy(content, matches, piiType); // Replaces with [REDACTED_EMAIL]
    // ...
  }
}

VI. Conclusion: The Orchestrated Agent

By combining core ReAct logic with a robust middleware layer, LangChain's createAgent moves beyond simple prompting tricks. It creates a resilient, multi-layered system capable of handling context growth, data privacy, and infrastructure instability. This orchestrated approach is what enables the deployment of autonomous agents in demanding, real-world environments.