Loop Engineering on a Local LLM: Agent Loops That Check Their Own Work
Loop engineering is designing the act → observe → verify → retry cycle around a model instead of prompting it one step at a time. It matters more locally than it does on a frontier API: a deterministic verifier (test suite, type checker, build) supplies the judgement a 27B model lacks, and an iteration cap plus a clean context reset stops it spiralling. The rule that decides everything: no objective check, no loop.
作者 Jakub Rusinowski · 最后更新 2026年8月4日 · 硬件数据由我们的 VRAM 引擎
Three practices have stacked up in three years. Prompt engineering was about the wording of one request. Context engineering was about what the model can see when it answers. Loop engineering — the term went mainstream in mid-2026 — is about what happens after the answer: the model acts, something objective observes the result, and the system decides whether to accept it, retry it, or stop. It's the layer underneath every serious coding agent shipped this year, and it's the layer where local models gain the most ground.
The reason is unglamorous. A frontier model can often carry a vague instruction to a decent result on the first try, so a sloppy loop is survivable. A 22–32B model running on your own GPU frequently cannot — but it is very good at making a failing test pass when you hand it the failure output. Loop engineering converts the model's weakness (judgement, long-horizon planning) into something the loop supplies for free, and leans on its strength (fast, local, unmetered iterations). A well-designed loop on a 24 GB card routinely beats a poorly-designed loop on an API model that costs money per attempt.
The anatomy of an agent loop
Every coding agent — Aider, Cline, OpenCode, Claude Code, Codex — is the same five-step cycle underneath. The differences are in what each step is allowed to do.
Step 4 is what separates loop engineering from "running the agent again." A verifier has one job: return pass or fail identically every time, with no model in the decision. pytest -q, tsc --noEmit, cargo test, go build ./..., ruff check, npm run lint — all valid. "Ask a second model whether the code looks right" is not, at least not as the gate; LLM-as-judge belongs outside the objective check, never in place of it.
This is why the first question of any local loop project is not "which model?" but "what is my pass/fail command, and how long does it take to run?" A repo without a fast, trustworthy test command cannot be looped on. Fixing that comes first.
The four levels of loops
Loops nest. Each level wraps the one beneath it and adds a different kind of correction. Knowing which level you are operating at tells you what your next improvement should be.
| Level | What it is | Who decides "done" | Local-hardware reality |
|---|---|---|---|
| 1 · Tool loop | Model calls tools repeatedly until it declares completion (the classic ReAct cycle) | The model | Works on 22B+ with solid tool calling. Fails silently when it "declares" success it didn't achieve |
| 2 · Goal loop | You define a measurable success condition; the loop keeps going until it's met | A deterministic check | The sweet spot for local models. The check is the intelligence the model doesn't have |
| 3 · Verification loop | Multiple gates stacked — tests, then types, then lint, then a review pass | A chain of checks | Cheap locally: gates are CPU work, not tokens. Each gate you add reduces the model tier you need |
| 4 · Meta loop | The loop improves its own inputs — failures feed back into the rules file, prompts, or task decomposition | You, periodically | Where local setups compound. Every fix you write into AGENTS.md is permanent and free |
Most people who say "the agent doesn't work" are stuck at level 1 with a model that isn't strong enough to self-assess. Moving to level 2 — same model, same hardware, one added pytest gate — is usually the largest single quality jump available, and it costs nothing.
What changes when the model is local
The concept is provider-neutral; the engineering constraints are not. Five differences drive every design decision on your own hardware.
- Iterations are free but not fast. No per-token bill, so a 12-iteration loop is fine financially. But a 27B at Q4 on a 24 GB card produces perhaps 30–60 tokens/sec, and each iteration re-reads a growing context. Loops are wall-clock-bound, not budget-bound — which flips the usual advice: prefer fewer, better-informed iterations over brute force.
- Context is a hardware cost. Every retry appends failure output. On an API the window is someone else's problem; locally, KV cache is VRAM you have to own. A 32K context on a 27B adds several GB on top of the weights — model the exact number with the VRAM checker before you design a loop that assumes 64K.
- Tool-call reliability is the failure mode. Frontier models emit well-formed tool calls essentially always. Local models drift — wrong argument names, prose wrapped around JSON, a second call hallucinated after the first. A loop that can't parse an action can't act. See tool calling with local models for the grammar-constrained fix.
- Degradation is steeper. Local models lose coherence faster as context fills. The practical consequence: reset context between attempts rather than accumulating ten failed attempts in one window. Carry forward the goal, the current diff, and the latest failure — not the transcript.
- You own the whole stack. Nothing rate-limits you, nothing deprecates your model, and no vendor sees the repo. For a loop that runs unattended over a private codebase, that is the entire argument.
A minimal loop you can run tonight
Before adopting a framework, build the twenty-line version — it makes the moving parts obvious and it is genuinely useful. This drives any OpenAI-compatible local endpoint (Ollama on :11434, LM Studio on :1234, llama-server, vLLM) and gates on your real test command.
# loop.py — goal loop (level 2) against a local endpoint
import subprocess, json, urllib.request
ENDPOINT = "http://localhost:11434/v1/chat/completions"
MODEL = "devstral-small-2:24b" # whatever your VRAM supports
VERIFY = ["pytest", "-q"] # THE gate. Must be deterministic.
MAX_ITERS = 6 # always cap. always.
def verify():
p = subprocess.run(VERIFY, capture_output=True, text=True)
return p.returncode == 0, (p.stdout + p.stderr)[-4000:] # tail only
def ask(messages):
body = json.dumps({"model": MODEL, "messages": messages,
"temperature": 0.1, "stream": False}).encode()
req = urllib.request.Request(ENDPOINT, body, {"Content-Type": "application/json"})
with urllib.request.urlopen(req) as r:
return json.load(r)["choices"][0]["message"]["content"]
goal = "Make the failing tests in tests/ pass. Change only src/."
for i in range(1, MAX_ITERS + 1):
ok, output = verify()
if ok:
print(f"PASS after {i-1} iteration(s)"); break
# Fresh context every attempt: goal + current failure only.
reply = ask([
{"role": "system", "content": "You are editing a repo. Reply ONLY with a unified diff."},
{"role": "user", "content": f"{goal}\n\nTest output:\n{output}"},
])
subprocess.run(["git", "apply", "-"], input=reply, text=True)
print(f"--- iteration {i} applied ---")
else:
print("Cap reached without a green suite — reverting.")
subprocess.run(["git", "checkout", "--", "."])
Four design decisions in that file are the whole lesson: the verifier is a subprocess, not a prompt; the failure output is truncated to its tail so context doesn't explode; context is rebuilt each iteration instead of accumulated; and the loop reverts on cap rather than leaving half-finished edits behind. Run it inside a git worktree or a container so a bad diff costs you nothing — see sandboxing a local coding agent.
Designing the verifier (the part everyone skips)
A loop inherits the quality of its check exactly. Grade yours against these, in order of value per minute spent:
1. Speed before coverage. A 4-second subset that runs every iteration is worth more than a 6-minute full suite that runs once. Point the loop at the fast subset; run the full suite once at the end. On local hardware the test run often costs less wall-clock than one model turn — that asymmetry is the reason to gate aggressively.
2. Fail loudly and specifically. The failure text is the next prompt. Assertion messages that say what was expected versus received teach the model in one turn; AssertionError alone burns three.
3. Stack cheap gates first. Type check (tsc --noEmit, mypy) and lint catch a large share of local-model errors — wrong import paths, invented method names, argument-count slips — in under a second, before a single test runs.
4. Close the reward-hacking holes. A model told "make tests pass" will, given the chance, edit the test, delete the assertion, or add @pytest.mark.skip. Constrain the writable path (Change only src/), and add a gate that fails if the test files changed: git diff --exit-code -- tests/. This is not paranoia; it is the single most common way loop output looks green and is worthless.
5. Know what the check does not cover. Green tests mean "no known regression," not "correct." Every loop needs a human diff review at the end. The loop's job is to hand you a small, plausible, already-passing diff — not to merge it.
Failure modes, and the fix for each
| Symptom | What's actually happening | Fix |
|---|---|---|
| Same edit reattempted forever | Model can't see it already tried this | Include the previous attempt's diff — or its rejection reason — in the next prompt, not the whole transcript |
| Quality collapses around iteration 5 | Context rot: window filled with stale failures | Rebuild context each attempt; keep goal + current diff + latest failure only |
| Tests pass, code is nonsense | Reward hacking on the gate | Freeze the test files, add a second gate (types, build, a golden-output check) |
| Loop never terminates | No cap, or a flaky test that fails randomly | Hard cap iterations; quarantine flaky tests before looping |
| Agent stops mid-task claiming success | Level-1 loop with a model that self-assesses badly | Move to level 2 — the exit condition must be the verifier, never the model's opinion |
| Broken tool call, loop dies on parse | Local model drifted off the schema | Grammar-constrained decoding, plus one parse-repair retry that never counts against the cap |
| Machine unusable while it runs | Model + KV cache saturating the GPU | Smaller quant or shorter context; or run the loop on a rented GPU overnight |
Two habits prevent most of these: cap everything (iterations, per-iteration wall clock, tokens per turn), and log every iteration to disk — prompt, tool call, verifier output. The log is how you tell "the model is too small" apart from "my prompt was ambiguous," and they demand opposite responses.
What size model does a loop actually need?
A good loop lowers the bar, but it doesn't erase it. The floor is set by two abilities: emitting a valid tool call or diff, and correctly interpreting a stack trace.
- 8B (8 GB GPU) — viable at level 2 for narrow, single-file goals with a crisp failing test. Expect to babysit; keep the cap at 3–4. Best paired with strict diff formats and constrained decoding.
- 16 GB tier — the first tier where an unattended loop is reasonable. Devstral Small 2 24B is explicitly agent-tuned and follows edit formats well.
- 24 GB tier — Qwen 3.6 27B or Qwen 2.5 Coder 32B: multi-file goals, tolerable failure-interpretation, loops that finish while you make coffee. This is the tier most people should target.
- Workstation / 96 GB+ Mac — Qwen3-Coder 80B-A3B. Fast MoE decode makes long loops practical, which matters more than raw quality once a verifier is in place.
Per-tier reasoning and exact VRAM figures live in best local coding models; the honest capability ladder for autonomy is in VRAM for coding models.
Where loop engineering pays off locally
Not every task deserves a loop. The ones that do share a shape: a machine-checkable definition of done, and enough repetition to justify writing the check.
- Test-driven repair — the canonical case. Failing test in, green diff out.
- Mechanical migrations — framework version bumps, API renames, type annotations across hundreds of files. The verifier is the build; the model does the tedium.
- Lint and type debt — point the loop at
tsc --noEmitand let it grind the error count down overnight on hardware you already own. - Test generation with a coverage gate — generate tests, gate on coverage delta and on the suite still passing.
- Doc/code drift — regenerate docs from source, verify by re-running the doc examples.
Tasks that resist loops: anything whose success is a matter of taste (API design, naming), anything where the check costs more to write than doing the work by hand, and anything touching a system you cannot safely re-run. For those, stay in the interactive assistant mode covered in the tool matrix.
没有硬件?先租一块 GPU
Loops are wall-clock-bound: the same 12-iteration job that takes an evening on an 8 GB card finishes over lunch on a rented 48 GB GPU. Renting by the hour is also the cheapest way to find out whether a bigger model actually fixes your loop, or whether your verifier was the problem.
- RunPod — RTX 4090 ≈ $0.3–0.7/hr, A100/H100 by the hour; serverless per-second billing available
- Vast.ai — Marketplace of hosted GPUs — often the cheapest per hour (interruptible options)
- Lambda — On-demand A100/H100/B200 instances and clusters, per-hour billing
Affiliate links — we may earn a commission if you sign up, at no extra cost to you.
完整列表见 云 AI 目录.
常见问题
What is loop engineering?
Loop engineering is the practice of designing the iterative cycle an AI agent runs in — act, observe the result, check it against an objective condition, and retry or stop — rather than prompting the model step by step. The defining element is a deterministic verifier (tests, type check, build) that decides whether the loop continues. Without that check you have automation, not a loop.
How is loop engineering different from prompt engineering?
Prompt engineering optimises a single request; context engineering optimises what the model sees; loop engineering optimises what happens after the model answers. In practice you still do all three, but the loop is where reliability comes from: a mediocre prompt inside a verified loop beats an excellent prompt with no check on the output.
Can loop engineering work with a small local model?
Yes, and it helps small models disproportionately. The verifier supplies the judgement the model lacks, and local iterations cost nothing but time. An 8B model handles narrow single-file goals with a crisp failing test; 22B and up handles multi-file work unattended. What a small model cannot do is decide for itself when it is finished — which is exactly the job the verifier takes over.
How many iterations should a coding agent loop run?
Cap it, typically at 4–8. Success almost always arrives in the first few attempts; past that, the model is usually cycling through variants of the same misunderstanding, and context degradation makes each attempt worse than the last. When the cap is hit, revert and re-scope the task rather than raising the cap.
Why does my agent pass the tests but produce broken code?
Reward hacking on the gate — most often the model edited or skipped the test rather than fixing the code. Restrict which paths it may write to, add a gate that fails if test files changed (git diff --exit-code -- tests/), and stack a second independent check such as a type pass or build. Green tests mean "no known regression", never "correct".
Do I need a framework for loop engineering, or is a script enough?
A twenty-line script covering assemble → act → verify → retry is enough for repair, migration and lint-debt loops, and it teaches you where your setup actually breaks. Reach for a full agent harness — Aider, Cline, OpenCode — when you need multi-file editing, repo search, permission handling and context compaction, which are the parts genuinely tedious to rebuild.
继续阅读
- Building autonomous agents — the general (non-coding) agent guide
- Reliable JSON & structured output from local models
- VRAM checker — what context length your GPU actually affords
- Context length & KV-cache OOM — the loop-killer
本专题更多内容
- The Agent Harness: What Actually Turns a Local Model Into a Coding Agent
- Context Engineering for Local Coding Agents: Budgeting a Window You Pay For
- Tool Calling With Local Models: Making Agents Stop Breaking on Malformed JSON
- Evaluating a Local Coding Agent: Benchmarks Lie, Your Repo Doesn’t
- 本地 AI 编程助手 — 完整专题