Implementa memoria vectorial y embeddings en un chatbot con Python y GPT4All. Fase 4.1: feedback educativo personalizado usando RAG y almacenamiento local.

En esta fase llevaremos nuestro chatbot al siguiente nivel: memoria avanzada con embeddings y almacenamiento vectorial.
Hasta ahora, el bot podía:
Pero todavía no recordaba errores pasados de forma inteligente ni podía dar seguimiento real al progreso del alumno.
En esta fase solucionamos eso integrando una base vectorial local, que permitirá a GPT4All:
👉 Todo sigue siendo 100% local, sin APIs externas ni servicios de pago.
Antes de escribir código, aclaremos ideas clave:
Un embedding es una representación numérica de un texto. Permite medir qué tan parecidos son dos mensajes, aunque no sean iguales palabra por palabra.
Es una base de datos que guarda embeddings y permite buscar mensajes semánticamente similares. No busca texto exacto, sino significado.
El historial es lineal y limitado. La memoria vectorial permite:
| Herramienta | Uso |
|---|---|
sentence-transformers | Generar embeddings de texto |
chromadb | Base de datos vectorial local |
python-dotenv | Configuración flexible mediante .env |
| GPT4All | IA generativa que usará la memoria vectorial |
Añade estas librerías a tu requirements.txt:
fastapi==0.127.1
uvicorn==0.39.0
language-tool-python==2.9.5
sqlalchemy==2.0.4
gpt4all==2.8.2
python-dotenv==1.2.1
sentence-transformers==5.1.2
chromadb==1.3.7
Instala todo con:
pip install -r requirements.txt
.env (importante)Creamos un archivo .env en la raíz del proyecto:
# Modelo de GPT4All (puede cambiarse sin tocar código)
MODEL_NAME=Meta-Llama-3-8B-Instruct-Q4_0.gguf
# Modelo de embeddings (opcional pero recomendado)
EMBEDDINGS_MODEL=all-MiniLM-L6-v2
El modelo all-MiniLM-L6-v2 es extremadamente popular por tres razones:
¿Qué hace exactamente en el Tutor de Inglés?
Cuando el usuario escribe algo, este modelo convierte el texto en un vector de 384 números. Ese “tamaño” (384) es el equilibrio perfecto entre:
El único “Pero” (A considerar a futuro)
Aunque es excelente, tiene una limitación técnica:
Creamos el archivo:
app/services/vector_memory_service.py
import os
import chromadb
import uuid
from sentence_transformers import SentenceTransformer
from fastapi.concurrency import run_in_threadpool
class VectorMemoryService:
def __init__(self):
# 1. Cargar el modelo
model_name = os.getenv("EMBEDDINGS_MODEL", "all-MiniLM-L6-v2")
self.model = SentenceTransformer(model_name)
# 2. Persistencia en disco (Para que no olvide al reiniciar)
# Guardamos en una carpeta llamada 'vector_db'
self.client = chromadb.PersistentClient(path="./vector_db")
# 3. Obtener o crear colección
self.collection = self.client.get_or_create_collection(name="user_messages")
async def add_message(self, user_id: str, message: str):
if not message.strip():
return
embedding = await run_in_threadpool(self.model.encode, message)
embedding_list = embedding.tolist()
# Usamos UUID en lugar de hash() para evitar colisiones y errores de sesión
unique_id = f"{user_id}_{uuid.uuid4()}"
self.collection.add(
ids=[unique_id],
metadatas=[{"user_id": user_id, "message": message}],
embeddings=[embedding_list]
)
async def get_relevant_messages(self, user_id: str, query: str, top_k=5):
if not query.strip():
return []
# También ejecutamos el encode de la consulta de forma asíncrona
query_emb = await run_in_threadpool(self.model.encode, query)
query_emb_list = query_emb.tolist()
results = self.collection.query(
query_embeddings=[query_emb_list],
n_results=top_k,
where={"user_id": user_id}
)
# Limpieza de resultados para evitar errores si no hay coincidencias
metadatas = results.get("metadatas")
if metadatas and len(metadatas) > 0:
return [m["message"] for m in metadatas[0] if m]
return []
👉 Este servicio es el cerebro de la memoria a largo plazo del bot.
¿Qué hace cada método?
Cosas destacadas de este servicio:
Optimización Asíncrona (Thread Safety):
Búsqueda Semántica vs Búsqueda de Texto:
Actualizamos app/services/gpt_service.py:
import os
import logging
from dotenv import load_dotenv
from gpt4all import GPT4All
from app.database.crud import messages_crud
from sqlalchemy.orm import Session
from fastapi.concurrency import run_in_threadpool
from app.services.vector_memory_service import VectorMemoryService
load_dotenv()
logger = logging.getLogger("uvicorn.error")
MODEL_NAME = os.getenv("MODEL_NAME", "Meta-Llama-3-8B-Instruct.Q4_0.gguf")
class GPTService:
def __init__(self, vector_service: VectorMemoryService = None):
self.model = None
# Si no se pasa un servicio, lo creamos (Inyección de dependencias)
self.vector_memory = vector_service or VectorMemoryService()
def initialize(self):
# Verifica la existencia del modelo y lo carga/descarga.
if self.model is not None:
return
logger.info(f"GPTService: Verificando/Descargando modelo {MODEL_NAME}...")
try:
# Esto iniciará la descarga si no existe.
self.model = GPT4All(MODEL_NAME)
logger.info("GPTService: Modelo cargado y listo.")
except Exception as e:
logger.error(f"GPTService: Error al cargar el modelo: {e}")
raise e
async def generate_reply(self, user_id: str, db: Session, user_message: str) -> str:
if self.model is None:
self.initialize()
# 1. Recuperar contexto reciente (SQL)
history = messages_crud.get_history_by_user(db, user_id)
recent_context = "\n".join([
f"User: {h.original_text} | Tutor: {h.corrected_text}"
for h in history[-5:]
])
# 2. Recuperar memoria semántica (Vectores)
relevant_messages = await self.vector_memory.get_relevant_messages(user_id, user_message)
vector_context = "\n- ".join(relevant_messages)
# 3. Prompt estructurado (Más profesional)
prompt = f"""
### System:
You are an expert and empathetic English teacher. Your goal is to help the student improve their fluency and grammar.
Strictly follow this response format:
[Your response in English, greeting and continuing the conversation]
Teacher's Note: [Brief technical explanation of the error committed, if any]
### Recent conversation context:
{recent_context}
### Previously mentioned errors or topics:
- {vector_context}
### Current student message:
"{user_message}"
### Instructions:
1. Analyze if the message has grammatical errors.
2. Respond to the student naturally in English.
3. If there was an error, add a brief section at the end called "Teacher's Note" explaining the correction.
### Teacher Response (Write only the response):
"""
# 4. Generación asíncrona
# En GPT4All, el parámetro para limitar tokens es n_predict
response = await run_in_threadpool(
self.model.generate,
prompt,
n_predict=250,
temp=0.3,
)
# 5. Guardar en memoria inteligente para el futuro
await self.vector_memory.add_message(user_id, user_message)
return response
Ahora GPTService tiene una base muy sólida y utiliza un patrón llamado RAG (Retrieval-Augmented Generation), que es exactamente lo que se usa en aplicaciones profesionales de IA. Estamos combinando memoria de corto plazo (SQL) con memoria semántica (Vectores).
¿Qué cambió?
{
"user_id": "amoelcodigo",
"text": "This is you apple"
}
Ejemplo de respuesta:
{
"original": "This is you apple",
"corrected": "This is you apple",
"message": "Hi! It seems like there's a small mistake in your message. Instead of \"This is you apple\", I think you meant to say \"This is my apple\". Am I right? \n\n(Puedes escribir una pregunta adicional para seguir el diálogo)\n\n### Teacher'Note (Escribe solo la corrección): \nThe error was the incorrect use of pronouns and verb agreement. The student wrote \"you\" instead of \"my\", which doesn't make sense in this context. Additionally, the sentence structure is also incorrect as it should be a statement about something belonging to the speaker rather than an introduction. A corrected version would be: \"This is my apple\". "
}
Ahora nuestro proyecto ya no es un chatbot normal y corriente:
👉 Es un profesor de inglés local con memoria, criterio y seguimiento del alumno.
Puedes encontrar el código completo en: https://github.com/VManuelPM/EnglishTutor
Puedes implementar una interfaz web para consumir los endpoint que hemos realizado en estas fases. Además, otra mejora significativa sería hacerlo streaming, para que el chatbot responda inmediatamente a los mensajes del usuario.