Single-Step-Agents (Anti-Pattern) + Fixes + Code

  • Erkenne die Falle, bevor sie in Prod landet.
  • Sieh, was bricht, wenn das Modell überzeugt danebenliegt.
  • Sichere Defaults kopieren: Permissions, Budgets, Idempotency.
  • Wissen, wann du keinen Agent brauchst.
Erkennungs-Signale
  • Tool-Calls pro Run steigen (oder wiederholen sich mit args-hash).
  • Kosten/Tokens pro Request steigen ohne bessere Ergebnisse.
  • Retries kippen von selten zu konstant (429/5xx).
Ein 'Single-Step Agent' ist meistens ein Chat-Completion, das an Seiteneffekte geklebt wurde. Warum das in Production bricht und wie die minimale Production-Loop-Variante aussieht.
Auf dieser Seite
  1. Problem (zuerst)
  2. Warum das in Production bricht
  3. 1) Kein Feedback Loop = kein Recovery
  4. 2) Budgets und stop reasons kommen zu spät dazu
  5. 3) Tool-Output wird ignoriert oder falsch benutzt
  6. 4) Writes werden zum Coin Flip
  7. Wann Single-Step reicht (ja, manchmal)
  8. Hard routing rule (die dich rettet)
  9. Migrationspfad (Single-Step → Loop)
  10. Implementierung (echter Code)
  11. Failure evidence (wie es aussieht, wenn es bricht)
  12. Example failure case (composite)
  13. 🚨 Incident: Premature ticket closure
  14. Abwägungen
  15. Wann NICHT nutzen
  16. Checklist (Copy-Paste)
  17. Safe default config
  18. FAQ
  19. Related pages
  20. Production takeaway
  21. What breaks without this
  22. What works with this
  23. Minimum to ship
Kurzfazit

Kurzfazit: Single-Step-„Agents“ (ein Model-Call → ausführen → fertig) haben keinen Platz für Validation, keinen Recovery-Loop und keine stop reasons. Wenn du Tools oder Seiteneffekte (Zustandsänderungen) hast, brauchst du einen bounded Loop + Governance.

Du lernst: Wann Single-Step wirklich okay ist • die minimale Routing-Regel • ein bounded Loop Interface • stop reasons • ein Incident-Smell-Test

Concrete metric

Single-Step: Validation hat keinen Platz • Recovery landet in „cleveren Prompts“ • Writes passieren zu früh
Looped runner: Budgets • Tool Gateway • stop reasons • safe-mode
Impact: weniger Incidents + debuggable Failures statt „execute & pray“


Problem (zuerst)

Jemand sagt: „wir haben einen Agenten gebaut“.

Der Code ist:

  1. Modell einmal aufrufen
  2. Tool Call parsen
  3. Ausführen
  4. Zurückgeben, was passiert ist
Truth

Das ist kein Agent. Das ist ein Function Call mit unvorhersehbaren Argumenten.

In einer Demo wirkt es schnell. In Production bricht es, weil reale Systeme noisy sind und du Feedback + Control brauchst.


Warum das in Production bricht

Failure analysis

1) Kein Feedback Loop = kein Recovery

Timeouts, Partial Responses, 429s, stale data, Schema Drift. Single-Step hat keinen Ort für Recovery-Logik, also landen „Recovery“-Ideen im Prompt und werden blind ausgeführt.

2) Budgets und stop reasons kommen zu spät dazu

Teams sagen: „es kann nicht loopen, also brauchen wir keine Budgets.“

Dann kommen Retries in Tools, Retries im Model-Call und ein zweiter Tool Call „just in case“.

Truth

Du hast Loops ohne Governance neu erfunden.

3) Tool-Output wird ignoriert oder falsch benutzt

Ein Tool Call, ein Output, und dann… einfach zurückgeben. Ohne Validation, ohne Invariants, ohne „haben wir das Problem gelöst?“

4) Writes werden zum Coin Flip

Single-Step erlaubt Writes sofort. Es gibt keine „read first, write later“-Policy. Der Blast Radius kommt früh.


Wann Single-Step reicht (ja, manchmal)

Single-Step ist okay, wenn alles davon stimmt:

  • keine Tools (oder Tools sind strikt read-only)
  • keine Seiteneffekte (Zustandsänderungen)
  • Output wird als Text genutzt, nicht als Command
  • du kannst Output mit einer strict schema validieren (oder brauchst es nicht)

Decision-Framework: Single-Step ist nur okay, wenn alles true ist:

  • ✅ Read-only (keine Seiteneffekte)
  • ✅ strongly typed output (oder keine Tools)
  • ✅ Failure ist billig (kleiner Blast Radius)
  • ✅ kein Retry/Recovery-Loop nötig

Wenn eins davon falsch ist: route zu einem looped runner.


Hard routing rule (die dich rettet)

Wenn der nächste Schritt Seiteneffekte verursachen kann, ist Single-Step nicht erlaubt.

TEXT
if action.has_side_effects:
  run_looped_runner()
else:
  run_single_step()

Migrationspfad (Single-Step → Loop)

So shippen Teams das oft — und darum bricht es:

PYTHON
# v1: single-step (fast, unsafe)
result = tool(llm_decide(task))  # damage can happen before validation

# v2: add validation (still unsafe if the tool already ran)
result = tool(llm_decide(task))
if not valid(result):
    raise RuntimeError("too late: side effect already happened")

# v3: bounded loop (safe enough to operate)
for step in range(max_steps):
    action = llm_decide(state)
    if action.kind == "tool":
        obs = tool_gateway.call(action.name, action.args)  # policy + budgets
        state = update(state, obs)
    else:
        return action.final_answer

Implementierung (echter Code)

Das Pattern hält Single-Step dort, wo es hingehört (safe, read-only), und routet alles andere in einen bounded Loop runner.

PYTHON
from __future__ import annotations

from dataclasses import dataclass
from typing import Any, Literal


@dataclass(frozen=True)
class Budgets:
  max_steps: int = 25
  max_tool_calls: int = 12
  max_seconds: int = 60


class Stopped(RuntimeError):
  def __init__(self, stop_reason: str):
      super().__init__(stop_reason)
      self.stop_reason = stop_reason


def is_side_effecting(action: dict[str, Any]) -> bool:
  # Production: decide side-effect class in code, not by prompt vibes.
  return action.get("kind") in {"write", "payment", "email", "ticket_close"}


def run_single_step(task: str, *, llm) -> dict[str, Any]:
  """
  Safe single-step: no tools, no writes.
  This is a completion, not an agent.
  """
  text = llm.text({"task": task, "style": "direct"})  # (pseudo)
  return {"status": "ok", "stop_reason": "single_step", "answer": text}


def run_looped(task: str, *, budgets: Budgets, runner) -> dict[str, Any]:
  """
  Delegate to a bounded runner that has:
  - tool gateway
  - output validation
  - stop reasons
  """
  return runner.run(task, budgets=budgets)  # (pseudo)


def route(task: str, *, llm, budgets: Budgets, runner) -> dict[str, Any]:
  # First decision is read-only: are we about to do anything with side effects?
  action = llm.json(
      {
          "task": task,
          "rule": "Return JSON {kind: 'read_only'|'side_effects'} and nothing else.",
          "examples": [{"task": "Summarize this text", "kind": "read_only"}, {"task": "Close ticket #123", "kind": "side_effects"}],
      }
  )  # (pseudo)

  if action.get("kind") == "side_effects":
      return run_looped(task, budgets=budgets, runner=runner)

  return run_single_step(task, llm=llm)
JAVASCRIPT
export class Stopped extends Error {
constructor(stopReason) {
  super(stopReason);
  this.stop_reason = stopReason;
}
}

export function runSingleStep(task, { llm }) {
// Safe single-step: no tools, no writes.
return llm.text({ task, style: "direct" }).then((text) => ({ status: "ok", stop_reason: "single_step", answer: text })); // (pseudo)
}

export function runLooped(task, { budgets, runner }) {
// Delegate to a bounded runner with tool gateway + stop reasons.
return runner.run(task, { budgets }); // (pseudo)
}

export async function route(task, { llm, budgets, runner }) {
const action = await llm.json({
  task,
  rule: "Return JSON {kind: 'read_only'|'side_effects'} and nothing else.",
  examples: [
    { task: "Summarize this text", kind: "read_only" },
    { task: "Close ticket #123", kind: "side_effects" },
  ],
}); // (pseudo)

if (action.kind === "side_effects") return await runLooped(task, { budgets, runner });
return await runSingleStep(task, { llm });
}
Note

Das sieht nicht „agentic“ aus. Es sieht operierbar aus. Genau das ist der Punkt.


Failure evidence (wie es aussieht, wenn es bricht)

Single-Step failt als „eine schlechte Entscheidung mit sofortigem Blast Radius“.

Ein Trace, der den Incident in 5 Zeilen erklärt:

JSON
{"run_id":"run_44a1","step":0,"event":"tool_call","tool":"ticket.close","args_hash":"b5d0aa","decision":"allow"}
{"run_id":"run_44a1","step":0,"event":"tool_result","tool":"ticket.close","ok":true}
{"run_id":"run_44a1","step":0,"event":"stop","reason":"success","note":"single-step"}

Example failure case (composite)

Incident

🚨 Incident: Premature ticket closure

System: Single-Step „close resolved tickets“ Agent
Duration: unter 1 Stunde
Impact: 18 Tickets fälschlich geschlossen


What happened

Der Agent hat ticket.close sofort auf Basis eines Snippets ausgeführt. Sarcasm wurde als „resolved“ gelesen.

Der Worst Part: niemand konnte erklären warum. Kein Loop State, keine stop reasons, keine Gelegenheit zu validieren.


Fix

  1. side-effecting Actions zu einem looped runner routen
  2. Tool Gateway Policy + Audit Logs
  3. Approvals für ticket.close

Abwägungen

Trade-offs
  • Ein Loop ist mehr Code als ein Model-Call.
  • Mehr Steps können mehr Latenz bedeuten (Budgets helfen).
  • Du brauchst Observability (aber du brauchtest sie sowieso).

Wann NICHT nutzen

Don’t
  • Wenn du wirklich nur einen deterministischen Transform hast, nenn es keinen Agenten.
  • Wenn du Tool-Feedback und Recovery brauchst, ist Single-Step fragil.
  • Wenn du keine Traces und stop reasons loggen kannst, fix Observability zuerst.

Checklist (Copy-Paste)

Production checklist
  • [ ] Wenn du Seiteneffekte hast, brauchst du einen looped runner
  • [ ] side-effecting Tasks weg von Single-Step routen
  • [ ] Budgets hinzufügen (steps, tool calls, seconds)
  • [ ] Tool Gateway nutzen (deny by default Allowlist)
  • [ ] Tool-Outputs validieren, bevor du handelst
  • [ ] stop reasons zurückgeben (und loggen)
  • [ ] Approvals für Writes

Safe default config

YAML
routing:
  allow_single_step_only_when: "read_only"
budgets:
  max_steps: 25
  max_tool_calls: 12
  max_seconds: 60
tools:
  allow: ["search.read", "kb.read", "http.get"]
writes:
  require_approval: true
stop_reasons:
  return_to_user: true

FAQ

FAQ
Sind Single-Step-Agents jemals okay?
Ja — wenn es keine Tools und keine Seiteneffekte gibt. Dann ist es eine Completion, kein Agent.
Ist ein Loop nicht langsamer?
Kann sein. Grob: Single-Step ist ~1 LLM call + 1 tool call; ein 3-step loop ist ~3 LLM calls + 2 tool calls. Das ist oft ~3× Latenz. Budgets cappen den Worst Case — und Speed ist egal, wenn’s wrong oder unoperierbar ist.
Was ist die minimale Governance für einen Loop?
Step Limits, Tool-Call Budgets, deny-by-default Tool Policy und stop reasons.
Wo bekomme ich ein gutes Loop-Pattern her?
Starte mit einem bounded ReAct-Runner und einem Tool Gateway. Erfinde keinen eigenen Loop ohne Budgets und Traces.

Related

Production takeaway

Production takeaway

What breaks without this

  • ❌ Writes passieren vor Validation
  • ❌ „Recovery“ lebt in Prompts und Tool-Retries
  • ❌ keine stop reasons, die Verhalten erklären

What works with this

  • ✅ Side effects routen zu einem bounded runner
  • ✅ Budgets + Tool Gateway halten Runs kontrollierbar
  • ✅ Failures sind erklärbar (stop reasons + traces)

Minimum to ship

  1. Routing-Regel (read-only kann single-step sein; side effects nicht)
  2. Bounded runner (budgets + stop reasons)
  3. Tool Gateway (deny by default)
  4. Validation layer (vor Writes)

Nicht sicher, ob das dein Fall ist?

Agent gestalten ->
⏱️ 8 Min. LesezeitAktualisiert Mär, 2026Schwierigkeit: ★★★
In OnceOnly umsetzen
Safe defaults for tool permissions + write gating.
In OnceOnly nutzen
# onceonly guardrails (concept)
version: 1
tools:
  default_mode: read_only
  allowlist:
    - search.read
    - kb.read
    - http.get
writes:
  enabled: false
  require_approval: true
  idempotency: true
controls:
  kill_switch: { enabled: true, mode: disable_writes }
audit:
  enabled: true
Integriert: Production ControlOnceOnly
Guardrails für Tool-Calling-Agents
Shippe dieses Pattern mit Governance:
  • Budgets (Steps / Spend Caps)
  • Tool-Permissions (Allowlist / Blocklist)
  • Kill switch & Incident Stop
  • Idempotenz & Dedupe
  • Audit logs & Nachvollziehbarkeit
Integrierter Hinweis: OnceOnly ist eine Control-Layer für Production-Agent-Systeme.
Autor

Diese Dokumentation wird von Engineers kuratiert und gepflegt, die AI-Agenten in der Produktion betreiben.

Die Inhalte sind KI-gestützt, mit menschlicher redaktioneller Verantwortung für Genauigkeit, Klarheit und Produktionsrelevanz.

Patterns und Empfehlungen basieren auf Post-Mortems, Failure-Modes und operativen Incidents in produktiven Systemen, auch bei der Entwicklung und dem Betrieb von Governance-Infrastruktur für Agenten bei OnceOnly.