El chat documentado
Hemos llegado al momento de la verdad. Uniremos todas las piezas para crear un sistema que primero piensa (busca) y luego habla (genera).
El flujo que programaremos es:
- Input: Usuario pregunta: “¿Cómo contacto a soporte?”
- Vectorización: Convertimos esa pregunta a números (Vector).
- Retrieval: Buscamos en nuestro
vector-db.jsonqué párrafos se parecen matemáticamente a esa pregunta. - Generación: Le damos a Gemini la pregunta + los párrafos encontrados y le decimos: “Responde usando solo esto”.
Paso 1: El algoritmo de búsqueda
Sección titulada «Paso 1: El algoritmo de búsqueda»Como no estamos usando una base de datos vectorial comercial (que hace esto automáticamente), implementaremos manualmente la fórmula mágica de la IA: Similitud del coseno.
Esta función mide qué tan parecidos son dos vectores.
- 1.0: Son idénticos.
- 0.0: No tienen nada que ver.
Crea el archivo src/rag-chat.js:
Directoriogemini-lab
- vector-db.json
Directoriosrc
- config.js
- ingest.js
- rag-chat.js Nuevo archivo
Copia este código completo:
import { client } from "./config.js";import fs from "fs";import path from "path";import readline from "readline";
// --- UTILIDADES MATEMÁTICAS ---
// Fórmula de similitud del coseno (cosine similarity)// Mide el ángulo entre dos vectores.function cosineSimilarity(vecA, vecB) { const dotProduct = vecA.reduce((acc, val, i) => acc + val * vecB[i], 0); const magA = Math.sqrt(vecA.reduce((acc, val) => acc + val * val, 0)); const magB = Math.sqrt(vecB.reduce((acc, val) => acc + val * val, 0)); return dotProduct / (magA * magB);}
// --- LÓGICA RAG ---
async function retrieveContext(query) { // 1. Cargar nuestra "Base de Datos" const dbPath = path.join(process.cwd(), "vector-db.json"); const database = JSON.parse(fs.readFileSync(dbPath, "utf-8"));
// 2. Vectorizar la PREGUNTA del usuario const result = await client.models.embedContent({ model: "text-embedding-004", contents: [query], }); const queryVector = result.embeddings[0].values;
// 3. Calcular similitud con CADA fragmento de la DB const scoredChunks = database.map(record => { return { content: record.content, score: cosineSimilarity(queryVector, record.vector) }; });
// 4. Ordenar de mayor a menor similitud y tomar los mejores 3 scoredChunks.sort((a, b) => b.score - a.score); const topChunks = scoredChunks.slice(0, 3);
// Filtramos ruido: si la similitud es muy baja, lo ignoramos return topChunks.filter(chunk => chunk.score > 0.4);}
async function chat() { const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
console.log("Chat RAG activado. Pregunta sobre el 'AeroBoard X3000' (Escribe 'exit' para salir)\n");
const ask = () => { rl.question('Tú: ', async (query) => { if (query.toLowerCase() === 'exit') { rl.close(); return; }
try { console.log("Buscando información relevante...");
// PASO A: RETRIEVAL (Recuperación) const relevantChunks = await retrieveContext(query);
if (relevantChunks.length === 0) { console.log("Gemini: Lo siento, no tengo información sobre eso en mis manuales.\n"); ask(); return; }
// Construimos el contexto uniendo los fragmentos encontrados const contextText = relevantChunks.map(c => c.content).join("\n---\n");
// PASO B: GENERATION (Generación) const prompt = ` Eres un asistente de soporte técnico experto. Usa ÚNICAMENTE la siguiente información de contexto para responder la pregunta del usuario. Si la respuesta no está en el contexto, di "No lo sé".
CONTEXTO: ${contextText}
PREGUNTA: ${query} `;
const response = await client.models.generateContent({ model: "gemini-3-flash-preview", contents: [{ role: "user", parts: [{ text: prompt }] }] });
console.log(`Gemini: ${response.text.trim()}\n`);
} catch (error) { console.error("Error:", error); } ask(); }); };
ask();}
chat();Paso 2: Probando el Sistema
Sección titulada «Paso 2: Probando el Sistema»Ejecuta el script:
node src/rag-chat.jsPrueba de fuego
Sección titulada «Prueba de fuego»Intenta engañar al modelo para verificar que realmente está leyendo el manual y no inventando cosas.
- Pregunta: “¿Cuánto dura la batería?”
- Respuesta esperada: Debería mencionar “400 años” y “celda de fusión fría”. (Información que solo existe en nuestro txt).
- Pregunta: “¿Cómo contacto a soporte?”
- Respuesta esperada: Debería dar la instrucción absurda de “gritarle al espejo”.
- Pregunta: “¿Cuál es la capital de Francia?”
- Respuesta esperada: “No lo sé” o “No tengo información sobre eso”.
- ¿Por qué? Porque el modelo está restringido a usar ÚNICAMENTE el contexto proporcionado. ¡Cero alucinaciones!