Tema 6 · Agentes Backend

6. Flujo de Ejecución de Agentes: Bucle de Herramientas y Ejecución Segura

Implementa el ciclo completo de ejecución de herramientas con listas blancas de seguridad, manejo de errores y respuesta enriquecida al cliente.

El Bucle de Ejecución de Agentes (The Agentic Loop)

Para que Function Calling funcione en una aplicación real, tu backend debe orquestar una conversación de dos o más turnos. Cuando el LLM emite una o más llamadas a herramientas en tool_calls, el servidor debe interceptarlas, ejecutar el código correspondiente de forma segura y volver a llamar a la API del modelo incluyendo los resultados para que redacte la respuesta final.

🛡️ Regla de Seguridad Indispensable (Lista Blanca): Jamás uses funciones dinámicas peligrosas como call_user_func($toolName) o eval() directamente con el nombre enviado por la IA. Mapea el nombre de la tool contra un diccionario cerrado y explícito de funciones autorizadas.

Anatomía del Mensaje de Retorno de Herramienta

Para que el LLM comprenda qué resultado corresponde a cada llamada previa, la respuesta debe incluir dos atributos obligatorios:

  • role: "tool": Indica que el contenido proviene de la ejecución del sistema.
  • tool_call_id: La cadena única (ej: call_abc123xyz) que el LLM generó originalmente para asociar la llamada con su resultado.
  • content: Cadena de texto en formato JSON serializado con el resultado o error controlado de la operación.

⚡ Orquestador Completo de Function Calling en PHP 8.3 con PDO

Script de backend que procesa una solicitud, despacha tool_calls contra la base de datos y entrega la respuesta final al cliente.

agent-tools-executor.php
Solo lectura
<?php
declare(strict_types=1);

namespace App\Agent;

class AgentOrchestrator
{
    private \PDO $db;
    private string $apiKey;

    public function __construct(\PDO $db, string $apiKey)
    {
        $this->db = $db;
        $this->apiKey = $apiKey;
    }

    /**
     * Diccionario estricto de funciones permitidas (Lista Blanca de Seguridad).
     */
    private function executeSafeTool(string $name, array $args): array
    {
        return match ($name) {
            'consultar_precio_producto' => $this->toolConsultarPrecio($args['sku'] ?? ''),
            'consultar_pedidos_cliente' => $this->toolConsultarPedidos((int)($args['cliente_id'] ?? 0)),
            default => ['error' => "Herramienta '{$name}' no autorizada o desconocida."],
        };
    }

    private function toolConsultarPrecio(string $sku): array
    {
        $stmt = $this->db->prepare('SELECT id, nombre, precio, stock FROM productos WHERE sku = :sku LIMIT 1');
        $stmt->execute(['sku' => $sku]);
        $prod = $stmt->fetch(\PDO::FETCH_ASSOC);

        if (!$prod) {
            return ['status' => 'not_found', 'message' => "No existe ningún producto con el SKU {$sku}"];
        }

        return ['status' => 'success', 'data' => $prod];
    }

    private function toolConsultarPedidos(int $clienteId): array
    {
        $stmt = $this->db->prepare('SELECT id, total, estado, fecha FROM pedidos WHERE cliente_id = :cid ORDER BY id DESC LIMIT 3');
        $stmt->execute(['cid' => $clienteId]);
        return ['status' => 'success', 'pedidos' => $stmt->fetchAll(\PDO::FETCH_ASSOC)];
    }

    /**
     * Bucle de ejecución de agente (máximo 3 turnos para evitar bucles infinitos).
     */
    public function handleUserQuery(string $userPrompt, array $toolsDefinition): string
    {
        $messages = [
            ['role' => 'system', 'content' => 'Eres el asistente comercial de la tienda. Consulta datos antes de responder.'],
            ['role' => 'user', 'content' => $userPrompt]
        ];

        for ($iteration = 0; $iteration < 3; $iteration++) {
            $response = $this->callOpenAI($messages, $toolsDefinition);
            $message = $response['choices'][0]['message'];

            // Si el modelo no solicitó ninguna herramienta, retornamos su texto final
            if (empty($message['tool_calls'])) {
                return (string)($message['content'] ?? 'Operación finalizada.');
            }

            // Agregamos la decisión del modelo al historial
            $messages[] = $message;

            // Ejecutamos cada herramienta solicitada
            foreach ($message['tool_calls'] as $toolCall) {
                $funcName = $toolCall['function']['name'];
                $funcArgs = json_decode($toolCall['function']['arguments'], true) ?? [];
                
                // Ejecución segura contra base de datos
                $result = $this->executeSafeTool($funcName, $funcArgs);

                // Inyectamos el resultado con su ID correspondiente
                $messages[] = [
                    'role'         => 'tool',
                    'tool_call_id' => $toolCall['id'],
                    'content'      => json_encode($result, JSON_UNESCAPED_UNICODE)
                ];
            }
        }

        return 'Se alcanzó el límite de llamadas consecutivas de herramientas.';
    }

    private function callOpenAI(array $messages, array $tools): array
    {
        $ch = curl_init('https://api.openai.com/v1/chat/completions');
        curl_setopt_array($ch, [
            CURLOPT_POST           => true,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER     => [
                'Content-Type: application/json',
                'Authorization: Bearer ' . $this->apiKey
            ],
            CURLOPT_POSTFIELDS     => json_encode([
                'model'       => 'gpt-4o-mini',
                'messages'    => $messages,
                'tools'       => $tools,
                'temperature' => 0.1
            ]),
            CURLOPT_TIMEOUT        => 30
        ]);
        $res = curl_exec($ch);
        curl_close($ch);
        return json_decode((string)$res, true) ?? [];
    }
}
🧠
Mini-Quiz de Retención
Comprueba tu comprensión antes de pasar a la siguiente lección
1 Pregunta
Pregunta 1
¿Por qué es fundamental asociar el tool_call_id en la respuesta con role: "tool" al reenviar los datos al LLM?
🦉
¿Tienes dudas con este tema?
Sócrates AI puede darte analogías y explicarte este código paso a paso.

🚀 ¿Quieres dominar IA Aplicada a Desarrollo Web: APIs, RAG & Agentes y crear proyectos reales?

Aprende arquitectura limpia, desarrollo web moderno, buenas prácticas y despliegue en producción con nuestro Bootcamp Fullstack Developer con mentorías en vivo.

🏛️ Autoría y Propiedad Intelectual: Obra educativa original de David Amador para Kilopolis Academia (Solucionáticos), estructurada sobre las especificaciones y estándares abiertos del W3C, el estándar vivo de WHATWG y la documentación oficial de código abierto. © Todos los derechos reservados.