Step limits para agentes IA (parar loops) + Código

Los step limits son el seguro más barato contra loops. Cappea steps, expone stop reasons, y evita el run de 700 tool calls.
En esta página
  1. El problema (en producción)
  2. Por qué esto se rompe en producción
  3. 1) “Parará cuando esté listo” no es un plan
  4. 2) Sin stop reason, no se ve
  5. 3) Enforce en el run loop
  6. Ejemplo de implementación (código real)
  7. Incidente real (con números)
  8. Trade-offs
  9. Cuándo NO usarlo
  10. Checklist (copiar/pegar)
  11. Config segura por defecto (JSON/YAML)
  12. FAQ (3–5)
  13. Páginas relacionadas (3–6 links)
Flujo interactivo
Escenario:
Paso 1/3: Execution

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

El problema (en producción)

Los agentes son loops. Los loops quieren seguir.

Sin step cap, “terminó” significa: el agente se rinde por casualidad. En prod, no se rinde.

Por qué esto se rompe en producción

1) “Parará cuando esté listo” no es un plan

En prod hay:

  • tools flaky
  • rate limits
  • outages parciales
  • tareas ambiguas

Ambiguo + tools + sin cap = logs gigantes.

2) Sin stop reason, no se ve

Si solo ves “timeout”, no sabes que explotaron los steps. Stop reasons ayudan a debuggear.

3) Enforce en el run loop

No en la UI. No “casi siempre”. En el loop, siempre.

Ejemplo de implementación (código real)

Un step guard mínimo:

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;
}
}

Incidente real (con números)

Un agente “solo” tenía que juntar una lista de resultados.

No tenía:

  • step caps
  • loop detection
  • y un tool devolvía resultados levemente distintos

Resultado:

  • ~700 tool calls en un run
  • ~18 minutos de runtime
  • el costo no fue enorme (suerte), pero rate limits y delays sí

Fix:

  1. step cap + stop reason
  2. loop detection (args hash)
  3. dedupe/caching para queries repetidas

Trade-offs

  • Step limits cortan temprano a veces. Mejor que sin límite.
  • Si lo pones demasiado bajo, tendrás más “stopped” → necesitas UX.
  • Step limits sin time/cost budgets es incompleto (pero útil).

Cuándo NO usarlo

  • En serio: siempre usa un cap. Si no quieres max_steps, necesitas otros budgets duros.

Checklist (copiar/pegar)

  • [ ] max_steps por run
  • [ ] stop reason max_steps loggeado + visible
  • [ ] step count en traces
  • [ ] loop detection / no-progress stop
  • [ ] combinado con time + cost budgets

Config segura por defecto (JSON/YAML)

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

FAQ (3–5)

¿Qué max_steps pongo?
Empieza en 25. Mide stops. Si corta seguido, el problema suele ser scope/tools/prompt, no el número.
¿Max_steps solo alcanza?
No. Agrega max_seconds, max_tool_calls y muchas veces max_usd.
¿Cómo nombro el stop reason?
Corto y machine-friendly: max_steps. Querrás alertas y dashboards.

P: ¿Qué max_steps pongo?
R: Empieza en 25. Mide stops. Si corta seguido, el problema suele ser scope/tools/prompt, no el número.

P: ¿Max_steps solo alcanza?
R: No. Agrega max_seconds, max_tool_calls y muchas veces max_usd.

P: ¿Cómo nombro el stop reason?
R: Corto y machine-friendly: max_steps. Querrás alertas y dashboards.

No sabes si este es tu caso?

Disena tu agente ->
⏱️ 4 min de lecturaActualizado Mar, 2026Dificultad: ★★★
Implementar en OnceOnly
Budgets + permissions you can enforce at the boundary.
Usar en 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 }
Integrado: control en producciónOnceOnly
Guardrails para agentes con tool-calling
Lleva este patrón a producción con gobernanza:
  • Presupuestos (pasos / topes de gasto)
  • Permisos de herramientas (allowlist / blocklist)
  • Kill switch y parada por incidente
  • Idempotencia y dedupe
  • Audit logs y trazabilidad
Mención integrada: OnceOnly es una capa de control para sistemas de agentes en producción.
Autor

Esta documentación está curada y mantenida por ingenieros que despliegan agentes de IA en producción.

El contenido es asistido por IA, con responsabilidad editorial humana sobre la exactitud, la claridad y la relevancia en producción.

Los patrones y las recomendaciones se basan en post-mortems, modos de fallo e incidentes operativos en sistemas desplegados, incluido durante el desarrollo y la operación de infraestructura de gobernanza para agentes en OnceOnly.