Módulo 6: El loop del agente SQL

Cuándo parar y el tope de iteraciones

Descripción

Un agente que empieza un ciclo tiene que saber cuándo salir de él. Esta cápsula se ocupa de las dos únicas formas en que el loop termina, y de por qué la segunda es obligatoria:

  1. El modelo responde sin pedir más herramientas. Es la terminación natural: el agente tiene lo que necesita y redacta la respuesta (stop_reason == "end_turn"). El while sale limpiamente.
  2. Se agota el tope de iteraciones. Es la red de seguridad: si el modelo entra en un bucle que no converge —por ejemplo, pide una y otra vez un SQL que siempre falla—, el tope corta el ciclo para que el programa no se cuelgue.

Verás las dos ejecutadas: un agente que termina porque responde, y un agente atrapado en un SQL roto que solo se detiene cuando el tope lo obliga. La lección de fondo: un agente sin tope es un programa que puede no terminar nunca, y eso es inaceptable en producción.

Conexión con el módulo

El tope es el max_iters que viste desde la cápsula 02, pero aquí lo miramos de frente. Es la diferencia entre "repite hasta la respuesta" (peligroso) y "repite hasta la respuesta o hasta un límite" (seguro). La cápsula 07 mostrará el caso feliz de un ciclo que converge —el modelo se auto-corrige antes del tope—; esta cápsula muestra qué pasa cuando no converge y por qué el tope tiene que estar ahí.


Analogía: la búsqueda con límite de intentos

Piensa en un cajero automático que te pide el PIN. Te deja intentar, pero no infinitas veces: a los tres intentos fallidos, traga la tarjeta. No es que el cajero "sepa" que no vas a acertar; es que un sistema que permite intentos ilimitados es un sistema que se puede quedar colgado —o abierto a abuso— para siempre. El límite de intentos no es una falta de fe en ti; es una garantía de que el proceso termina.

El tope de iteraciones es ese límite. El agente intenta resolver la pregunta vuelta tras vuelta; si a las max_iters vueltas no llegó, el runner "traga la tarjeta": corta el ciclo y devuelve un mensaje honesto de "no pude". La mayoría de las veces el agente responde mucho antes del tope —igual que tú aciertas el PIN al primer intento—; pero el tope está ahí para el caso en que no, y ese caso, en un sistema real, ocurre.


Forma 1 de parar: el modelo responde

Es la terminación que ya conoces. Cuando el turno del modelo es una respuesta de texto (en la Messages API real, stop_reason == "end_turn"; en nuestro guion, {"type": "text", ...}), el runner hace return y el ciclo termina. Recordemos la parte del runner que lo maneja:

if turn["type"] == "text":                      # el modelo responde -> FIN
    print(f"  [paso {step}] el modelo responde (sin pedir tools) -> FIN")
    print(f"Respuesta: {turn['text']}")
    return turn["text"]

Corramos el caso feliz y miremos también qué devuelve el runner:

con = open_reservo()
final = run_agent("¿Cuántas reservas confirmadas hay?", con, [
    {"type": "tool_use", "tool_calls": [
        {"id": "t1", "name": "run_sql",
         "input": {"query": "SELECT COUNT(*) AS n FROM bookings WHERE status = 'confirmed'"}}]},
    {"type": "text", "text": "Hay 20 reservas confirmadas en Reservo."}])
print(f"(devuelto): {final!r}")
con.close()

Qué esperar:

Pregunta: ¿Cuántas reservas confirmadas hay?
  [paso 1] tool_use run_sql("SELECT COUNT(*) AS n FROM bookings WHERE status = 'confirmed'")
            -> 1 fila(s): [[20]]
  [paso 2] el modelo responde (sin pedir tools) -> FIN
Respuesta: Hay 20 reservas confirmadas en Reservo.
(devuelto): 'Hay 20 reservas confirmadas en Reservo.'

El ciclo salió en el paso 2 porque el modelo respondió. El runner devuelve el texto de la respuesta ('Hay 20 reservas confirmadas en Reservo.'), que es lo que el asistente le mostraría al usuario. Salió por la puerta buena: el modelo tuvo lo que necesitaba y respondió. Este es el 95 % de los casos.


Forma 2 de parar: el tope de iteraciones

Ahora el caso que justifica el tope. Imagina que el modelo, por lo que sea, insiste en un SQL que siempre falla —una columna que no existe— y no cambia de estrategia. Sin un tope, el runner ejecutaría ese SQL roto, devolvería el error, el modelo pediría el mismo SQL, y así para siempre. El tope lo impide. Recordemos la parte del runner que lo maneja: es el propio for con su límite, y el mensaje final cuando el for se acaba sin que el modelo haya respondido:

    for step in range(1, max_iters + 1):
        # ... si el modelo nunca responde, el for llega a su fin ...
    print(f"  [tope] {max_iters} iteraciones alcanzadas -> FIN forzado")
    return "No pude resolverlo dentro del tope de pasos."

Simulemos un modelo atascado: un guion donde todos los turnos piden el mismo SQL roto, con un tope bajo (max_iters=3) para verlo saltar:

con = open_reservo()
stuck = {"type": "tool_use", "tool_calls": [
    {"id": "t", "name": "run_sql", "input": {"query": "SELECT SUM(total) FROM bookings"}}]}
final = run_agent("¿Cuánto ingresó Reservo en total?", con,
                  [stuck, stuck, stuck, stuck, stuck], max_iters=3)
print(f"(devuelto): {final!r}")
con.close()

Qué esperar:

Pregunta: ¿Cuánto ingresó Reservo en total?
  [paso 1] tool_use run_sql('SELECT SUM(total) FROM bookings')
            -> ERROR: OperationalError: no such column: total
  [paso 2] tool_use run_sql('SELECT SUM(total) FROM bookings')
            -> ERROR: OperationalError: no such column: total
  [paso 3] tool_use run_sql('SELECT SUM(total) FROM bookings')
            -> ERROR: OperationalError: no such column: total
  [tope] 3 iteraciones alcanzadas -> FIN forzado
(devuelto): 'No pude resolverlo dentro del tope de pasos.'

Lee las tres vueltas: el modelo pide el mismo SELECT SUM(total) cada vez, el runner lo ejecuta, run_sql devuelve el mismo no such column: total, y el modelo —en este guion— nunca cambia de idea. A la tercera vuelta, el for se agota: el runner imprime [tope] 3 iteraciones alcanzadas -> FIN forzado y devuelve un mensaje honesto. El ciclo terminó, no porque el problema se resolviera, sino porque el tope lo cortó. Sin ese tope, este programa no habría terminado jamás.

Este es un guion pesimista a propósito. En la práctica, claude-sonnet-5 casi nunca se queda atrapado así: al ver el error no such column: total, lo normal es que corrija a price_cents en la vuelta siguiente (eso es la cápsula 07). Pero "casi nunca" no es "nunca", y un sistema de producción no puede depender de que el modelo siempre coopere. El tope es la garantía de terminación que no depende del comportamiento del modelo.


Elegir un buen tope

¿Cuánto vale max_iters? Es un balance:

  • Demasiado bajo (por ejemplo, 2): cortas ciclos legítimos. Una pregunta que necesita explorar el esquema (paso 1), consultar (paso 2), corregir un error (paso 3) y responder (paso 4) necesitaría 4 vueltas; con un tope de 2 la matarías antes de tiempo.
  • Demasiado alto (por ejemplo, 100): si el modelo se atasca, gastas 100 llamadas a la API (caras y lentas) antes de rendirte. El usuario espera de más y la factura sube.

Un valor razonable para un agente SQL está entre 5 y 10: suficiente para explorar, consultar, y corregirse una o dos veces, pero no tanto como para arder si el modelo cicla. El valor exacto depende de qué tan complejas son tus preguntas y cuánto esquema le precargas al modelo (menos exploración → topes más bajos). En este módulo usamos max_iters=6 por defecto.

Una sutileza: el tope cuenta vueltas del loop, no herramientas. Si en una vuelta el modelo pide tres describe_table a la vez (como en la cápsula 05), esas tres cuentan como una vuelta. El tope acota el número de veces que el modelo "decide", no el número de herramientas ejecutadas.

Frontera con AI Engineering. Estrategias más finas para cuándo parar —detectar que el agente está repitiendo la misma acción, presupuestos de tokens (task_budget), planificación que evita ciclos— son materia del diseño de agentes en general, del ecosistema de AI Engineering. Aquí nos quedamos con el tope de iteraciones, que es la garantía de terminación más simple y la que todo agente SQL necesita como piso.


Terminar no es acertar (otra vez)

Vale la pena repetirlo, porque esta cápsula lo hace evidente: el loop puede terminar de las dos formas y en ninguna hay garantía de que la respuesta sea correcta.

  • Cuando termina por respuesta del modelo, el modelo pudo haber contado mal: respondería "hay 15 reservas confirmadas" (falso; son 20) y el ciclo saldría igual de contento.
  • Cuando termina por tope, la respuesta es explícitamente un "no pude" —honesto, pero tampoco es la respuesta correcta—.

El tope garantiza terminación; la validación (M4) garantiza que no corres SQL roto; pero ninguno garantiza corrección. Medir si la respuesta es verdadera —comparar lo que el agente devolvió con lo que la pregunta pedía— es el Módulo 7. Un agente que siempre termina y nunca acierta es un mal agente que, sin embargo, pasa el chequeo de "¿el loop salió?". Por eso hacen falta las dos cosas: que el ciclo termine (M6) y que la respuesta sea correcta (M7).


Errores comunes

  1. Un while True sin tope. El error que esta cápsula existe para prevenir. "Repite hasta que el modelo responda" es una bomba de tiempo: un modelo que se atasca —o un guion mal armado— hace que el programa no termine nunca. Siempre un tope (for step in range(...), o un contador que se compare contra un máximo).

  2. Un tope demasiado bajo que corta ciclos legítimos. Si tus preguntas necesitan explorar + consultar + corregir, un tope de 2 o 3 las mata antes de que converjan. El tope debe dar aire suficiente para los casos normales (5-10), no solo para los triviales.

  3. Creer que llegar al tope significa un bug. No siempre. Un agente bien hecho llega al tope ocasionalmente con preguntas genuinamente difíciles o ambiguas, y devolver "no pude" es la respuesta correcta ahí. Lo que sí es un bug es llegar al tope con frecuencia en preguntas simples: eso apunta a un modelo mal prompteado o herramientas mal descritas.

  4. No devolver nada útil al agotar el tope. Si el runner simplemente termina sin devolver un mensaje, el asistente le muestra al usuario una pantalla en blanco. Devolver un "no pude resolverlo" honesto es parte de terminar bien.

  5. Confundir "terminó" con "acertó". Que el ciclo salga —por respuesta o por tope— no dice nada sobre la corrección. El tope evita colgarse; la evaluación (M7) mide el acierto. Son problemas distintos.


Ejercicios

Ejercicio 1: ¿Por qué el tope? (Fácil)

Sin ejecutar nada: en el ejemplo del agente atascado, ¿qué habría pasado si run_agent usara while True en vez de for step in range(1, max_iters + 1)? ¿Y por qué el modelo real (claude-sonnet-5) probablemente no se habría atascado?

Ver solución

Con while True, el runner ejecutaría el mismo SELECT SUM(total) roto, devolvería el error, el guion daría otra vez el mismo turno de tool_use, y así para siempre —el programa nunca terminaría, colgado en un bucle infinito—. El for step in range(...) lo impide: a las max_iters vueltas, el for se acaba solo. El modelo real probablemente no se atascaría porque, al ver no such column: total en el tool_result, lo normal es que corrija a price_cents en la vuelta siguiente (cápsula 07). El guion aquí es pesimista a propósito, para mostrar por qué el tope es necesario aunque el modelo casi siempre coopere: un sistema de producción no puede depender de esa cooperación.

Ejercicio 2: Un tope que da justo (Medio)

Corre el agente de Ana Torres de la cápsula 05 (dos pasos + respuesta) con max_iters=3. ¿Termina bien? ¿Y con max_iters=2? Explica la diferencia.

Ver solución

Con max_iters=3, el agente termina bien: paso 1 (describe_table), paso 2 (run_sql), paso 3 (respuesta). Justo entra. Con max_iters=2, el for solo llega hasta el paso 2: ejecuta la exploración y la consulta, pero nunca llega al turno de respuesta —el for se agota antes—, así que imprime [tope] 2 iteraciones alcanzadas -> FIN forzado y devuelve "No pude resolverlo dentro del tope de pasos.". La diferencia: la pregunta necesita tres vueltas (explorar, consultar, responder), y un tope de 2 la corta una vuelta antes de que el modelo pueda dar su respuesta. La moraleja: el tope debe cubrir la vuelta de respuesta, no solo las de herramienta. (Puedes comprobarlo cambiando max_iters en la llamada a run_agent del ejemplo de Ana Torres.)

Ejercicio 3: Contar vueltas vs. herramientas (Difícil)

Un agente resuelve una pregunta así: vuelta 1 pide dos describe_table a la vez; vuelta 2 pide un run_sql; vuelta 3 responde. ¿Cuántas vueltas del loop consumió? ¿Cuántas ejecuciones de herramienta? ¿Qué max_iters mínimo necesita para no cortarse?

Ver solución

Consumió 3 vueltas del loop (una por cada iteración del for step): la vuelta 1 con las dos exploraciones, la vuelta 2 con la consulta, la vuelta 3 con la respuesta. Pero hubo 3 ejecuciones de herramienta (dos describe_table + un run_sql) —las dos exploraciones comparten la vuelta 1—. El max_iters mínimo para no cortarse es 3, porque el tope cuenta vueltas del loop, no herramientas: aunque se ejecutaron tres herramientas, solo hicieron falta tres decisiones del modelo. Este es el matiz clave del tope: acota cuántas veces el modelo decide, no cuántas herramientas corren. Pedir varias herramientas en una sola vuelta (cápsula 05) es, entre otras cosas, una forma de resolver más con menos vueltas.


Resumen y siguiente paso

  • El loop termina de dos formas: el modelo responde (terminación natural, stop_reason == "end_turn") o se agota el tope de iteraciones (red de seguridad).
  • El tope no es opcional: un agente sin tope es un programa que puede no terminar nunca. El for step in range(1, max_iters + 1) es la garantía de terminación que no depende de que el modelo coopere.
  • Un buen tope da aire para los casos normales (explorar + consultar + corregir → 5-10 vueltas) sin arder si el modelo cicla. Cuenta vueltas del loop, no herramientas.
  • Al agotar el tope, el runner devuelve un "no pude" honesto en vez de colgarse o dejar la pantalla en blanco.
  • Terminar no es acertar. El tope garantiza terminación; la validación evita SQL roto; pero la corrección de la respuesta se mide en el Módulo 7.

Siguiente cápsula: Manejo de errores dentro del loop — Verás el caso feliz del ciclo que sí converge: el SQL falla, el error vuelve al modelo como tool_result, y el modelo lo lee y pide run_sql de nuevo con el SQL corregido —la auto-corrección de M4, ahora dentro del loop del agente—, ejecutada de punta a punta contra Reservo.


Recursos adicionales

  1. Python — range — El for ... in range(1, max_iters + 1) que acota el ciclo y garantiza terminación.
  2. Claude — Tool use loop — Cuándo el modelo devuelve stop_reason == "end_turn" (responde) vs. tool_use (pide otra herramienta).
  3. Claude — Task budgets — Una estrategia más avanzada para acotar el gasto de un agente (frontera con AI Engineering); aquí basta el tope de iteraciones.
  4. BIRD: exactitud de ejecución en text-to-SQL — Por qué "el loop terminó" no es lo mismo que "la respuesta es correcta"; el problema que mide el Módulo 7.