{"slug": "building-a-self-correcting-ai-agent-with-reflection-loops-in-python", "title": "Building a Self-Correcting AI Agent with Reflection Loops in Python", "summary": "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.", "body_md": "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.\n\nA 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.\n\nThe 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.\n\nHere is a Python class that wraps any LLM call in a reflection loop:\n\n``` python\nimport json\nfrom typing import Any, Callable\nimport httpx\n\nMAX_ITERATIONS = 4\n\ndef call_llm(prompt: str, system: str = \"\") -> str:\n    response = httpx.post(\n        \"https://api.example-llm.com/v1/messages\",\n        headers={\"x-api-key\": \"YOUR_KEY\", \"Content-Type\": \"application/json\"},\n        json={\n            \"model\": \"your-model\",\n            \"max_tokens\": 1024,\n            \"system\": system,\n            \"messages\": [{\"role\": \"user\", \"content\": prompt}],\n        },\n        timeout=30,\n    )\n    response.raise_for_status()\n    return response.json()[\"content\"][0][\"text\"]\n\nclass ReflectionAgent:\n    def __init__(\n        self,\n        task_prompt: str,\n        critique_prompt: str,\n        validator: Callable[[str], tuple[bool, str]],\n    ):\n        self.task_prompt = task_prompt\n        self.critique_prompt = critique_prompt\n        self.validator = validator\n\n    def run(self, user_input: str) -> dict[str, Any]:\n        history: list[dict] = []\n        attempt = 0\n\n        while attempt < MAX_ITERATIONS:\n            attempt += 1\n            context = f\"User input: {user_input}\"\n            if history:\n                last = history[-1]\n                context += (\n                    f\"\\n\\nPrevious attempt:\\n{last['output']}\"\n                    f\"\\nCritique:\\n{last['critique']}\"\n                )\n\n            output = call_llm(f\"{self.task_prompt}\\n\\n{context}\")\n            ok, critique = self.validator(output)\n\n            history.append(\n                {\"attempt\": attempt, \"output\": output, \"critique\": critique, \"passed\": ok}\n            )\n\n            if ok:\n                return {\"success\": True, \"output\": output, \"iterations\": attempt, \"history\": history}\n\n            llm_critique = call_llm(\n                f\"{self.critique_prompt}\\n\\nOutput to critique:\\n{output}\",\n                system=\"Be specific about what is wrong. Do not repeat the corrected version.\",\n            )\n            history[-1][\"critique\"] = llm_critique\n\n        return {\"success\": False, \"output\": history[-1][\"output\"], \"iterations\": attempt, \"history\": history}\n```\n\nThe `validator`\n\nis a plain Python callable that returns `(passed: bool, message: str)`\n\n. Keeping it separate from the LLM logic means you can unit-test it independently and swap it without touching the agent loop.\n\nMalformed 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:\n\n``` python\nimport jsonschema\n\nEXPECTED_SCHEMA = {\n    \"type\": \"object\",\n    \"required\": [\"summary\", \"severity\", \"cve_ids\"],\n    \"properties\": {\n        \"summary\": {\"type\": \"string\", \"minLength\": 10},\n        \"severity\": {\"type\": \"string\", \"enum\": [\"low\", \"medium\", \"high\", \"critical\"]},\n        \"cve_ids\": {\n            \"type\": \"array\",\n            \"items\": {\"type\": \"string\", \"pattern\": \"^CVE-\\\\d{4}-\\\\d+$\"},\n        },\n    },\n}\n\ndef validate_security_report(text: str) -> tuple[bool, str]:\n    clean = text.strip()\n    if clean.startswith(\"```\n\n\"):\n        clean = \"\\n\".join(clean.split(\"\\n\")[1:])\n    if clean.endswith(\"\n\n```\"):\n        clean = \"\\n\".join(clean.split(\"\\n\")[:-1])\n\n    try:\n        data = json.loads(clean)\n    except json.JSONDecodeError as exc:\n        return False, f\"Invalid JSON: {exc}\"\n\n    try:\n        jsonschema.validate(data, EXPECTED_SCHEMA)\n    except jsonschema.ValidationError as exc:\n        return False, f\"Schema violation: {exc.message}\"\n\n    return True, \"OK\"\n\nagent = ReflectionAgent(\n    task_prompt=(\n        \"Analyze the following security advisory and return a JSON object \"\n        \"with keys 'summary' (string), 'severity' (low/medium/high/critical), \"\n        \"and 'cve_ids' (array of CVE strings). Return only the JSON, no prose.\"\n    ),\n    critique_prompt=(\n        \"Review this JSON output for accuracy, completeness, and schema compliance. \"\n        \"List each specific violation and explain why it fails.\"\n    ),\n    validator=validate_security_report,\n)\n\nresult = agent.run(\n    \"CVE-2024-3094: backdoor found in XZ Utils 5.6.0 and 5.6.1 affecting liblzma, \"\n    \"allowing remote code execution on affected Linux distributions.\"\n)\nprint(json.dumps(result, indent=2))\n```\n\nThe 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.\n\nReflection loops without a hard cap on iterations will generate runaway costs in production. Three rules to enforce:\n\n**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.\n\n**2. Track token spend across iterations.** Add a budget guard:\n\n```\nCOST_PER_1K_TOKENS = 0.003  # adjust to your model's pricing\n\nclass BudgetedReflectionAgent(ReflectionAgent):\n    def __init__(self, *args, max_cost_usd: float = 0.10, **kwargs):\n        super().__init__(*args, **kwargs)\n        self.max_cost_usd = max_cost_usd\n        self._total_cost = 0.0\n\n    def _check_budget(self, prompt: str) -> None:\n        # Real usage: read token counts from the API response body\n        estimated_tokens = len(prompt.split()) * 1.3\n        projected_cost = (estimated_tokens / 1000) * COST_PER_1K_TOKENS\n        if self._total_cost + projected_cost > self.max_cost_usd:\n            raise RuntimeError(\n                f\"Budget exceeded: ${self._total_cost:.4f} spent, \"\n                f\"limit ${self.max_cost_usd}\"\n            )\n        self._total_cost += projected_cost\n```\n\n**3. Log every iteration to a structured store.** A simple SQLite table with `(task_id, attempt, input_tokens, passed, critique_text)`\n\nis enough to identify which task types consistently fail and need prompt improvements.\n\nThe pattern fails in predictable ways:\n\n**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.\"*\n\n**Vague critique prompts.** Asking “is this correct?” returns “yes” more often than not. Ask “list each specific constraint the output violates and explain why.”\n\n**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.\n\nSecurity-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.\n\nReflection 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.\n\nStart 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.\n\n*I run AYI NEDJIMI Consultants, a cybersecurity consulting firm. We publish free security hardening checklists — PDF and Excel.*", "url": "https://wpnews.pro/news/building-a-self-correcting-ai-agent-with-reflection-loops-in-python", "canonical_source": "https://dev.to/ayinedjimi-consultants/building-a-self-correcting-ai-agent-with-reflection-loops-in-python-hda", "published_at": "2026-08-23 10:03:11+00:00", "updated_at": "2026-08-23 10:13:30.383446+00:00", "lang": "en", "topics": ["artificial-intelligence", "large-language-models", "ai-agents", "developer-tools"], "entities": [], "alternates": {"html": "https://wpnews.pro/news/building-a-self-correcting-ai-agent-with-reflection-loops-in-python", "markdown": "https://wpnews.pro/news/building-a-self-correcting-ai-agent-with-reflection-loops-in-python.md", "text": "https://wpnews.pro/news/building-a-self-correcting-ai-agent-with-reflection-loops-in-python.txt", "jsonld": "https://wpnews.pro/news/building-a-self-correcting-ai-agent-with-reflection-loops-in-python.jsonld"}}