Construye un Profesor de Inglés Personal con Python - Fase 4.1

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

Construye un Profesor de Inglés Personal con Python - Fase 4.1

En esta fase llevaremos nuestro chatbot al siguiente nivel: memoria avanzada con embeddings y almacenamiento vectorial.

Hasta ahora, el bot podía:

  • Corregir frases con LanguageTool
  • Guardar historial de usuario en base de datos
  • Generar respuestas educativas con GPT4All usando los últimos mensajes

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:

  • Recordar errores recurrentes
  • Detectar patrones
  • Actuar como un profesor, no solo como un corrector

👉 Todo sigue siendo 100% local, sin APIs externas ni servicios de pago.


📚 Tabla de Contenidos



1️⃣ Objetivo de esta fase

  • Implementar memoria vectorial para almacenar mensajes del usuario como embeddings
  • Recuperar mensajes relevantes (RAG) según el contexto actual
  • Permitir feedback educativo, progresivo y personalizado
  • Mantener una arquitectura clara, modular y escalable en Python


2️⃣ Nuevos conceptos que aprenderás aquí

Antes de escribir código, aclaremos ideas clave:

🔹 ¿Qué es un embedding?

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.

🔹 ¿Qué es memoria vectorial?

Es una base de datos que guarda embeddings y permite buscar mensajes semánticamente similares. No busca texto exacto, sino significado.

🔹 ¿Por qué no basta con el historial tradicional?

El historial es lineal y limitado. La memoria vectorial permite:

  • Recordar errores antiguos
  • Detectar patrones repetidos
  • Dar feedback contextual incluso semanas después

3️⃣ Herramientas nuevas de esta fase

HerramientaUso
sentence-transformersGenerar embeddings de texto
chromadbBase de datos vectorial local
python-dotenvConfiguración flexible mediante .env
GPT4AllIA generativa que usará la memoria vectorial

  • El modelo SentenceTransformer convierte una frase (“Me gusta el café”) en una lista de números (un vector). Esa lista representa el “concepto” de la frase en un espacio matemático.
  • Base de Datos Vectorial (ChromaDB): Es como un archivador gigante donde guardas esos números.
  • Búsqueda Semántica: Cuando el usuario pregunta algo, el servicio busca en el archivador los números que más se “parezcan” al significado de la pregunta

4️⃣ Actualizar dependencias

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

5️⃣ Configuración con .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:

  • Velocidad: Es increíblemente rápido. Puede procesar cientos de frases por segundo incluso sin una tarjeta gráfica (GPU), lo cual es ideal para un servidor web con FastAPI.
  • Ligereza: Solo pesa unos 80MB - 100MB. Otros modelos de IA pueden pesar gigabytes. No va a “matar” la memoria RAM de tu computadora.
  • Calidad/Precio: Para ser un modelo pequeño, entiende muy bien el significado de las oraciones en inglés y otros 50 idiomas.

¿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:

  • Ser lo suficientemente detallado para entender la diferencia entre “I’m feeling blue” (tristeza) y “The sky is blue” (color).
  • Ser lo suficientemente pequeño para que las búsquedas en ChromaDB sean instantáneas.

El único “Pero” (A considerar a futuro)

Aunque es excelente, tiene una limitación técnica:

  • Ventana de contexto: Está diseñado para frases o párrafos cortos (máximo unas 250-300 palabras). Si intentas guardar un ensayo completo de 10 páginas como un solo bloque, el modelo perderá precisión.
  • Solución: Para nuestro proyecto, como los mensajes suelen ser cortos (diálogos), no vamos a tener ningún problema.

6️⃣ Servicio de memoria vectorial

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?

  • init: Prepara el motor de traducción (el modelo de IA) y crea el archivador (la colección en ChromaDB).
  • add_message: Toma un mensaje, lo traduce a números y lo guarda con el ID del usuario. Así el chatbot “aprende” o “memoriza” lo que el usuario dice.
  • get_relevant_messages: Si el usuario pregunta “¿Qué hablamos de mi viaje?”, este método no busca la palabra “viaje” exactamente, sino que busca mensajes que signifiquen algo parecido a viajar, y te devuelve los 5 más cercanos.

Cosas destacadas de este servicio:

  • Persistencia Real: Al usar PersistentClient(path="./vector_db"), aseguras que el “cerebro” de tu chatbot no se borre al apagar el servidor.
  • Identificación Única: El uso de uuid.uuid4() es perfecto. Evita que si un usuario dice dos veces lo mismo, la base de datos se confunda o lance un error de “ID duplicado”.
  • Manejo de Errores Silencioso: Los chequeos de if not message.strip() y la validación de metadatas previenen que el programa explote si no hay resultados.

Optimización Asíncrona (Thread Safety):

  • No bloqueo del Event Loop: Al utilizar run_in_threadpool, delegamos la carga pesada de la CPU (el cálculo de embeddings por el modelo SentenceTransformer) a hilos secundarios. Esto permite que FastAPI siga atendiendo otras peticiones simultáneamente, garantizando una aplicación fluida y escalable.
  • Arquitectura RAG (Retrieval-Augmented Generation): Este servicio es el pilar que permite al GPTService generar respuestas no solo basadas en su entrenamiento general, sino enriquecidas con el contexto específico y el historial histórico del usuario, eliminando el “olvido” característico de los modelos de IA tradicionales.

Búsqueda Semántica vs Búsqueda de Texto:

  • A diferencia de una base de datos SQL tradicional que busca palabras exactas, este servicio utiliza distancia coseno (o similitud vectorial). Si el usuario habla sobre “soccer”, el bot podrá recuperar mensajes anteriores sobre “football” o “sports”, ya que sus representaciones numéricas (embeddings) están cerca en el espacio vectorial.

7️⃣ Integrar memoria vectorial con GPT4All

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ó?

  • Contexto mejorado: El historial ahora muestra pares de User/Tutor, lo que le da a la IA una mejor idea de cómo va la charla.
  • Prompt Robusto: Se añaden roles (System, Instructions) y delimitadores. Esto reduce drásticamente las alucinaciones de la IA.
  • Parámetros de generación: Se añaden max_tokens y temp (temperatura). Esto evita que la IA se extienda infinitamente o sea demasiado aburrida/repetitiva.
  • Auto-inicialización: Si por algún motivo se olvida llamar a initialize() en el main.py, el método generate_reply lo hace por nosotros la primera vez.

8️⃣ Probar el endpoint

{
  "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\". "
}

9️⃣ Ventajas de la Fase 4.1

  • Feedback personalizado y progresivo
  • Detección de errores recurrentes
  • Memoria a largo plazo real
  • Todo local y escalable

🚀 Resultado final

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.

📁 Repositorio

Puedes encontrar el código completo en: https://github.com/VManuelPM/EnglishTutor

🚀 ¿Qué sigue?

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.