Web Search en Agentes IA con Laravel: Búsqueda en Vivo
Web Search en Agentes IA con Laravel: Búsqueda en Vivo
Los agentes de IA son herramientas poderosas para automatizar tareas complejas, pero tienen una limitación fundamental: su conocimiento se detiene en la fecha de entrenamiento. Si necesitas que tu agente responda preguntas sobre eventos actuales, precios en tiempo real o información que cambia constantemente, una base de conocimiento estática no es suficiente.
En este artículo, exploraremos cómo integrar búsqueda web en vivo en tus agentes IA dentro de Laravel, permitiendo que accedan a información actualizada mientras mantienen control y seguridad sobre qué dominios pueden consultar.
El Problema: Conocimiento Obsoleto en Agentes IA
Imagina que has construido un asistente de servicio al cliente que puede responder preguntas sobre tu empresa. Tu base de conocimiento contiene información sobre políticas de envío, horarios, y productos disponibles. Todo funciona bien hasta que:
- Un cliente pregunta: “¿Cuál es el estado actual de mi pedido?”
- Tu agente responde desde la base de conocimiento, pero no tiene acceso a datos en tiempo real
- El cliente queda insatisfecho
O peor aún:
- Un cliente pregunta: “¿Cuánto cuesta el envío a Barcelona hoy?”
- Tu agente no puede acceder a tasas de envío actualizadas
- Proporciona información obsoleta
Aquí es donde entra la búsqueda web: extender los agentes IA con herramientas que les permitan consultar información actualizada en tiempo real.
Conceptos Clave: Tools en Agentes IA
Un agente IA funciona con “herramientas” (tools) que puede usar para completar tareas. Estas herramientas son funciones que el agente puede llamar cuando las necesita. Las herramientas comunes incluyen:
- Knowledge Base Tool: Consulta tu documentación interna
- Database Tool: Accede a datos de tu aplicación
- Web Search Tool: Busca información en internet
- API Tool: Llama endpoints específicos
Las herramientas se definen con un esquema JSON que describe:
- Qué hace la herramienta
- Qué parámetros acepta
- Qué retorna
Cuando el agente necesita resolver una tarea, decide qué herramientas usar y en qué orden.
Implementar Web Search en Laravel
Paso 1: Crear la Herramienta de Búsqueda Web
Primero, crearemos una clase que encapsule la lógica de búsqueda web:
<?php
namespace App\Services\AI;
use Illuminate\Support\Facades\Http;
class WebSearchTool
{
private array $allowedDomains = [
'wikipedia.org',
'stackoverflow.com',
'github.com',
'laravel.com',
'php.net',
'weather.com',
'news.google.com',
];
public function search(string $query): array
{
// Validar que la búsqueda sea segura
if (strlen($query) > 500) {
return [
'error' => 'Consulta demasiado larga',
'results' => [],
];
}
try {
$response = Http::timeout(10)
->get('https://api.search.brave.com/res/v1/web/search', [
'q' => $query,
'count' => 5,
'key' => config('services.brave_search.key'),
])
->json();
if (!isset($response['web'])) {
return ['results' => []];
}
$filtered = collect($response['web'])
->map(fn($result) => [
'title' => $result['title'] ?? null,
'url' => $result['url'] ?? null,
'description' => $result['description'] ?? null,
])
->filter(fn($item) => $this->isDomainAllowed($item['url']))
->take(3)
->values()
->toArray();
return [
'results' => $filtered,
'query' => $query,
];
} catch (\Exception $e) {
return [
'error' => 'No se pudo realizar la búsqueda',
'results' => [],
];
}
}
private function isDomainAllowed(string $url): bool
{
$host = parse_url($url, PHP_URL_HOST);
foreach ($this->allowedDomains as $domain) {
if (str_ends_with($host, $domain)) {
return true;
}
}
return false;
}
public function getDefinition(): array
{
return [
'name' => 'web_search',
'description' => 'Busca información actualizada en internet sobre un tema específico',
'parameters' => [
'type' => 'object',
'properties' => [
'query' => [
'type' => 'string',
'description' => 'La consulta de búsqueda (máximo 500 caracteres)',
],
],
'required' => ['query'],
],
];
}
}
Paso 2: Configurar las Variables de Entorno
Añade tus credenciales de API en .env:
BRAVE_SEARCH_KEY=tu_clave_api_aqui
AI_ALLOWED_DOMAINS=wikipedia.org,stackoverflow.com,github.com,laravel.com,php.net
AI_MAX_SEARCH_RESULTS=3
AI_SEARCH_TIMEOUT=10
Paso 3: Crear el Agente IA con Herramienta de Búsqueda
Ahora crearemos un agente que pueda usar la herramienta de búsqueda:
<?php
namespace App\Services\AI;
use Illuminate\Support\Facades\Http;
class AIAgent
{
private WebSearchTool $webSearchTool;
private string $apiKey;
private string $model = 'gpt-4o-mini';
public function __construct()
{
$this->webSearchTool = new WebSearchTool();
$this->apiKey = config('services.openai.api_key');
}
public function chat(string $message, array $context = []): string
{
$messages = $this->buildMessages($message, $context);
$tools = $this->getAvailableTools();
$response = Http::withToken($this->apiKey)
->post('https://api.openai.com/v1/chat/completions', [
'model' => $this->model,
'messages' => $messages,
'tools' => $tools,
'tool_choice' => 'auto',
'max_tokens' => 4096,
])
->json();
return $this->processResponse($response, $messages);
}
private function buildMessages(string $message, array $context): array
{
$systemMessage = "Eres un asistente útil y preciso. Tienes acceso a una herramienta de búsqueda web. "
. "Úsala cuando necesites información actualizada o que cambia frecuentemente. "
. "Siempre cita tus fuentes cuando uses resultados de búsqueda.";
$messages = [
[
'role' => 'system',
'content' => $systemMessage,
],
];
if (!empty($context)) {
$messages[] = [
'role' => 'user',
'content' => "Contexto: " . json_encode($context),
];
}
$messages[] = [
'role' => 'user',
'content' => $message,
];
return $messages;
}
private function getAvailableTools(): array
{
return [
[
'type' => 'function',
'function' => $this->webSearchTool->getDefinition(),
],
];
}
private function processResponse(array $response, array &$messages): string
{
$assistantMessage = $response['choices'][0]['message'];
$messages[] = $assistantMessage;
// Verificar si el modelo quiere usar una herramienta
if ($assistantMessage['role'] === 'assistant'
&& isset($assistantMessage['tool_calls'])) {
foreach ($assistantMessage['tool_calls'] as $toolCall) {
if ($toolCall['function']['name'] === 'web_search') {
$args = json_decode($toolCall['function']['arguments'], true);
$searchResult = $this->webSearchTool->search($args['query']);
// Agregar resultado a los mensajes
$messages[] = [
'role' => 'tool',
'tool_call_id' => $toolCall['id'],
'content' => json_encode($searchResult),
];
}
}
// Hacer otra llamada con los resultados de la búsqueda
$finalResponse = Http::withToken($this->apiKey)
->post('https://api.openai.com/v1/chat/completions', [
'model' => $this->model,
'messages' => $messages,
'max_tokens' => 2048,
])
->json();
return $finalResponse['choices'][0]['message']['content'];
}
return $assistantMessage['content'] ?? '';
}
}
Paso 4: Usar el Agente en tu Controlador
<?php
namespace App\Http\Controllers;
use App\Services\AI\AIAgent;
use Illuminate\Http\Request;
class ChatController extends Controller
{
private AIAgent $agent;
public function __construct(AIAgent $agent)
{
$this->agent = $agent;
}
public function ask(Request $request)
{
$validated = $request->validate([
'message' => 'required|string|max:1000',
'context' => 'nullable|array',
]);
$response = $this->agent->chat(
$validated['message'],
$validated['context'] ?? []
);
return response()->json([
'response' => $response,
'timestamp' => now(),
]);
}
}
Configurar Dominios de Confianza
La seguridad es crítica cuando permites que tus agentes accedan a internet. Implementa una lista blanca robusta:
<?php
namespace App\Services\AI;
class DomainWhitelist
{
private array $trustedDomains;
private array $blockedPatterns;
public function __construct()
{
$this->trustedDomains = config('ai.trusted_domains', [
'wikipedia.org',
'laravel.com',
'stackoverflow.com',
]);
$this->blockedPatterns = [
'/localhost/',
'/127\.0\.0\.1/',
'/internal\.company\.net/',
'/admin\./i',
'/private\./i',
];
}
public function isAllowed(string $url): bool
{
$host = parse_url($url, PHP_URL_HOST);
if (!$host) {
return false;
}
// Verificar patrones bloqueados
foreach ($this->blockedPatterns as $pattern) {
if (preg_match($pattern, $host)) {
return false;
}
}
// Verificar lista blanca
foreach ($this->trustedDomains as $domain) {
if (str_ends_with($host, $domain)) {
return true;
}
}
return false;
}
public function addDomain(string $domain): void
{
if (!in_array($domain, $this->trustedDomains)) {
$this->trustedDomains[] = $domain;
}
}
public function removeDomain(string $domain): void
{
$this->trustedDomains = array_filter(
$this->trustedDomains,
fn($d) => $d !== $domain
);
}
}
Monitoreo y Rate Limiting
Implementa controles para evitar abusos:
<?php
namespace App\Services\AI;
use Illuminate\Support\Facades\RateLimiter;
class SearchRateLimiter
{
public function allowSearch(string $userId, string $query): bool
{
$key = "ai-search:{$userId}";
// Máximo 10 búsquedas por minuto por usuario
return RateLimiter::attempt(
$key,
$perMinute = 10,
fn() => true
);
}
public function logSearch(string $userId, string $query, array $results): void
{
\DB::table('ai_search_logs')->insert([
'user_id' => $userId,
'query' => $query,
'results_count' => count($results),
'created_at' => now(),
]);
}
}
Mejores Prácticas
Caché de Resultados
public function search(string $query): array
{
return \Cache::remember(
"websearch:{$query}",
minutes: 60,
callback: fn() => $this->performSearch($query)
);
}
Manejo de Errores
try {
$response = $this->agent->chat($message);
} catch (\Throwable $e) {
\Log::error('AI Agent Error', [
'message' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
return response()->json([
'error' => 'El agente no pudo procesar tu solicitud',
], 500);
}
Testing
<?php
namespace Tests\Feature;
use App\Services\AI\WebSearchTool;
use Tests\TestCase;
class WebSearchToolTest extends TestCase
{
public function test_filters_results_by_allowed_domains()
{
$tool = new WebSearchTool();
$results = $tool->search('Laravel database');
foreach ($results['results'] as $result) {
$this->assertTrue($tool->isDomainAllowed($result['url']));
}
}
public function test_limits_query_length()
{
$tool = new WebSearchTool();
$longQuery = str_repeat('a', 600);
$results = $tool->search($longQuery);
$this->assertArrayHasKey('error', $results);
}
}
Casos de Uso Reales
- Soporte al Cliente: Agentes que buscan información de soporte actualizada
- Análisis de Precios: Comparar precios en tiempo real
- Noticias y Tendencias: Proporcionar información sobre eventos actuales
- Monitoreo de Competencia: Seguimiento de cambios en sitios competidores
- Investigación Técnica: Buscar soluciones a problemas de programación
Puntos Clave
- Los agentes