AI-Assisted Programming in Practice: Fixing a Stubborn Legacy Bug in 9 Minutes

    A hands-on retrospective on using an LLM coding assistant to fix a long-standing, intermittent bug in about nine minutes—without hand-writing a single line of code. The article explains why legacy bugs resist fixes (context gaps), walks through a four-step workflow covering structured problem statements, curated context, reproduction-first debugging, and automated verification, and distills practical lessons on budgeting for AI coding tools and on where human judgment must stay in the loop.

    Background: A Legacy Bug Nobody Wanted to Touch

    One of my day-to-day responsibilities is maintaining an in-house content distribution tool. The project kicked off about three months ago and has already grown to roughly 270,000 lines of code, including generated code and test code. As the codebase scaled, the classic maintenance problems arrived with it, and at the top of my task list sat one aging bug: intermittent, with scattered logs and no complete stack trace—nobody wanted to touch it.

    I had previously tried several different model-and-tool combinations without fully resolving it: either the model located the wrong spot and the fix was incomplete, or the fix itself introduced new regressions. Then, in a recent attempt, I handed the entire task to an LLM coding assistant. From pressing Enter to having the bug reproduced, fixed, and verified, the whole run took about nine minutes—and I never hand-wrote a line of code.

    This is not a product review but a methodology retrospective: with the same model capability, why did this attempt succeed? The answer lies not in the model itself, but in how it was used.

    Why Legacy Bugs Are Hard to Fix: The Context Gap

    First, why are bugs like this so hard to fix? For the vast majority of “unfixable” bugs, the root cause is not how deep the code defect runs, but a context gap, which shows up as three obstacles:

    1. Lost design intent. The code has gone through many iterations, and the original boundary conditions and special data-compatibility logic are scattered across comments and commit history, making it hard for anyone who comes later to piece together the full picture.
    2. Harsh reproduction conditions. Triggering the bug depends on specific data shapes, timing, or concurrency windows that ordinary test cases never cover, so “looks fixed” and “actually fixed” are hard to tell apart.
    3. High fix risk. The affected modules are depended on by many other parts of the system; any change can set off a chain reaction, and without regression tests as a safety net, no one dares touch it.

    For a human engineer, the cost of overcoming these three obstacles is enormous: reading code, digging through history, running experiments—easily hours or even days. LLMs happen to have natural advantages in exactly these three areas: reading large volumes of context, enumerating hypotheses, and quickly generating experimental code. The only catch is that you have to feed the context correctly.

    What Happened in Those 9 Minutes: A Complete Workflow Breakdown

    Step 1 (about 2 minutes): Structure the Bug Report Instead of Describing It Casually

    Throwing the model a vague “the list occasionally loses data, please fix it” is inefficient. Following a fixed template, I first organized the problem statement:

    1. ## Symptom
    2. Dispatch tasks are occasionally lost—about 1 to 3 occurrences per 1,000 runs
    3. ## Reproduction path
    4. Happens when high-concurrency writes overlap with the queue consumption window
    5. ## Expected behavior
    6. All tasks eventually converge: nothing lost, nothing duplicated
    7. ## Actual behavior
    8. A race condition exists between consumer acknowledgment and write-side status updates
    9. ## Already ruled out
    10. Storage-layer write failures and network timeouts (confirmed by logs)
    11. ## Relevant modules
    12. Task state machine, dispatch queue, acknowledgment callback

    The value of a structured statement is that it compresses the search space from the entire system down to a handful of modules, and explicitly listing what has been ruled out keeps the model from burning inference budget on directions that humans have already falsified. The time spent on this step pays for itself in every step that follows.

    Step 2 (about 2 minutes): Feed Enough Context for the Model to “Understand” the System

    I gave the model three kinds of material: the source code and data-structure definitions of the relevant modules, commit messages covering recent key changes (from version control history), and a real exception log. Note that you do not need to stuff all 270,000 lines into the prompt—relevance filtering is itself the engineer’s non-delegable value.

    A practical self-check: if a newly hired colleague could understand this module just by reading the materials you provided, without asking anyone a single question, the context is sufficient. If not, keep adding material instead of hoping the model will guess.

    Step 3 (about 3 minutes): Reproduce First, Fix Second

    This is the most critical step of the entire process. I did not ask the model for a fix straight away; I required it to first write a test case that could reliably reproduce the problem:

    1. # Illustrative code: build a race-condition case that reproduces reliably
    2. def test_dispatch_task_lost_under_race():
    3. store = TaskStore()
    4. producer = Producer(store, delay=0) # write side
    5. consumer = Consumer(store, delay=1) # consumer acknowledgment is delayed
    6. for i in range(1000):
    7. producer.submit(Task(id=i))
    8. wait_until_settled(producer, consumer)
    9. assert store.count(status="done") == 1000 # reproduction point: occasionally below 1000

    “Reproduce first” delivers two decisive benefits. First, the model is forced to genuinely understand the trigger conditions instead of pattern-matching its way to a patch that merely “looks plausible.” Second, the reproduction case naturally becomes the regression test once the fix lands, so the verification loop closes itself.

    In this step, the model enumerated three root-cause hypotheses: a write-ordering problem between consumer acknowledgment and status updates, a reentrancy flaw in the queue’s retry logic, and a missing branch in the state machine’s transitions. It then verified each hypothesis against the reproduction script and finally confirmed the second one: a condition check on the retry path would, under a specific timing window, skip persisting the task status, causing the task to “disappear” before acknowledgment.

    Step 4 (about 2 minutes): Generate the Fix and Verify It Automatically

    The fix the model produced was small: reorder a condition check in the retry logic and add idempotency protection to the acknowledgment callback. It then automatically ran the reproduction case (passed), the existing unit tests for the related modules (all passed), and—on my request—added boundary test cases for concurrency scenarios. The only thing I did was review the few dozen changed lines one by one to confirm they did not conflict with business constraints.

    Timeline Recap

    • 0–2 minutes: structured problem statement and context preparation
    • 2–4 minutes: reproduction case generation and hypothesis verification
    • 4–7 minutes: root-cause localization and patch generation
    • 7–9 minutes: regression testing and human review

    The nine-minute figure itself does not matter. What matters is the structure of the process: reproduce first, fix second, closed-loop verification, human oversight.

    The Budget: Making Around US$2,400 a Year Go Further

    This practice ran under an annual budget of roughly 17,000 RMB (about US$2,400), which has to cover model calls, cloud resources, and tool subscriptions. To make AI coding effective at this budget level, three principles apply:

    1. Allocate by leverage, not by frequency. Budget should flow to high-leverage scenarios: legacy bug hunting, understanding historical code, filling in test coverage, generating boilerplate. What these have in common is high human cost and low AI marginal cost. Routine small fixes, by contrast, may need little or no AI at all.
    2. Prefer usage-based elasticity over locked-in flat fees. Rely primarily on pay-as-you-go model inference, paired with object storage for archiving code and logs and a logging service for call auditing and usage review. These commodity capabilities are standard offerings across public cloud platforms (Baidu AI Cloud is one example) and can simply be combined on demand. For a low-frequency, high-intensity usage pattern, the total cost is significantly lower than long-term monthly subscriptions.
    3. Leave enough budget for “verification.” Many people count generation costs but not verification costs. In practice, having the model write more tests and run more cases raises the unit cost only modestly while sharply reducing rework—so the total bill goes down.

    To be clear, this budget framework applies to the scale of individual developers and small teams. Larger teams need their own accounting, but the core approach—allocate by leverage, verification first—stays the same.

    Six Lessons for Turning One Success into a Repeatable Capability

    1. Context quality determines output quality. Garbage in, garbage out applies to models too. Every minute spent organizing context pays for itself downstream.
    2. Guide step by step; don’t dump the whole requirement at once. A “reproduce → locate → fix → verify” rhythm is far more reliable than a blanket “just fix it” instruction.
    3. Always require regression tests. A fix without regression tests simply turns today’s bug into tomorrow’s bug.
    4. Write out what has already been ruled out. Putting directions that humans have already falsified into the prompt drastically cuts the model’s wasted effort in dead ends.
    5. Human review is non-negotiable. The model can guarantee that code “looks right”; only a human can guarantee that it is “right for the business.” Review should focus on boundary conditions, permissions and data security, and conflicts with existing conventions.
    6. Capture process assets. After every successful AI-assisted debugging session, archive the problem statement, the context checklist, and the reproduction cases into the team knowledge base. The next time a similar problem appears, preparation time drops dramatically.

    Where Is the Human-AI Boundary?

    I did not write a single line of code in this session, but that does not mean “humans can step away.” The real division of labor looks like this:

    • The model handles: reading large volumes of code, enumerating hypotheses, writing test cases, generating patches, and mechanical verification;
    • The human handles: relevance filtering (deciding what to feed in), business-constraint judgment (deciding whether to make the change), and risk decisions (deciding when to ship).

    In other words, the human moves from “executor” to “quality gatekeeper and context curator.” That is where AI-assisted programming genuinely changes the development workflow: it does not replace engineering judgment—it frees engineering judgment from tedious execution and concentrates it on the decisions that matter most.

    Closing Thoughts

    Looking back, fixing a legacy bug in nine minutes looks like a victory of model capability, but it is really a victory of working method: a structured problem statement, curated context, a reproduce-first-fix-second rhythm, closed-loop verification, and human review that never stepped away. Combined with a disciplined budget strategy, the same method lets individual developers and small teams turn an AI coding assistant from a “toy” into genuine productivity at very low cost.