Lección 6 de 7 35 min

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

  1. Tabla resenas con datos de prueba.
  2. Script PHP que clasifica cada reseña con IA y guarda el resultado.
  3. Manejo de batch: procesar 100+ reseñas sin saturar la API.
  4. 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 LIMIT y cron job nocturno.
  • Idempotencia: filtra siempre por WHERE tono IS NULL para 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 resenas con datos de prueba (o adaptado a una tuya).
  • Has ejecutado analizar_resenas.php y verificado que los tono y razon_ia están en BD.
  • Has comprobado que la IA detectó al menos el sarcasmo de una reseña.
  • Has abierto dashboard.php y 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".

1 En el script analizar_resenas.php, ¿por qué usamos WHERE tono IS NULL al consultar pendientes?

2 Para procesar miles de reseñas sin saturar la API ni gastar de golpe, ¿qué haces?

3 En el ejemplo, ¿por qué un sistema de "palabras positivas/negativas" falla con la reseña "Buenísimo, claro... si te gusta esperar 1h por pizza fría"?

4 ¿Para qué sectores comerciales se puede vender este patrón (análisis de reseñas con IA)?

Cuando termines, guarda tu avance. Así podrás continuar desde tu panel y desbloquear el certificado al completar el curso.