Langchain  

Self-Correcting AI Agents Using Reflection Patterns in LangGraph

In the rapidly evolving landscape of Generative AI, Large Language Models (LLMs) have proven to be incredibly capable. However, when it comes to mission-critical, real-world applications—like financial transactions—standard "prompt-in, text-out" LLM chains fall short. They hallucinate, ignore formatting constraints, and fail to recover from logical errors.

Enter Self-Correcting AI Agents powered by Reflection Patterns.

In this end-to-end guide, we will explore how to build a robust, self-correcting agent using LangGraph. To make this practical, we will build a Smart Payment Routing Agent capable of handling real-world payment intents like "Pay to Mobile Number" and "Scan any QR Code", gracefully handling user typos and LLM extraction errors.

The Problem: Why Standard LLMs Fail in Fintech

Imagine a user interacting with a banking chatbot:

User: "Scan the QR code to pay 500 to john@paytm"

A standard LLM might extract this into a JSON payload:

{
  "payment_method": "qr_code",
  "target": "john@paytm",
  "amount": 500
}

The Catch: The downstream payment gateway requires a strict UPI URI format (e.g., upi://pay?pa=john@paytm&am=500). The LLM didn't format it correctly. In a standard chain, this bad JSON is sent to the API, resulting in a failed transaction and a frustrated user.

The Solution: The Reflection Pattern

The Reflection Pattern mimics human problem-solving. Instead of just generating an answer, the AI:

  1. Generates an initial solution.

  2. Evaluates (Reflects on) its own solution against strict rules.

  3. Corrects the solution based on the evaluation.

  4. Repeats until the solution is valid.

LangGraph is the perfect framework for this because it allows us to build cyclic graphs (loops), enabling the agent to "think again" if it makes a mistake.

42

Real-World Use Case: Smart Payment Routing Agent

Let's build an agent that processes natural language payment requests. It must support two methods:

  1. Pay to Mobile Number: Requires a valid 10-digit numeric string.

  2. Scan QR Code: Requires a valid UPI URI string (must start with upi://).

Architecture of the Agent

Our LangGraph workflow will consist of three main nodes:

  1. Extractor (Generator): Parses user input into a structured PaymentIntent.

  2. Validator (Critic): A deterministic Python function that checks if the extracted data meets strict payment gateway rules.

  3. Reflector (Corrector): An LLM that takes the validation errors and rewrites the PaymentIntent.

Step-by-Step Implementation

Prerequisites

pip install langgraph langchain-openai pydantic

Step 1: Define the State and Pydantic Models

First, we define the strict schema for our payment data and the state of our LangGraph.

from typing import TypedDict, Annotated, List, Literal, Optional
from pydantic import BaseModel, Field
import operator

# 1. Define the strict schema for the Payment Gateway
class PaymentIntent(BaseModel):
    method: Literal["mobile", "qr_code"] = Field(description="Payment method")
    target: str = Field(description="Mobile number or UPI QR string")
    amount: float = Field(description="Amount to pay")

# 2. Define the LangGraph State
class AgentState(TypedDict):
    messages: Annotated[List[str], operator.add] # User input
    payment_intent: Optional[dict]               # Extracted JSON
    validation_errors: List[str]                 # Errors found by Validator
    retries: int                                 # Loop prevention

Step 2: The Extractor Node (Generator)

This node uses an LLM to convert natural language into our PaymentIntent schema.

from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model="gpt-4o", temperature=0)

def extract_payment_intent(state: AgentState):
    """Generates the initial payment intent from user input."""
    user_input = state["messages"][-1]
    
    structured_llm = llm.with_structured_output(PaymentIntent)
    
    prompt = f"""
    Extract the payment details from the user's request.
    User Request: {user_input}
    """
    
    intent = structured_llm.invoke(prompt)
    
    # Update state
    state["payment_intent"] = intent.model_dump()
    state["validation_errors"] = [] # Reset errors
    return state

Step 3: The Validator Node (Critic)

Crucial Best Practice: Do not use an LLM to validate strict business logic. Use deterministic Python code. This node checks if the mobile number is 10 digits or if the QR code is a valid UPI URI.

import re

def validate_payment_intent(state: AgentState):
    """Validates the extracted payment intent against business rules."""
    intent = state["payment_intent"]
    errors = []
    
    # Rule 1: Validate Mobile Number
    if intent["method"] == "mobile":
        # Remove spaces/dashes and check if it's exactly 10 digits
        clean_number = re.sub(r'\D', '', intent["target"])
        if len(clean_number) != 10:
            errors.append(f"Invalid mobile number '{intent['target']}'. Must be exactly 10 digits.")
        else:
            # Auto-correct the formatting for the gateway
            intent["target"] = clean_number 

    # Rule 2: Validate QR Code (UPI URI)
    elif intent["method"] == "qr_code":
        if not intent["target"].startswith("upi://"):
            errors.append(f"Invalid QR target '{intent['target']}'. Must be a valid UPI URI starting with 'upi://pay?pa=...'")
            
    # Rule 3: Validate Amount
    if intent["amount"] <= 0:
        errors.append("Amount must be greater than 0.")

    state["validation_errors"] = errors
    state["payment_intent"] = intent # Save auto-corrections if any
    return state

Step 4: The Reflector Node (Corrector)

If the Validator finds errors, this node uses the LLM to fix them. It feeds the original user input, the bad extraction, and the specific errors back to the LLM.

def reflect_and_correct(state: AgentState):
    """Uses LLM to correct the payment intent based on validation errors."""
    user_input = state["messages"][-1]
    current_intent = state["payment_intent"]
    errors = state["validation_errors"]
    
    prompt = f"""
    You are a payment data correction assistant. 
    The previous extraction was invalid.
    
    Original User Input: {user_input}
    Current Invalid Extraction: {current_intent}
    Validation Errors: {errors}
    
    Please correct the extraction to strictly resolve the validation errors.
    """
    
    structured_llm = llm.with_structured_output(PaymentIntent)
    corrected_intent = structured_llm.invoke(prompt)
    
    state["payment_intent"] = corrected_intent.model_dump()
    state["retries"] += 1
    return state

Step 5: Building the LangGraph Workflow

Now, we wire the nodes together using conditional edges to create the reflection loop.

from langgraph.graph import StateGraph, START, END

def route_after_validation(state: AgentState):
    """Decides whether to correct the data or proceed to execution."""
    if state["validation_errors"]:
        if state["retries"] >= 3: # Prevent infinite loops
            return "fail"
        return "correct"
    return "execute"

# Initialize the graph
workflow = StateGraph(AgentState)

# Add nodes
workflow.add_node("extract", extract_payment_intent)
workflow.add_node("validate", validate_payment_intent)
workflow.add_node("reflect", reflect_and_correct)

# Define edges
workflow.add_edge(START, "extract")
workflow.add_edge("extract", "validate")

# Conditional edge: The core of the Reflection Pattern
workflow.add_conditional_edges(
    "validate",
    route_after_validation,
    {
        "correct": "reflect",
        "execute": END,
        "fail": END
    }
)

# Loop back: After reflection, we must validate AGAIN
workflow.add_edge("reflect", "validate")

# Compile the graph
app = workflow.compile()

Execution: Seeing the Self-Correction in Action

Let's test our agent with a tricky user input that will intentionally trigger the LLM to make a mistake, forcing the reflection loop to activate.

# Simulate user input
# The user provides a UPI ID, but the LLM might just extract the ID without the 'upi://' prefix
user_query = "Scan the QR code to pay 500 to john@paytm"

initial_state = {
    "messages": [user_query],
    "payment_intent": None,
    "validation_errors": [],
    "retries": 0
}

# Run the agent
final_state = app.invoke(initial_state)

print("--- FINAL RESULT ---")
if final_state["validation_errors"]:
    print(f" Failed after {final_state['retries']} retries. Errors: {final_state['validation_errors']}")
else:
    print(f" Success! Processed in {final_state['retries']} retries.")
    print(f"Payload sent to Gateway: {final_state['payment_intent']}")

What happens under the hood?

  1. Extract: The LLM extracts {"method": "qr_code", "target": "john@paytm", "amount": 500}.

  2. Validate: The Python validator checks the target. It doesn't start with upi://. It adds an error: "Invalid QR target... Must start with 'upi://'".

  3. Route: The graph sees errors and routes to reflect.

  4. Reflect: The LLM reads the error, realizes it missed the UPI URI schema, and generates: {"method": "qr_code", "target": "upi://pay?pa=john@paytm&am=500", "amount": 500}.

  5. Validate (Again): The validator checks the new target. It starts with upi://. No errors!

  6. Route: The graph routes to END (Execution).

Output:

--- FINAL RESULT ---
 Success! Processed in 1 retries.
Payload sent to Gateway: {'method': 'qr_code', 'target': 'upi://pay?pa=john@paytm&am=500', 'amount': 500.0}

When moving this from a prototype to a production fintech environment, keep these rules in mind:

  1. Always Cap Retries: Notice the state["retries"] >= 3 check in the routing logic. LLMs can get stuck in "apology loops" where they continuously fail to fix an error. Always implement a hard break and fallback to a human agent or a clarifying question.

  2. Deterministic Validators: Never use an LLM to validate its own work. As shown in Step 3, use strict Python/Regex for validation. LLMs are for generation and semantic correction; code is for validation.

  3. Structured Outputs: Always use Pydantic models with with_structured_output(). This guarantees the LLM returns valid JSON, eliminating syntax errors before the reflection loop even begins.

  4. Human-in-the-Loop (HITL): For high-value transactions (e.g., > $10,000), add a node before END that pauses the graph, sends a confirmation prompt to the user's mobile app, and waits for a webhook callback to resume the graph.

Conclusion

Building AI agents for critical workflows like payments requires moving beyond simple prompt chains. By implementing the Reflection Pattern in LangGraph, we create systems that can acknowledge their own mistakes, evaluate them against strict business logic, and self-correct without human intervention.

Whether routing a payment to a mobile number or parsing a complex QR code, self-correcting agents bridge the gap between the probabilistic nature of LLMs and the deterministic requirements of enterprise software.