Agentes IA con Memoria en Laravel: Guía Completa
Agentes IA con Memoria en Laravel: Implementa Conversaciones Persistentes
Los agentes de inteligencia artificial están revolucionando cómo interactuamos con nuestras aplicaciones. Sin embargo, un problema crítico emerge rápidamente: los agentes olvidan. Un usuario pregunta sobre su pedido número 1042, realiza un seguimiento con “¿puedo devolverlo?”, y el agente no tiene contexto. Esto destruye la experiencia del usuario.
En este artículo aprenderás a construir agentes IA con memoria persistente en Laravel, manteniendo el contexto conversacional entre interacciones. Exploraremos patrones de diseño, almacenamiento de estado y mejores prácticas para producción.
El Problema: Agentes Amnésicos
Los modelos de IA como GPT no tienen memoria inherente. Cada solicitud es independiente. Para un usuario final, esto se traduce en:
- Preguntas que requieren reexplicación constantemente
- Pérdida de contexto entre turnos de conversación
- Incapacidad para acceder a datos históricos o preferencias
- Experiencia frustrada y poco natural
La solución es implementar un sistema de memoria conversacional que almacene contexto entre interacciones.
Arquitectura de Memoria Conversacional
Necesitamos tres componentes principales:
- Almacenamiento de Conversaciones: Persistencia de mensajes e historiales
- Recuperación de Contexto: Obtener información relevante rápidamente
- Gestión de Estado: Mantener el estado del agente entre solicitudes
Estructura Base del Modelo
Comencemos con una estructura de base de datos para almacenar conversaciones:
// database/migrations/2025_04_18_create_conversations_table.php
Schema::create('conversations', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('agent_type')->default('default');
$table->json('metadata')->nullable();
$table->timestamp('last_activity_at');
$table->timestamps();
$table->index(['user_id', 'created_at']);
});
Schema::create('conversation_messages', function (Blueprint $table) {
$table->id();
$table->foreignId('conversation_id')->constrained()->cascadeOnDelete();
$table->enum('role', ['user', 'assistant', 'system']);
$table->longText('content');
$table->json('context')->nullable();
$table->integer('tokens')->nullable();
$table->timestamps();
$table->index(['conversation_id', 'created_at']);
});
Schema::create('agent_memories', function (Blueprint $table) {
$table->id();
$table->foreignId('conversation_id')->constrained()->cascadeOnDelete();
$table->string('key');
$table->json('value');
$table->timestamp('expires_at')->nullable();
$table->timestamps();
$table->unique(['conversation_id', 'key']);
});
Modelos Eloquent para Conversaciones
// app/Models/Conversation.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Conversation extends Model
{
protected $fillable = ['user_id', 'agent_type', 'metadata'];
protected $casts = ['metadata' => 'array'];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function messages(): HasMany
{
return $this->hasMany(ConversationMessage::class)
->orderBy('created_at', 'asc');
}
public function memories(): HasMany
{
return $this->hasMany(AgentMemory::class);
}
public function addMessage(
string $role,
string $content,
?array $context = null
): ConversationMessage {
return $this->messages()->create([
'role' => $role,
'content' => $content,
'context' => $context,
]);
}
public function getRecentMessages(int $limit = 10): array
{
return $this->messages()
->latest('created_at')
->limit($limit)
->get()
->map(fn ($msg) => [
'role' => $msg->role,
'content' => $msg->content,
])
->reverse()
->values()
->all();
}
public function rememberData(string $key, mixed $value, ?int $expiresInMinutes = null): void
{
$this->memories()->updateOrCreate(
['key' => $key],
[
'value' => $value,
'expires_at' => $expiresInMinutes
? now()->addMinutes($expiresInMinutes)
: null,
]
);
}
public function recall(string $key): mixed
{
$memory = $this->memories()
->where('key', $key)
->where(function ($query) {
$query->whereNull('expires_at')
->orWhere('expires_at', '>', now());
})
->first();
return $memory?->value;
}
public function updateActivity(): void
{
$this->update(['last_activity_at' => now()]);
}
}
// app/Models/ConversationMessage.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ConversationMessage extends Model
{
protected $fillable = ['conversation_id', 'role', 'content', 'context', 'tokens'];
protected $casts = ['context' => 'array'];
public function conversation(): BelongsTo
{
return $this->belongsTo(Conversation::class);
}
public function toApiFormat(): array
{
return [
'role' => $this->role,
'content' => $this->content,
];
}
}
Servicio de Agente IA con Memoria
Ahora crearemos el servicio central que gestiona la inteligencia artificial y la memoria:
// app/Services/AIAgentService.php
namespace App\Services;
use App\Models\Conversation;
use OpenAI\Client;
use Illuminate\Support\Collection;
class AIAgentService
{
public function __construct(
private Client $openaiClient,
private int $maxContextMessages = 15,
private int $maxTokens = 2000,
) {}
public function processMessage(
Conversation $conversation,
string $userMessage,
?array $tools = null
): string {
// Guardar mensaje del usuario
$conversation->addMessage('user', $userMessage);
$conversation->updateActivity();
// Construir contexto conversacional
$messages = $this->buildContextMessages($conversation);
// Construir instrucciones del sistema
$systemPrompt = $this->buildSystemPrompt($conversation);
// Llamar a OpenAI con contexto
$response = $this->openaiClient->chat()->create([
'model' => config('services.openai.model', 'gpt-4-turbo'),
'messages' => array_merge(
[['role' => 'system', 'content' => $systemPrompt]],
$messages
),
'max_tokens' => $this->maxTokens,
'temperature' => 0.7,
'tools' => $tools,
]);
$assistantMessage = $response->choices[0]->message->content;
// Guardar respuesta del asistente
$conversation->addMessage('assistant', $assistantMessage);
// Analizar y almacenar información importante
$this->extractAndStoreMemories($conversation, $userMessage, $assistantMessage);
return $assistantMessage;
}
private function buildContextMessages(Conversation $conversation): array
{
$recentMessages = $conversation->messages()
->latest('created_at')
->limit($this->maxContextMessages)
->get()
->reverse()
->values();
return $recentMessages->map(fn ($msg) => [
'role' => $msg->role,
'content' => $msg->content,
])->all();
}
private function buildSystemPrompt(Conversation $conversation): string
{
$userInfo = $conversation->user;
$savedMemories = $this->formatMemoriesForPrompt($conversation);
$basePrompt = <<<PROMPT
Eres un asistente útil, amable y profesional.
Tu objetivo es ayudar al usuario de manera precisa y eficiente.
Información del usuario:
- Nombre: {$userInfo->name}
- Email: {$userInfo->email}
Contexto previamente recordado:
{$savedMemories}
Sigue estas directrices:
1. Sé conciso pero completo
2. Si no sabes algo, admítelo
3. Usa el contexto previo para proporcionar respuestas más personalizadas
4. Mantén un tono profesional pero amable
PROMPT;
return $basePrompt;
}
private function formatMemoriesForPrompt(Conversation $conversation): string
{
$memories = $conversation->memories()
->whereNull('expires_at')
->orWhere('expires_at', '>', now())
->get();
if ($memories->isEmpty()) {
return "No hay información previa almacenada.";
}
return $memories->map(function ($memory) {
$value = is_array($memory->value)
? json_encode($memory->value)
: $memory->value;
return "- {$memory->key}: {$value}";
})->join("\n");
}
private function extractAndStoreMemories(
Conversation $conversation,
string $userMessage,
string $assistantMessage
): void {
// Extraer información importante usando patrones
$this->extractOrderInfo($conversation, $userMessage);
$this->extractPreferences($conversation, $userMessage);
$this->extractDataPoints($conversation, $assistantMessage);
}
private function extractOrderInfo(Conversation $conversation, string $message): void
{
if (preg_match('/order\s*#?(\d{4,})/i', $message, $matches)) {
$conversation->rememberData('current_order_id', $matches[1], 1440);
}
}
private function extractPreferences(Conversation $conversation, string $message): void
{
if (preg_match('/prefer|like|want|need/i', $message)) {
$preferences = $conversation->recall('preferences') ?? [];
$preferences[] = [
'mentioned_at' => now(),
'context' => substr($message, 0, 200),
];
$conversation->rememberData('preferences', $preferences);
}
}
private function extractDataPoints(Conversation $conversation, string $message): void
{
// Extraer nombres, locaciones, fechas, etc.
$conversation->rememberData(
'last_assistant_response_length',
strlen($message)
);
}
public function getConversationHistory(Conversation $conversation): Collection
{
return $conversation->messages()
->orderBy('created_at', 'asc')
->get();
}
public function createNewConversation(
int $userId,
string $agentType = 'default'
): Conversation {
return Conversation::create([
'user_id' => $userId,
'agent_type' => $agentType,
'last_activity_at' => now(),
]);
}
}
Controlador para Interacciones del Agente
// app/Http/Controllers/AIAgentController.php
namespace App\Http\Controllers;
use App\Models\Conversation;
use App\Services\AIAgentService;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
class AIAgentController extends Controller
{
public function __construct(private AIAgentService $agentService) {}
public function store(Request $request): JsonResponse
{
$validated = $request->validate([
'message' => 'required|string|max:2000',
'conversation_id' => 'nullable|integer|exists:conversations,id',
]);
$conversation = $validated['conversation_id']
? Conversation::findOrFail($validated['conversation_id'])
: $this->agentService->createNewConversation($request->user()->id);
try {
$response = $this->agentService->processMessage(
$conversation,
$validated['message']
);
return response()->json([
'success' => true,
'conversation_id' => $conversation->id,
'message' => $response,
'memories' => $conversation->memories()
->get()
->mapWithKeys(fn ($m) => [$m->key => $m->value])
->all(),
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'error' => $e->getMessage(),
], 500);
}
}
public function show(Conversation $conversation): JsonResponse
{
$this->authorize('view', $conversation);
return response()->json([
'conversation_id' => $conversation->id,
'created_at' => $conversation->created_at,
'messages' => $conversation->getRecentMessages(50),
'memories' => $conversation->memories()
->get()
->mapWithKeys(fn ($m) => [$m->key => $m->value])
->all(),
]);
}
public function destroy(Conversation $conversation): JsonResponse
{
$this->authorize('delete', $conversation);
$conversation->delete();
return response()->json(['success' => true]);
}
}
Limpiar Memorias Expiradas
Es importante eliminar memorias expiradas periódicamente:
// app/Console/Commands/PruneExpiredMemories.php
namespace App\Console\Commands;
use App\Models\AgentMemory;
use Illuminate\Console\Command;
class PruneExpiredMemories extends Command
{
protected $signature = 'agent:prune-memories';
protected $description = 'Elimina memorias de agente expiradas';
public function handle(): int
{
$deleted = AgentMemory::where('expires_at', '<', now())->delete();
$this->info("Se eliminaron {$deleted} memorias expiradas.");
return self::SUCCESS;
}
}
Agregua a app/Console/Kernel.php:
protected function schedule(Schedule $schedule)
{
$schedule->command('agent:prune-memories')->hourly();
}
Mejores Prácticas y Consideraciones
Rate Limiting
Protege tu API contra abuso:
// routes/api.php
Route::middleware('auth:sanctum')->group(function () {
Route::post('/agent/message', [AIAgentController::class, 'store'])
->middleware('throttle