Building a Self-Correcting AI Agent with Reflection Loops in Python A developer detailed a Python implementation of a self-correcting AI agent using reflection loops, where the model critiques its own output and retries until it meets a quality bar. The approach leverages the asymmetry that critiquing is easier than generating, and includes a validator for structured outputs like JSON schemas. The code wraps LLM calls in a loop with a separate validator function, enabling automated error correction without human intervention. Language models produce wrong answers. Not occasionally — regularly. When you deploy an LLM to automate tasks, you need a way to catch and fix those errors without human intervention. Reflection loops are one practical answer: the model checks its own output, flags problems, and retries until it meets a quality bar you define in code. A reflection loop is a control flow pattern where an agent runs a task, evaluates the result, then decides whether to retry. The simplest form is a two-step cycle: generate and critique . The critique step is usually a second LLM call with a different prompt, but it can also be a deterministic check — a JSON parser, a unit test runner, or a schema validator. The key insight is that critique is easier than generation. A language model will often miss edge cases in a first pass but correctly identify them when asked “what is wrong with this output?” This asymmetry is what makes the pattern work at all. Here is a Python class that wraps any LLM call in a reflection loop: python import json from typing import Any, Callable import httpx MAX ITERATIONS = 4 def call llm prompt: str, system: str = "" - str: response = httpx.post "https://api.example-llm.com/v1/messages", headers={"x-api-key": "YOUR KEY", "Content-Type": "application/json"}, json={ "model": "your-model", "max tokens": 1024, "system": system, "messages": {"role": "user", "content": prompt} , }, timeout=30, response.raise for status return response.json "content" 0 "text" class ReflectionAgent: def init self, task prompt: str, critique prompt: str, validator: Callable str , tuple bool, str , : self.task prompt = task prompt self.critique prompt = critique prompt self.validator = validator def run self, user input: str - dict str, Any : history: list dict = attempt = 0 while attempt < MAX ITERATIONS: attempt += 1 context = f"User input: {user input}" if history: last = history -1 context += f"\n\nPrevious attempt:\n{last 'output' }" f"\nCritique:\n{last 'critique' }" output = call llm f"{self.task prompt}\n\n{context}" ok, critique = self.validator output history.append {"attempt": attempt, "output": output, "critique": critique, "passed": ok} if ok: return {"success": True, "output": output, "iterations": attempt, "history": history} llm critique = call llm f"{self.critique prompt}\n\nOutput to critique:\n{output}", system="Be specific about what is wrong. Do not repeat the corrected version.", history -1 "critique" = llm critique return {"success": False, "output": history -1 "output" , "iterations": attempt, "history": history} The validator is a plain Python callable that returns passed: bool, message: str . Keeping it separate from the LLM logic means you can unit-test it independently and swap it without touching the agent loop. Malformed or schema-violating JSON is the most common failure mode in structured LLM output tasks. Here is a concrete validator that enforces a fixed schema: python import jsonschema EXPECTED SCHEMA = { "type": "object", "required": "summary", "severity", "cve ids" , "properties": { "summary": {"type": "string", "minLength": 10}, "severity": {"type": "string", "enum": "low", "medium", "high", "critical" }, "cve ids": { "type": "array", "items": {"type": "string", "pattern": "^CVE-\\d{4}-\\d+$"}, }, }, } def validate security report text: str - tuple bool, str : clean = text.strip if clean.startswith " " : clean = "\n".join clean.split "\n" 1: if clean.endswith " " : clean = "\n".join clean.split "\n" :-1 try: data = json.loads clean except json.JSONDecodeError as exc: return False, f"Invalid JSON: {exc}" try: jsonschema.validate data, EXPECTED SCHEMA except jsonschema.ValidationError as exc: return False, f"Schema violation: {exc.message}" return True, "OK" agent = ReflectionAgent task prompt= "Analyze the following security advisory and return a JSON object " "with keys 'summary' string , 'severity' low/medium/high/critical , " "and 'cve ids' array of CVE strings . Return only the JSON, no prose." , critique prompt= "Review this JSON output for accuracy, completeness, and schema compliance. " "List each specific violation and explain why it fails." , validator=validate security report, result = agent.run "CVE-2024-3094: backdoor found in XZ Utils 5.6.0 and 5.6.1 affecting liblzma, " "allowing remote code execution on affected Linux distributions." print json.dumps result, indent=2 The deterministic schema check runs before the LLM critique. If JSON parsing fails, the error message is already precise and actionable. The LLM critique step is reserved for semantic problems that a schema cannot express. Reflection loops without a hard cap on iterations will generate runaway costs in production. Three rules to enforce: 1. Hard iteration limit. Never exceed 4–5 cycles per request. If the model has not produced valid output after four attempts, the problem is almost always the prompt, not a fixable runtime error. 2. Track token spend across iterations. Add a budget guard: COST PER 1K TOKENS = 0.003 adjust to your model's pricing class BudgetedReflectionAgent ReflectionAgent : def init self, args, max cost usd: float = 0.10, kwargs : super . init args, kwargs self.max cost usd = max cost usd self. total cost = 0.0 def check budget self, prompt: str - None: Real usage: read token counts from the API response body estimated tokens = len prompt.split 1.3 projected cost = estimated tokens / 1000 COST PER 1K TOKENS if self. total cost + projected cost self.max cost usd: raise RuntimeError f"Budget exceeded: ${self. total cost:.4f} spent, " f"limit ${self.max cost usd}" self. total cost += projected cost 3. Log every iteration to a structured store. A simple SQLite table with task id, attempt, input tokens, passed, critique text is enough to identify which task types consistently fail and need prompt improvements. The pattern fails in predictable ways: Self-reinforcing errors. The model accepts a wrong assumption from iteration 1 in iteration 2. Fix: add to the system prompt — "Do not treat facts from the previous output as verified. Re-derive from the original input." Vague critique prompts. Asking “is this correct?” returns “yes” more often than not. Ask “list each specific constraint the output violates and explain why.” No ground truth. Reflection only works when the validator has a clear, programmatic success criterion. Subjective tasks — tone, persuasiveness, brand alignment — need human review, not another LLM iteration. Security-sensitive use cases, where the agent takes real actions based on its output, need sandboxing on top of reflection. Tool calls should be scoped to least-privilege, outputs sanitized before use, and every action logged with rollback support. We maintain free hardening checklists for agentic LLM deployments https://ayinedjimi-consultants.fr/checklists covering tool isolation, output validation, and incident recovery. Reflection loops improve output reliability without fine-tuning or swapping models. The pattern has three components: a generation step, a deterministic validator, and an LLM critique that feeds the next attempt. Keep the iteration count low ≤4 , track costs per request, and log every iteration. The goal is not to make the model infallible — it is to catch errors programmatically before they propagate downstream. Start with the schema validator version above. It handles 80% of structured output failures and requires zero prompt engineering on the critique side. Add LLM self-critique only when deterministic checks cannot express the quality bar you need. I run AYI NEDJIMI Consultants, a cybersecurity consulting firm. We publish free security hardening checklists — PDF and Excel.