Step limits для AI агентів (зупинити loops) + Код

Step limits — найдешевша страховка від нескінченних loops. Як cap’нути steps, surfacити stop reasons, і не отримати 700 tool calls.
На цій сторінці
  1. Проблема (з реального продакшену)
  2. Чому це ламається в продакшені
  3. 1) “Він зупиниться, коли буде готово” — не стратегія
  4. 2) Без stop reason це не видно
  5. 3) Enforce в run loop
  6. Приклад реалізації (реальний код)
  7. Реальний інцидент (з цифрами)
  8. Компроміси
  9. Коли НЕ варто
  10. Чекліст (можна копіювати)
  11. Безпечний дефолтний конфіг (JSON/YAML)
  12. FAQ (3–5)
  13. Пов’язані сторінки (3–6 лінків)
Інтерактивний флоу
Сценарій:
Крок 1/3: Execution

Action is proposed as structured data (tool + args).

Проблема (з реального продакшену)

Агенти — це loops. Loops хочуть продовжувати.

Без step cap “завершив” означає: агент випадково здався. У проді він не здається.

Чому це ламається в продакшені

1) “Він зупиниться, коли буде готово” — не стратегія

У проді є:

  • flaky tools
  • rate limits
  • partial outages
  • неоднозначні задачі

Неоднозначно + tools + без cap = лог з сотнями викликів.

2) Без stop reason це не видно

Якщо ти бачиш тільки “timeout”, ти не бачиш, що steps “вибухнули”. Stop reasons — це дебаг.

3) Enforce в run loop

Не в UI. Не “майже завжди”. У loop, на кожному step.

Приклад реалізації (реальний код)

Мінімальний step guard:

PYTHON
from dataclasses import dataclass


@dataclass(frozen=True)
class StepPolicy:
  max_steps: int = 25


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


def run(task: str, *, policy: StepPolicy) -> dict:
  steps = 0
  try:
      while True:
          steps += 1
          if steps > policy.max_steps:
              raise StepExceeded("max_steps")

          action = llm_decide(task)  # (pseudo)
          if action.kind != "tool":
              return {"status": "ok", "answer": action.final_answer, "steps": steps}

          obs = call_tool(action.name, action.args)  # (pseudo)
          task = update(task, action, obs)  # (pseudo)

  except StepExceeded as e:
      return {"status": "stopped", "stop_reason": e.stop_reason, "steps": steps}
JAVASCRIPT
export class StepExceeded extends Error {
constructor(stopReason) {
  super(stopReason);
  this.stopReason = stopReason;
}
}

export function run(task, { maxSteps = 25 } = {}) {
let steps = 0;
try {
  while (true) {
    steps += 1;
    if (steps > maxSteps) throw new StepExceeded("max_steps");

    const action = llmDecide(task); // (pseudo)
    if (action.kind !== "tool") return { status: "ok", answer: action.finalAnswer, steps };

    const obs = callTool(action.name, action.args); // (pseudo)
    task = update(task, action, obs); // (pseudo)
  }
} catch (e) {
  if (e instanceof StepExceeded) return { status: "stopped", stopReason: e.stopReason, steps };
  throw e;
}
}

Реальний інцидент (з цифрами)

Агент мав “просто” зібрати список результатів.

У нього не було:

  • step cap
  • loop detection
  • і tool повертав трохи різні результати

Результат:

  • ~700 tool calls за один run
  • ~18 хвилин runtime
  • cost був не максимальний (пощастило), але rate limits і queue delay — реальні

Fix:

  1. step cap + stop reason
  2. loop detection (args hash)
  3. dedupe/caching для повторних queries

Компроміси

  • Step limits інколи зупиняють раніше. Це краще, ніж unbounded.
  • Якщо cap занадто низький, буде більше “stopped” → потрібна UX.
  • Step limits без time/cost budgets — неповно (але вже корисно).

Коли НЕ варто

  • Чесно: завжди краще мати cap. Якщо ти не хочеш max_steps, тобі потрібні інші жорсткі budgets.

Чекліст (можна копіювати)

  • [ ] max_steps на run
  • [ ] Stop reason max_steps логувати + показувати
  • [ ] Step count у traces
  • [ ] loop detection / no-progress stop
  • [ ] комбінувати з time + cost budgets

Безпечний дефолтний конфіг (JSON/YAML)

YAML
step_limits:
  max_steps: 25
stop_reasons:
  surface_to_user: true
  log: true

FAQ (3–5)

Який max_steps ставити?
Стартуй з 25. Міряй stops. Якщо часто stop — проблема в scope/tools/prompt, а не в цифрі.
Max_steps сам по собі достатній?
Ні. Додай max_seconds, max_tool_calls і часто max_usd.
Як називати stop reason?
Коротко і машинно: max_steps. Тобі потрібні алерти і дашборди.

Q: Який max_steps ставити?
A: Стартуй з 25. Міряй stops. Якщо часто stop — проблема в scope/tools/prompt, а не в цифрі.

Q: Max_steps сам по собі достатній?
A: Ні. Додай max_seconds, max_tool_calls і часто max_usd.

Q: Як називати stop reason?
A: Коротко і машинно: max_steps. Тобі потрібні алерти і дашборди.

Пов’язані сторінки (3–6 лінків)

Не впевнені, що це ваш кейс?

Спроєктувати агента →
⏱️ 4 хв читанняОновлено Бер, 2026Складність: ★★★
Реалізувати в OnceOnly
Budgets + permissions you can enforce at the boundary.
Використати в OnceOnly
# onceonly guardrails (concept)
version: 1
budgets:
  max_steps: 25
  max_tool_calls: 12
  max_seconds: 60
  max_usd: 1.00
policy:
  tool_allowlist:
    - search.read
    - http.get
writes:
  require_approval: true
  idempotency: true
controls:
  kill_switch: { enabled: true }
Інтегровано: продакшен-контрольOnceOnly
Додай guardrails до агентів з tool-calling
Зашип цей патерн з governance:
  • Бюджетами (кроки / ліміти витрат)
  • Дозволами на інструменти (allowlist / blocklist)
  • Kill switch та аварійна зупинка
  • Ідемпотентність і dedupe
  • Audit logs та трасування
Інтегрована згадка: OnceOnly — контрольний шар для продакшен агент-систем.
Автор

Цю документацію курують і підтримують інженери, які запускають AI-агентів у продакшені.

Контент створено з допомогою AI, із людською редакторською відповідальністю за точність, ясність і продакшн-релевантність.

Патерни та рекомендації базуються на постмортемах, режимах відмов і операційних інцидентах у розгорнутих системах, зокрема під час розробки та експлуатації governance-інфраструктури для агентів у OnceOnly.