Self-Debugging AI Agents: GEPA & LinUCB in Practice
May 22, 2025 · 6 min read
What I learned building a natural-language SQL agent that improves its own accuracy without retraining — using evolutionary prompt optimization, a self-debug loop, and a contextual bandit to pick repair strategies.
The Core Problem With LLM-Generated SQL
LLMs write surprisingly good SQL — until they don't. When they fail, the failure modes are predictable: wrong table joins, hallucinated column names, off-by-one aggregations. The standard industry answer is fine-tuning on a domain-specific dataset. That works, but it's expensive, brittle to schema changes, and requires retraining every time your database evolves.
I wanted to see how far you could push accuracy improvements using only inference-time techniques: no weight updates, no fine-tuning, no labeled SQL pairs.
Architecture: Three Layers of Self-Correction
The agent stacks three mechanisms on top of a base LLM call:
1. Self-Debug Loop
The simplest layer: run the generated SQL against the actual database. If it throws an error, feed the error message back to the model with a "fix this" prompt. Repeat up to 5 times. This alone catches ~60% of syntax errors and obvious schema mismatches. The surprising part is how much context the error message carries — "column 'user_id' does not exist in table 'orders'" is often enough for the model to self-correct without any additional guidance.
2. GEPA — Evolutionary Prompt Optimization
GEPA (Genetic Evolutionary Prompt Algorithm) treats your system prompt as an organism to be evolved. Instead of manually tuning instructions, you run a population of prompt variants against a small evaluation set, score them, and breed the top performers.
The key result from our experiments: GEPA found better prompts in 35× fewer evaluations than a standard RL-based prompt optimizer. The reason is selection pressure — genetic crossover tends to preserve the structural patterns that work while mutating the specifics, rather than exploring randomly.
// Pseudo-code for one GEPA generation
const next_gen = top_k(population, k=5)
.flatMap(p => [mutate(p), crossover(p, random_from(top_k))]);
population = evaluate_and_rank(next_gen, eval_set);
3. LinUCB — Contextual Bandit for Repair Strategy
The self-debug loop has a problem: "just try again" is only one of many repair strategies. Others include: rewriting the query from scratch, decomposing into subqueries, asking for schema clarification, or switching to a different model. Choosing the right strategy depends on the error type, query complexity, and schema characteristics.
LinUCB (Linear Upper Confidence Bound) is a contextual bandit algorithm that learns which repair strategy works best given the current context vector — a feature representation of the error, query, and schema. The "Upper Confidence Bound" part means it naturally balances exploration (try underused strategies) with exploitation (use what's been working).
After ~200 queries, the bandit learned a coherent policy: syntax errors → targeted fix, schema hallucinations → schema re-injection, aggregation errors → query decomposition.
What Surprised Me
The interaction between layers was more important than any single layer. GEPA-optimized prompts reduced the self-debug loop's required iterations from an average of 2.3 to 1.1. The bandit's strategy distribution shifted significantly after GEPA ran — because the base failure modes changed.
The other surprise: the GitHub-style diff UI we built for showing query changes between iterations was more useful than any metric dashboard. Watching the model's reasoning about its own errors turned out to be the best debugging tool we had.
All the code is open-source on GitHub — including the LinUCB implementation and the GEPA runner.
Tags: ai, llm, prompt-engineering, research, ml
By Arnav Gupta. More posts at Gists & Blogs.