Caso real: analizar reseñas con IA y persistir resultados en MySQL
Caso real, end-to-end. En esta lección montas un sistema que: (1) coge cientos de reseñas de clientes en MySQL, (2) les pasa cada una por Claude/OpenAI para clasificar el tono, (3) guarda el resultado en BD, y (4) genera un mini-dashboard. Es exactamente el tipo de proyecto que un cliente paga 500-2000 € por hacer.
Lo que vas a construir
- Tabla
resenascon datos de prueba. - Script PHP que clasifica cada reseña con IA y guarda el resultado.
- Manejo de batch: procesar 100+ reseñas sin saturar la API.
- Mini-dashboard HTML que muestra los porcentajes y permite filtrar.
1. Preparar la tabla y datos de prueba
CREATE TABLE resenas (
id INT PRIMARY KEY AUTO_INCREMENT,
cliente VARCHAR(100),
texto TEXT NOT NULL,
fecha DATE,
tono ENUM('positivo','negativo','neutro') NULL,
razon_ia VARCHAR(255) NULL,
analizada_en DATETIME NULL
);
-- Datos de prueba reales (mezcla de tonos)
INSERT INTO resenas (cliente, texto, fecha) VALUES
('Ana M.', 'La comida estaba buenísima y el servicio rápido. Volveré seguro.', '2026-05-10'),
('Pedro G.', 'El servicio fue lento y la pizza llegó fría. Decepcionante.', '2026-05-11'),
('Lucía R.', 'Estuvo bien, sin más. Ni espectacular ni malo.', '2026-05-12'),
('Carlos D.', 'Buenísimo, claro... si te gusta esperar 1 hora por una pizza fría.', '2026-05-13'),
('María P.', 'Sitio acogedor, raciones generosas y precios justos. Repetiremos en familia.', '2026-05-14'),
('Javier L.', 'No me hizo gracia que me cobraran el pan sin avisar. La comida bien.', '2026-05-15');
2. Script de análisis con IA
Crea analizar_resenas.php:
<?php
require __DIR__ . '/vendor/autoload.php';
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->load();
use Symfony\Component\HttpClient\HttpClient;
$pdo = new PDO(
'mysql:host=localhost;dbname=mi_db;charset=utf8mb4',
'root',
'password',
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
$client = HttpClient::create();
function clasificarTono(string $texto): array
{
global $client;
$prompt = "Analiza el tono de esta reseña de un cliente. "
. "Responde SOLO con JSON válido con dos claves:\n"
. "- tono: \"positivo\", \"negativo\" o \"neutro\"\n"
. "- razon: breve explicación (máx 20 palabras)\n\n"
. "Detecta el sarcasmo y los matices, no te quedes en palabras sueltas.\n\n"
. "Reseña: \"$texto\"";
$response = $client->request('POST', 'https://api.anthropic.com/v1/messages', [
'headers' => [
'x-api-key' => $_ENV['ANTHROPIC_API_KEY'],
'anthropic-version' => '2023-06-01',
],
'json' => [
'model' => 'claude-haiku-4-5-20251001',
'max_tokens' => 150,
'messages' => [['role' => 'user', 'content' => $prompt]],
],
]);
$textoRespuesta = $response->toArray()['content'][0]['text'];
// Limpia ```json...``` si Claude lo envuelve
$textoRespuesta = preg_replace('/^```json\s*|\s*```$/m', '', trim($textoRespuesta));
return json_decode($textoRespuesta, true) ?? ['tono' => 'neutro', 'razon' => 'sin parsear'];
}
// Procesa solo reseñas sin analizar (idempotente: puedes re-ejecutar)
$pendientes = $pdo->query("SELECT id, texto FROM resenas WHERE tono IS NULL LIMIT 50")->fetchAll(PDO::FETCH_ASSOC);
echo "Procesando " . count($pendientes) . " reseñas...\n\n";
$update = $pdo->prepare("UPDATE resenas SET tono = ?, razon_ia = ?, analizada_en = NOW() WHERE id = ?");
foreach ($pendientes as $r) {
$resultado = clasificarTono($r['texto']);
$icono = ['positivo' => '✅', 'negativo' => '❌', 'neutro' => '➖'][$resultado['tono']] ?? '❓';
$update->execute([$resultado['tono'], $resultado['razon'], $r['id']]);
echo "$icono [{$resultado['tono']}] #{$r['id']}: " . substr($r['texto'], 0, 60) . "...\n";
echo " → {$resultado['razon']}\n\n";
// Pausa de 200ms entre llamadas para no saturar la API
usleep(200_000);
}
echo "Listo. Total procesadas: " . count($pendientes) . "\n";
Ejecuta:
php analizar_resenas.php
Verás algo como:
✅ [positivo] #1: La comida estaba buenísima y el servicio rápido...
→ Cliente satisfecho, valora comida y servicio, intención de volver
❌ [negativo] #2: El servicio fue lento y la pizza llegó fría...
→ Queja por servicio lento y comida fría, decepción
➖ [neutro] #3: Estuvo bien, sin más. Ni espectacular ni malo...
→ Reseña tibia, sin entusiasmo ni queja clara
❌ [negativo] #4: Buenísimo, claro... si te gusta esperar 1 hora...
→ Sarcasmo evidente, queja por tiempo de espera y comida fría
...
Nota cómo la IA detectó el sarcasmo de la reseña #4 — algo que ninguna regla manual de "palabras positivas/negativas" haría bien.
3. Manejar batches grandes
Si tienes miles de reseñas:
- Procesa en batches con
LIMITy cron job nocturno. - Idempotencia: filtra siempre por
WHERE tono IS NULLpara que re-ejecutar sea seguro. - Rate limit: pausa
usleep()entre llamadas (200ms suele bastar). - Logs: si una llamada falla, guarda el error en una tabla aparte para retry manual.
4. Mini-dashboard HTML
Crea dashboard.php:
<?php
$pdo = new PDO('mysql:host=localhost;dbname=mi_db;charset=utf8mb4', 'root', 'password');
$stats = $pdo->query("
SELECT tono, COUNT(*) total
FROM resenas
WHERE tono IS NOT NULL
GROUP BY tono
")->fetchAll(PDO::FETCH_KEY_PAIR);
$total = array_sum($stats);
$negativas = $pdo->query("
SELECT cliente, texto, razon_ia, fecha
FROM resenas
WHERE tono = 'negativo'
ORDER BY fecha DESC
LIMIT 10
")->fetchAll(PDO::FETCH_ASSOC);
?>
<!DOCTYPE html>
<html lang="es">
<head><meta charset="UTF-8"><title>Reseñas con IA</title>
<style>
body { font-family: system-ui; max-width: 800px; margin: 2rem auto; padding: 1rem; }
.stats { display: flex; gap: 1rem; margin-bottom: 2rem; }
.stat { flex: 1; padding: 1.5rem; border-radius: 8px; text-align: center; }
.stat--pos { background: #dcfce7; color: #166534; }
.stat--neg { background: #fee2e2; color: #991b1b; }
.stat--neu { background: #f1f5f9; color: #475569; }
.review { border-left: 4px solid #ef4444; padding: 1rem; margin-bottom: 1rem; background: #fef2f2; }
.review small { color: #64748b; }
</style></head>
<body>
<h1>📊 Reseñas analizadas con IA</h1>
<p>Total analizadas: <?= $total ?></p>
<div class="stats">
<div class="stat stat--pos">
<strong>✅ Positivas</strong><br>
<?= $stats['positivo'] ?? 0 ?> (<?= round((($stats['positivo'] ?? 0) / $total) * 100) ?>%)
</div>
<div class="stat stat--neg">
<strong>❌ Negativas</strong><br>
<?= $stats['negativo'] ?? 0 ?> (<?= round((($stats['negativo'] ?? 0) / $total) * 100) ?>%)
</div>
<div class="stat stat--neu">
<strong>➖ Neutras</strong><br>
<?= $stats['neutro'] ?? 0 ?> (<?= round((($stats['neutro'] ?? 0) / $total) * 100) ?>%)
</div>
</div>
<h2>🚨 Últimas reseñas negativas (atención prioritaria)</h2>
<?php foreach ($negativas as $r): ?>
<div class="review">
<strong><?= htmlspecialchars($r['cliente']) ?></strong>
<small><?= $r['fecha'] ?></small>
<p><?= htmlspecialchars($r['texto']) ?></p>
<small><em>IA: <?= htmlspecialchars($r['razon_ia']) ?></em></small>
</div>
<?php endforeach; ?>
</body></html>
Abre http://localhost/dashboard.php y verás un dashboard funcional con estadísticas + el feed de reseñas negativas que requieren atención.
Cómo vender este sistema a un cliente real
Este patrón se vende exactamente igual de bien a:
- Restaurantes y hoteles con reseñas en Google/TripAdvisor — exportas, analizas, alertas al dueño.
- Tiendas online que reciben opiniones de productos.
- Soporte al cliente que recibe miles de tickets — clasificar por prioridad y tono.
- Recursos humanos con encuestas de clima interno.
Coste de la IA: céntimos por mes incluso analizando miles de mensajes. Tu margen: en el rango de cientos o miles de euros por proyecto. El ROI para el cliente es brutal y el coste para ti minúsculo.
Checklist antes de marcar completada
- Has creado la tabla
resenascon datos de prueba (o adaptado a una tuya). - Has ejecutado
analizar_resenas.phpy verificado que lostonoyrazon_iaestán en BD. - Has comprobado que la IA detectó al menos el sarcasmo de una reseña.
- Has abierto
dashboard.phpy ves estadísticas con porcentajes. - Entiendes cómo escalar el patrón a miles de filas (LIMIT + cron + idempotencia).
Última lección: te dejo el mapa de qué aprender después (function calling, agentes, librerías PHP serias del ecosistema IA) y un listado de 10 proyectos para practicar tú solo. Después de eso, descargas tu certificado.
📝 Comprueba lo aprendido
Responde estas 4 preguntas y acierta al menos el 60% para desbloquear el botón "Marcar como completada".