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.
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.
<?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¿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.