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:
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}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:
- step cap + stop reason
- loop detection (args hash)
- 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_stepspor run - [ ] stop reason
max_stepsloggeado + visible - [ ] step count en traces
- [ ] loop detection / no-progress stop
- [ ] combinado con time + cost budgets
Config segura por defecto (JSON/YAML)
step_limits:
max_steps: 25
stop_reasons:
surface_to_user: true
log: true
FAQ (3–5)
Usado por patrones
Fallos relacionados
- AI Agent Infinite Loop (Detectar + arreglar, con código)
- Explosión de presupuesto (cuando un agente quema dinero) + fixes + código
- Tool Spam Loops (fallo del agente + fixes + código)
- Incidentes de exceso de tokens (prompt bloat) + fixes + código
- Corrupción de respuestas de tools (schema drift + truncation) + código
Gobernanza requerida
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.
Páginas relacionadas (3–6 links)
- Foundations: The ReAct loop explained · Planning vs reactive agents
- Failure: Infinite loop failure · Tool spam loops
- Governance: Budget controls · Kill switch design
- Production stack: Production agent stack