Part 3 of 6 · 3 min read
The loop in code
What goes wrong, and what the example shows.
The loop in code
The whole thing, trimmed:
messages = [system_prompt, question]
for n in range(1, max_steps + 1):
raw = model(messages) # the model's text
messages.append(assistant(raw))
try:
thought, action, args = parse(raw) # may raise: format error
except ParseError as e:
messages.append(observation(str(e))) # tell the model what was wrong
continue
if action == "finish":
return args["answer"]
if action not in TOOLS:
messages.append(observation(f"Unknown tool {action!r}. Available: ..."))
continue
result = TOOLS[action](**args) # your code runs it
messages.append(observation(result))
return None # ran out of steps
The decisions that matter are the ones around the model call: what you do on a bad reply, a bad tool name, a tool error, a repeated action, and the step limit. The rest is plumbing.
What goes wrong
| Failure | What you see | Guard |
|---|---|---|
| Repeats itself | the same action, again and again | repeated-action check: nudge once, then stop |
| Breaks the format | no Action: line, broken JSON |
send the format error back as an observation |
| Unknown tool | calls lookup instead of search |
return an error that lists the available tools |
| Bad arguments | wrong argument name, wrong type | validate; return an error naming the expected argument |
| Tool error | file not found, division by zero | write errors the model can act on |
| Never finishes | keeps searching after it has the answer | step limit, plus a "you may finish now" instruction |
| Finishes too early | answers from memory without using tools | require evidence; check the answer (module 4 of this level) |
| Invents an observation | writes its own "Observation:" lines | stop generation at "Observation:" (a stop sequence) |
| Loses track | forgets an early result in a long history | shorter histories; summaries (Level 7) |
What the example shows
With the loop, against answering from memory. The twelve tasks need up to three tool steps. A model that cannot use tools is right on 2 of 12 (17%: the multiplication it can do in its head and the question that has no answer). The model with the loop is right on all 12, with 2.9 model calls per task on average. (A real model without tools would guess some facts; this stand-in just says it does not know.)
One bad habit under three loop designs (share of the 12 tasks right, and model calls per task):
| Habit | Errors end the run | Errors fed back | Fed back plus repeat guard |
|---|---|---|---|
| repeats itself, ignores nudges | 0% (8.0 calls) | 0% (8.0) | 0% (4.0) |
| repeats until told it is repeating | 0% (8.0) | 0% (8.0) | 100% (4.9) |
| breaks the format | 0% (2.0, crashes) | 100% (4.8) | 100% (4.8) |
| names a tool that does not exist | 0% (1.0, crashes) | 100% (3.9) | 100% (3.9) |
Three lessons:
- Feeding errors back turns format and tool-name slips from fatal into a one-step detour. The parse error must say what was expected.
- A repeat guard helps a model that can react to a nudge, and caps the cost of one that cannot (8 calls down to 4). It cannot make a stubborn model succeed; that is a prompt, model, or tool-description problem (module 2).
- Guards live in your code. You cannot rely on the model to stop itself.