Today, before the article, let me introduce our new site β currently in beta:
π Free TypeScript & AI Engineering Masterclass
This article is part of a structured roadmap designed to take you from TypeScript foundations to building production-ready AI applications. I have made the entire series available on a dedicated, lightning-fast platform:
π Access the TypeScript & AI Series on Programming Central
Why learn here?
- Zero Friction: No signup, no email required, and no "waitlists." Everything is instantly accessible for free.
- Structured Learning: Use the sidebar menu on the left to browse the full curriculum. Chapters flow logically from core concepts to real-world AI implementation.
- Engineering First: We don't just show syntax; we dive into practical examples and identify common pitfalls that senior developers avoid.
- Interactive Quizzes: At the end of each chapter, you can test your knowledge with our custom quiz engine.
How the quizzes work:
The system generates a random set of engineering challenges for every attempt. You get instant feedback and, most importantly, a detailed architectural explanation for every correct and incorrect choice. Itβs designed to ensure you master the logic of AI engineering, not just the code.
Let me know what you think in the comments below.
Below you will find today's article.
Youβve built an AI agent. It takes a prompt, generates a response, and... hopes for the best.
But in a production SaaS environment, "hoping" isn't a strategy. When your agent hallucinates facts, writes buggy code, or produces vague summaries, your users don't care about your underlying LLMβthey just see a broken product.
The solution isn't just a better prompt; it's a better architecture. Enter the Reflection Pattern: a self-correction loop that transforms a fragile, one-shot agent into a robust, iterative system.
The Core Concept: The Reflection Pattern as a Quality Gate
In the landscape of autonomous agents, the ability to generate a response is only half the battle. The other half is ensuring that response is correct, coherent, and contextually appropriate.
The Reflection Pattern addresses this by introducing a self-correction loop. Unlike a linear agent that executes a task and immediately returns the result, a reflection agent incorporates a critical evaluation step. It treats its own output not as a final product, but as a draft subject to scrutiny.
This pattern fundamentally shifts the agent's architecture from a simple "generate-and-return" pipeline to a "generate-critique-refine" cycle.
Why You Need This: Mitigating Hallucination and Enhancing Reliability
The primary motivation for the Reflection Pattern is the inherent non-determinism of Large Language Models (LLMs). LLMs can produce "hallucinations" (factually incorrect information), logical inconsistencies, or outputs that simply don't align with the user's intent.
Consider a web development analogy: Unit Testing and Continuous Integration (CI).
When a developer writes code, they don't just push it to production. They write unit tests. The Reflection Pattern is the agent's equivalent of a CI pipeline.
- Initial Generation: The "code commit."
- Reflection Step: The "automated test suite."
If the tests fail (the critique finds errors), the code must be fixed (regenerated) before it can be deployed (returned to the user). This is crucial for reliability. In a multi-agent system, if one agent produces faulty output, that error propagates downstream, causing a cascade of failures.
How It Works: The Cyclical Graph Structure
The Reflection Pattern is implemented using a Cyclical Graph Structure. This is a LangGraph design where an edge points back to a previously executed node, creating a loop that iterates until a termination condition is met.
The Nodes and Edges
- Generator Node (Drafting): Produces the first draft.
- Evaluator/Critic Node (Reviewing): Evaluates the draft against specific criteria (factual accuracy, format, logic).
- Router Node (Decision Making): Checks the Evaluator's output. If the draft fails, the graph loops back to the Generator.
- Final Output Node (Deploying): Returns the validated response.
Visualizing the Loop
The cyclical nature is what makes this pattern powerful. It's not a one-shot attempt; it's an iterative refinement process.
graph TD
A[Generator Node] -->|Draft| B[Evaluator/Critic Node]
B -->|Critique & Pass/Fail| C{Router}
C -->|Fail| A
C -->|Pass| D[Final Output Node]
Analogy: The Microservices Architecture
To understand the modularity of the Reflection Pattern, think of Microservices. You don't have one giant monolithic server; you have specialized services.
- Generator Agent (Microservice A): Its sole job is to generate text. It doesn't worry about correctness.
- Evaluator Agent (Microservice B): Its sole job is to critique text. It is trained to identify errors.
- Orchestrator (API Gateway): The LangGraph acts as the orchestrator, routing requests between these microservices.
This separation of concerns is key. The Generator doesn't need the cognitive load of self-evaluation. The Evaluator can be a smaller, more efficient model fine-tuned for critique, reducing computational cost.
Connecting to Previous Concepts: The Supervisor Node
In Book 3, Chapter 15: Supervisor Nodes and Hierarchical Control, we introduced a Supervisor managing a Worker Agent Pool.
The Reflection Pattern is a specialized, recursive instance of this hierarchy. The "Supervisor" (Router) routes a task to a "Generator Worker." Instead of accepting the result immediately, it routes that result to an "Evaluator Worker" for review. The Evaluator's output is fed back to the Supervisor, which decides: Loop back or Pass forward.
This demonstrates how the Supervisor pattern can be extended to create complex, iterative workflows that enable self-correction.
Under the Hood: Mechanics of Iterative Refinement
The power lies in the loop. Each iteration provides an opportunity for improvement.
- Initial Generation: The Generator produces a draft based on the prompt.
- Critique Generation: The Evaluator analyzes the draft. The critique is usually structured (JSON) containing:
- Error Type (e.g., "Factual Inaccuracy")
- Location (e.g., "Sentence 3")
- Suggested Correction
- Regeneration with Context: If the draft fails, the Generator is invoked again. Crucially, it receives the critique.
- Prompt: "Your previous draft contained errors: [Critique]. Regenerate addressing these issues."
- Termination: The loop stops when the draft passes the quality gate or a maximum iteration limit is reached (to prevent infinite loops).
Quality Gates: Defining "Good Enough"
You must define what constitutes a "pass." The Quality Gate is a set of criteria the draft must meet.
- Factual Accuracy: Cross-reference claims with a trusted knowledge base.
- Code Correctness: Run the code through a linter or unit test suite.
- Adherence to Format: Validate JSON syntax against a schema.
- Coherence and Tone: Ensure the response is polite and brand-appropriate.
Practical Implementation: The SaaS Code Review Agent
Let's look at a TypeScript implementation simulating a SaaS tool where a "Junior Dev" generates code and a "Senior Dev" critiques it. This uses a Cyclical Graph Structure to implement the iterative refinement loop.
The Code Example
This script is fully self-contained. It simulates LLM calls using mock functions to demonstrate state management, conditional routing, and iterative loops.
/**
* Reflection Agent: Self-Correction Loop
* Context: SaaS AI Assistant for generating task summaries.
* Objective: Ensure the generated summary is not "too vague" before finalizing.
*/
// 1. Define the State Interface
interface AgentState {
request: string; // User's original request
draftSummary: string; // Current draft
critique: string; // Feedback from reflection
shouldRefine: boolean; // Flag for conditional edge
finalOutput: string; // Final response
}
// 2. Mock LLM Function (Simulating an API Call)
const mockLLMCall = async (prompt: string): Promise<string> => {
await new Promise(resolve => setTimeout(resolve, 100)); // Simulate latency
if (prompt.includes("Critique the following summary")) {
// The "Reflection" LLM call
if (prompt.includes("Buy milk and eggs")) {
return "The summary is too vague. It lacks context on why the items are needed or the deadline.";
}
return "The summary is clear and actionable.";
} else {
// The "Generation" LLM call
if (prompt.includes("Buy milk and eggs for dinner tonight")) {
return "Task: Purchase groceries. Items: Milk, Eggs. Deadline: Tonight.";
}
// Simulating a vague initial generation
return "Buy milk and eggs";
}
};
// Node A: Generate Initial Draft
async function generateDraft(state: AgentState): Promise<AgentState> {
console.log("π€ [Node] Generating initial draft...");
const prompt = `Summarize this request concisely: "${state.request}"`;
const draft = await mockLLMCall(prompt);
return { ...state, draftSummary: draft };
}
// Node B: Reflect (Critique)
async function reflectOnDraft(state: AgentState): Promise<AgentState> {
console.log("π [Node] Reflecting on draft quality...");
const prompt = `Critique the following summary for clarity and specificity: "${state.draftSummary}"`;
const critique = await mockLLMCall(prompt);
const needsRefinement = critique.toLowerCase().includes("vague");
return { ...state, critique: critique, shouldRefine: needsRefinement };
}
// Node C: Refine (Regenerate)
async function refineDraft(state: AgentState): Promise<AgentState> {
console.log("β¨ [Node] Refining draft based on critique...");
const prompt = `Original Request: "${state.request}". Previous Summary: "${state.draftSummary}". Critique: "${state.critique}". Please rewrite the summary to address the critique.`;
const refinedDraft = await mockLLMCall(prompt);
return { ...state, draftSummary: refinedDraft, critique: "" };
}
// Node D: Finalize Output
async function finalizeOutput(state: AgentState): Promise<AgentState> {
console.log("β
[Node] Finalizing output...");
return { ...state, finalOutput: `Final Summary: ${state.draftSummary}` };
}
// Conditional Edge: Router
function router(state: AgentState): "refine" | "finalize" {
return state.shouldRefine ? "refine" : "finalize";
}
// Main Execution Loop (Simulating LangGraph)
async function runReflectionAgent(userRequest: string) {
let state: AgentState = {
request: userRequest,
draftSummary: "",
critique: "",
shouldRefine: false,
finalOutput: "",
};
console.log(`\n--- Starting Session: "${userRequest}" ---\n`);
// 1. Generate
state = await generateDraft(state);
console.log(` Draft: "${state.draftSummary}"`);
// 2. Reflect
state = await reflectOnDraft(state);
console.log(` Critique: "${state.critique}"`);
console.log(` Decision: ${state.shouldRefine ? "Refine" : "Finalize"}`);
// 3. Conditional Loop
const decision = router(state);
if (decision === "refine") {
state = await refineDraft(state);
console.log(` Refined Draft: "${state.draftSummary}"`);
state = await reflectOnDraft(state); // Verify the fix
}
// 4. Finalize
state = await finalizeOutput(state);
console.log(`\n--- Final Result ---\n${state.finalOutput}\n---------------------\n`);
}
// Run Scenario
runReflectionAgent("Buy milk and eggs for dinner tonight");
Line-by-Line Explanation
- State Definition (
interface AgentState): Defines the data structure.shouldRefineis the critical boolean flag used by the conditional edge (router). - Mock LLM (
mockLLMCall): Simulates API calls. It checks the prompt string to return different responses, allowing us to test the logic without a real API key. -
generateDraft: The entry point. It formats the user input and awaits the LLM response. -
reflectOnDraft: The "Reflection" node. It sends the previousdraftSummaryto the LLM with a critique instruction. It parses the response to setshouldRefine. -
refineDraft: Triggered only if the router decides to refine. It provides the LLM with the original request, the failed draft, and the critique to guide the regeneration. -
router: Implements the critique-based routing. It looks atshouldRefineto decide whether to loop back or move forward.
Common Pitfalls in TypeScript & LangGraph.js
When implementing this in a production SaaS (e.g., Vercel, Node.js), watch out for these issues:
1. Async/Await Loops and Timeouts
Reflection agents run multiple LLM calls in a single request. Serverless platforms (like Vercel) often have default timeouts of 10 seconds.
- Risk: The function times out mid-loop.
- Fix: Optimize iterations (max 2-3 loops) and use
Promise.racefor custom timeouts.
2. Infinite Loops
If your reflect node has a bug where it always returns shouldRefine: true, the graph loops infinitely.
- Fix: Implement a
max_iterationscounter in your state. In the router, checkif (state.iteration >= 3) force finalize.
3. State Mutability
TypeScript objects are mutable. Modifying state directly can cause race conditions.
- Fix: Always return a new object spread from the previous state (
return { ...state, draftSummary: draft }). This ensures functional purity.
Conclusion: Building Trustworthy Autonomous Systems
The Reflection Pattern is a cornerstone of building reliable autonomous agents. By moving beyond a single-shot generation model and embracing an iterative, self-critical approach, we significantly enhance output quality and accuracy.
The cyclical graph structure, inspired by proven software engineering principles like CI/CD and microservices, provides a robust framework for implementation. As we build more complex multi-agent systems, the ability to self-correct will be the defining factor between a demo and a dependable SaaS product.
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)