Ir al contenido principal
Laravel, thinking fast.

Capitulo 8

El bucle del agente

Julian Beaujardin

Una llamada a un modelo es una pregunta.

Un bucle alrededor de esa llamada —enviar, leer la respuesta, actuar sobre ella, volver a enviar— es un agente. Ese bucle son unas cuarenta líneas de código, y casi todas existen porque algo salió mal. Este capítulo son esas cuarenta líneas.

Si te saltas todo lo demás de este libro, lee esto. Es la parte que de verdad se diferencia del trabajo habitual con Laravel, y es la parte donde un código de aspecto plausible tiene más probabilidades de estar sutilmente mal.

La forma

$maxIterations = 10;
$iteration = 0;
$history = $this->conversationHistory;
$deadline = now()->addSeconds($this->timeout - 20);

while ($iteration < $maxIterations) {
    $iteration++;

    if (Cache::has(self::cancelKey($this->cacheKey))) {
        $state['stopped'] = true;
        break;
    }

    if (now()->greaterThan($deadline)) {
        report(new \RuntimeException('Hit its wall-clock budget after '.$iteration.' iterations'));
        break;
    }

    $response = $this->callClaudeAPI($history, $deadline);

    // … handle the response …
}

Cuatro salidas antes siquiera de llamar al modelo: contador de iteraciones, cancelación del usuario, presupuesto de reloj y —dentro de la llamada— la negativa a empezar un trabajo que no se puede terminar. El capítulo 6 explicó por qué los relojes tienen esa forma. Lo que sigue es qué pasa con la respuesta.

stop_reason es tu control de flujo

Toda respuesta dice por qué se detuvo la generación, y ese valor no es información de diagnóstico. Es la condición de bifurcación de todo el bucle.

Ignorarlo es la forma más común de equivocarse con un bucle de agente, porque un bucle que lo ignora parece funcionar. Maneja bien el caso común y maneja mal otros cuatro en silencio.

| stop_reason | Significado | Qué debe hacer el bucle | |---|---|---| | end_turn | El modelo terminó | Salir. Mostrar lo que hay. | | (uso de herramienta) | Quiere ejecutar una herramienta | Continuar: devolver los resultados | | pause_turn | Un turno largo se suspendió | Continuar: reenviar el historial sin cambios | | max_tokens | La salida se cortó | Parar y decirle al usuario que se truncó | | refusal | La seguridad declinó | Parar y decirlo: esto no es un error |

Vamos por el orden en que te van a morder.

pause_turn, y por qué importa el orden

Cuando el conjunto de herramientas se ejecuta del lado del proveedor, un turno largo puede suspenderse a medias y devolverse para que lo reanudes. La respuesta trae pause_turn, algo de contenido y —crucialmente— ninguna llamada a herramienta sobre la que actuar.

Ese último detalle es una trampa, y este es el fallo que provoca. La condición de salida natural de un bucle de agente es «para cuando el modelo haya terminado o no haya pedido nada»:

if ($stopReason === 'end_turn' || empty($toolUseBlocks)) {
    break;
}

Una respuesta pause_turn no tiene bloques de uso de herramientas. Cae en empty($toolUseBlocks), el bucle sale, y un turno que sólo estaba pausado se trata como terminado. El usuario ve una respuesta a medias y ningún error en absoluto.

Así que la comprobación tiene que ir primero:

// The MCP toolset runs server-side, so a long turn can pause at the server's
// iteration limit. Re-sending the history resumes it — there are no tool_use
// blocks to act on, so this must be checked before the end_turn/no-tools exit.
if ($stopReason === 'pause_turn') {
    continue;
}

if ($stopReason === 'end_turn' || empty($toolUseBlocks)) {
    break;
}

Reanudar es simplemente continuar: añade lo que volvió al historial y vuelve a enviarlo. Sin carga especial, sin token de reanudación.

Ordena las ramas de razón de parada de más específica a menos, y pon todos los casos de «continuar» por encima de todos los de «salir». Una condición de salida que es un superconjunto de una de continuación se la tragará, y lo hará en silencio.