<!-- 🧠 Prompt: "armá la portada del TP5: chatbot RAG y evaluación de embeddings, reusando el dominio biblioteca del TP4, con alumno, objetivo, pipeline RAG y nota de que es continuación del TP4" -->
TP5 — Chatbot RAG y evaluación de embeddings
Procesamiento del Habla (PH) · ISSD · 4° IAR — Dist · Noche · A
Alumno: Nicolás Bargioni
Continuación del TP4. Reutilizamos el mismo dominio y dataset: el asistente de la Biblioteca Popular Domingo F. Sarmiento. En el TP4 el chatbot devolvía la respuesta más parecida (recuperación pura). Acá damos el salto a RAG (Retrieval-Augmented Generation): recuperamos los contextos más relevantes con embeddings + base vectorial y luego un LLM genera la respuesta a partir de ese contexto.
> Objetivo (consigna).
> - a) Crear un dataset de evaluación (preguntas–respuestas nuevas, misma lógica que el del TP4).
> - b) Elegir un LLM de HuggingFace y ≥ 2 modelos de embeddings; justificar.
> - c) Implementar una clase ChatBot (recuperación con base vectorial FAISS + generación con el LLM).
> - d) Probar el chatbot con el dataset de a) comparando los dos embeddings y elegir el mejor.
> - e) BONUS: *context precision* / *context recall*.
> - f) BONUS: *answer relevancy* / *faithfulness*.
> - g) Referencias.
Pipeline RAG:
pregunta → embedding → FAISS (top-k contextos) → prompt con contexto → LLM → respuesta
> ⚠️ Nota de ejecución. Este notebook descarga modelos de HuggingFace (embeddings + LLM ≈ 0.5–1 GB) y conviene correrlo en Google Colab (idealmente con GPU: *Entorno de ejecución → Cambiar tipo de entorno → T4 GPU*). El código está probado/validado en su lógica y es Colab-ready. En CPU también corre, sólo más lento.
<!-- 🧠 Prompt: "sección de instalación del TP5: sentence-transformers, faiss-cpu, transformers, torch, accelerate; aclarar reinicio de runtime en Colab" -->
0. Preparación del entorno
Instalamos las librerías del stack RAG: sentence-transformers (embeddings de oraciones), faiss-cpu (base de datos vectorial), transformers + torch (el LLM generador).
# 🧠 Prompt: "instalá sentence-transformers, faiss-cpu, transformers, torch y accelerate para Colab"
!pip -q install sentence-transformers faiss-cpu transformers torch accelerate
# Si Colab pide "RESTART RUNTIME" tras instalar, reiniciar y volver a ejecutar desde acá.
# 🧠 Prompt: "importá numpy, pandas, torch, faiss, SentenceTransformer y las clases de transformers para un modelo seq2seq (flan-t5), y detectá si hay GPU disponible"
import numpy as np
import pandas as pd
import torch
import faiss
from sentence_transformers import SentenceTransformer
from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
print("Dispositivo:", DEVICE)
<!-- 🧠 Prompt: "explicá que la base de conocimiento es el dataset del TP4 (biblioteca) y pegalo de nuevo acá para que el notebook sea autocontenido" -->
1. Base de conocimiento (dataset del TP4)
La base de conocimiento del RAG es el dataset de 22 pares pregunta–respuesta de la biblioteca que armamos en el TP4. Lo reproducimos acá para que el notebook sea autocontenido.
# 🧠 Prompt: "pegá las 22 preguntas y respuestas de la biblioteca del TP4 como knowledge base y armá un DataFrame"
kb_preguntas = [
"¿Cómo me asocio a la biblioteca?",
"¿Qué documentación necesito para sacar el carnet de socio?",
"¿Cuánto cuesta la membresía anual?",
"Perdí mi carnet, ¿cómo pido uno nuevo?",
"¿Cuál es el horario de atención?",
"¿La biblioteca abre los fines de semana?",
"¿Dónde queda la biblioteca?",
"¿Cuántos libros puedo pedir prestados a la vez?",
"¿Por cuántos días me prestan un libro?",
"¿Cómo renuevo el préstamo de un libro?",
"¿Qué pasa si devuelvo un libro fuera de término?",
"¿Cómo reservo un libro que está prestado a otra persona?",
"¿Tienen libros electrónicos para descargar?",
"¿Cómo accedo a la biblioteca digital desde mi casa?",
"¿Tienen audiolibros disponibles?",
"¿Puedo usar las computadoras de la sala de lectura?",
"¿Hay wifi gratuito en la biblioteca?",
"¿Puedo imprimir o sacar fotocopias?",
"¿Tienen una sala de estudio para grupos?",
"¿Realizan actividades o talleres para niños?",
"¿Tienen clubes de lectura para adultos?",
"¿Cómo puedo donar libros a la biblioteca?",
]
kb_respuestas = [
"Para asociarte acercate al mostrador con tu DNI y una foto carnet, o completá el formulario online en nuestro sitio web. El alta es inmediata.",
"Necesitás tu DNI, una foto tipo carnet y un comprobante de domicilio reciente.",
"La membresía anual cuesta $3.000 para adultos y es gratuita para menores de 18 años, estudiantes y jubilados.",
"Si perdiste el carnet, informalo en el mostrador. Te emitimos uno nuevo en el momento por un costo de reposición de $500.",
"Atendemos de lunes a viernes de 9 a 20 h, de corrido.",
"Sí: los sábados abrimos de 9 a 14 h. Los domingos y feriados permanecemos cerrados.",
"Estamos en Av. de los Lectores 1234, a dos cuadras de la plaza central.",
"Cada socio puede llevarse hasta 3 libros simultáneamente en préstamo a domicilio.",
"El préstamo a domicilio es por 15 días corridos, renovable si nadie más reservó el material.",
"Podés renovar el préstamo desde tu cuenta en la biblioteca digital, por teléfono o en el mostrador, antes del vencimiento.",
"Por cada día de demora se aplica una multa de $100 por libro. Con multas impagas no se pueden retirar nuevos materiales.",
"Pedí la reserva en el mostrador o desde la web. Cuando se devuelva, te avisamos por mail y queda reservado 48 horas.",
"Sí, tenemos un catálogo de libros electrónicos que podés descargar gratis con tu usuario de socio desde la biblioteca digital.",
"Ingresá a biblioteca-sarmiento.org.ar con tu número de socio y contraseña: accedés a ebooks, audiolibros y bases de datos las 24 horas.",
"Sí, tenemos una colección de audiolibros en español, para escuchar online o descargar desde la app de la biblioteca.",
"Sí, hay 10 computadoras de uso libre en la sala de lectura, gratuitas por turnos de una hora.",
"Sí, ofrecemos wifi gratuito para todos los visitantes; la contraseña está publicada en la sala de lectura.",
"Tenemos servicio de impresión y fotocopias: $20 la copia en blanco y negro y $80 a color.",
"Sí, tenemos dos salas de estudio grupal que se reservan con anticipación en el mostrador o por teléfono.",
"Todos los sábados a las 11 h hacemos 'La hora del cuento' y talleres de lectura para niños de 4 a 10 años, con entrada libre.",
"Sí, el club de lectura para adultos se reúne el último jueves de cada mes a las 18 h, sin inscripción previa.",
"Aceptamos donaciones de libros en buen estado: acercalos al mostrador y los evaluamos para el catálogo o la feria solidaria.",
]
assert len(kb_preguntas) == len(kb_respuestas)
# Documentos de contexto para el RAG: combinamos pregunta + respuesta
kb_contextos = [f"P: {p} R: {r}" for p, r in zip(kb_preguntas, kb_respuestas)]
print("Documentos en la base de conocimiento:", len(kb_contextos))
pd.DataFrame({"pregunta": kb_preguntas, "respuesta": kb_respuestas}).head(4)
<!-- 🧠 Prompt: "presentá la consigna a) y armá un dataset de evaluación NUEVO con preguntas parafraseadas y su respuesta esperada (ground truth), distinto al de la base" -->
2. a) Conjunto de datos de evaluación
> Consigna a). Además del dataset original, crear un dataset de prueba/evaluación con la misma lógica (preguntas y respuestas).
Creamos 10 preguntas nuevas que un usuario real escribiría —parafraseadas, más coloquiales, distintas a las de la base— junto con la respuesta esperada (*ground truth*). Sirven para medir si el RAG recupera el contexto correcto y genera una respuesta acorde.
# 🧠 Prompt: "definí eval_preguntas (10 paráfrasis coloquiales) y eval_respuestas_esperadas (ground truth) alineadas, para evaluar el chatbot RAG"
eval_preguntas = [
"Quiero hacerme socio, ¿qué tengo que hacer?",
"¿A qué hora cierra la biblioteca?",
"¿Atienden los sábados?",
"¿Cuántos ejemplares me puedo llevar?",
"Se me pasó la fecha de devolución, ¿hay multa?",
"¿Puedo leer libros desde el celular en casa?",
"¿Tienen internet para los visitantes?",
"¿Hay algo para que vayan los chicos?",
"Quiero regalar libros que ya no uso, ¿los reciben?",
"¿Se puede estudiar en grupo en algún lugar?",
]
eval_respuestas_esperadas = [
"Acercarse al mostrador con DNI y foto, o asociarse online; el alta es inmediata.",
"Cierra a las 20 h de lunes a viernes.",
"Sí, los sábados de 9 a 14 h.",
"Hasta 3 libros a la vez.",
"Sí, $100 por día y por libro de multa.",
"Sí, con libros electrónicos de la biblioteca digital usando el usuario de socio.",
"Sí, wifi gratuito para todos los visitantes.",
"Sí, talleres y 'La hora del cuento' para niños los sábados.",
"Sí, reciben donaciones de libros en buen estado en el mostrador.",
"Sí, hay dos salas de estudio grupal que se reservan con anticipación.",
]
assert len(eval_preguntas) == len(eval_respuestas_esperadas)
print("Preguntas de evaluación:", len(eval_preguntas))
pd.DataFrame({"pregunta_eval": eval_preguntas, "respuesta_esperada": eval_respuestas_esperadas})
<!-- 🧠 Prompt: "presentá la consigna b) y justificá la elección del LLM (flan-t5) y de los DOS modelos de embeddings (MiniLM multilingüe y multilingual-e5-small), explicando por qué cada uno" -->
3. b) Elección de modelos
> Consigna b). Elegir un LLM de HuggingFace y al menos dos modelos de embeddings. Justificar.
LLM generador: google/flan-t5-base.
- Es un modelo seq2seq instruido (sigue instrucciones), abierto y liviano (~250M parámetros) → corre en Colab gratis, incluso en CPU.
- Funciona bien para preguntas-respuesta sobre un contexto dado, que es justo lo que necesita RAG. (Limitación conocida: está entrenado mayormente en inglés, así que a veces la redacción en español sale algo rígida; lo comentaremos en las conclusiones.)
Embeddings (comparamos dos):
1. sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2 — 384 dimensiones. Rápido y chico, multilingüe (incluye español), excelente relación calidad/velocidad. Buen *baseline*.
2. intfloat/multilingual-e5-small — 384 dimensiones. Modelo E5, entrenado específicamente para recuperación; requiere prefijar las consultas con "query: " y los documentos con "passage: ". Suele rendir mejor en *retrieval* que MiniLM a igual tamaño.
Comparar MiniLM vs E5 nos permite ver el impacto del modelo de embeddings sobre la calidad del RAG, manteniendo fijos el LLM y la base vectorial.
# 🧠 Prompt: "definí los nombres de los modelos: el LLM flan-t5-base y los dos embeddings (MiniLM y multilingual-e5-small), en constantes"
LLM_MODEL = "google/flan-t5-base"
EMB_MINILM = "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2"
EMB_E5 = "intfloat/multilingual-e5-small"
print("LLM:", LLM_MODEL)
print("Embeddings 1:", EMB_MINILM)
print("Embeddings 2:", EMB_E5)
# 🧠 Prompt: "cargá el tokenizer y el modelo flan-t5-base con transformers, moviéndolo al dispositivo, y definí una función generar(prompt) que produzca texto con el LLM"
print("Cargando LLM (puede tardar la primera vez)...")
llm_tokenizer = AutoTokenizer.from_pretrained(LLM_MODEL)
llm_model = AutoModelForSeq2SeqLM.from_pretrained(LLM_MODEL).to(DEVICE)
def generar(prompt, max_new_tokens=120):
"""Genera texto con el LLM a partir de un prompt."""
inputs = llm_tokenizer(prompt, return_tensors="pt", truncation=True,
max_length=512).to(DEVICE)
with torch.no_grad():
out = llm_model.generate(**inputs, max_new_tokens=max_new_tokens,
num_beams=4, early_stopping=True)
return llm_tokenizer.decode(out[0], skip_special_tokens=True)
print("LLM listo. Prueba:", generar("Responde en español: ¿Qué es una biblioteca?"))
<!-- 🧠 Prompt: "presentá la consigna c) y explicá la arquitectura de la clase ChatBot: indexa la base con un embedder en FAISS (índice de producto interno = coseno con vectores normalizados), recupera top-k y arma un prompt para el LLM" -->
4. c) Implementación de la clase ChatBot (RAG con FAISS)
> Consigna c). Implementar una clase ChatBot con lo elegido en b), usando una base de datos vectorial (Chroma/FAISS).
Diseño de la clase:
1. Indexación: codifica los contextos de la base con el embedder, normaliza L2 y los guarda en un índice faiss.IndexFlatIP (producto interno = similitud del coseno sobre vectores normalizados).
2. Recuperación: dada una pregunta, la codifica (con el prefijo query: si el modelo es E5), busca los top-k contextos más parecidos.
3. Generación: arma un prompt con esos contextos y se lo pasa al LLM para que redacte la respuesta. Devuelve también los contextos (para poder evaluarlos después).
# 🧠 Prompt: "implementá la clase ChatBot RAG: __init__ que recibe contextos, embedder, su nombre, función generadora y k; indexa en FAISS con IndexFlatIP y vectores normalizados; método _encode que agrega prefijos E5; método recuperar(pregunta) y método responder(pregunta) que arma el prompt y llama al LLM"
class ChatBot:
"""Chatbot RAG: recupera contextos con FAISS y genera la respuesta con un LLM."""
def __init__(self, contextos, embedder, embedder_name, gen_fn, k=3):
self.contextos = contextos
self.embedder = embedder
self.is_e5 = "e5" in embedder_name.lower() # E5 necesita prefijos query/passage
self.gen_fn = gen_fn
self.k = k
# --- indexación en FAISS ---
docs = [f"passage: {c}" for c in contextos] if self.is_e5 else contextos
emb = self.embedder.encode(docs, convert_to_numpy=True,
normalize_embeddings=True).astype("float32")
self.index = faiss.IndexFlatIP(emb.shape[1]) # producto interno = coseno (normalizado)
self.index.add(emb)
def _encode_query(self, pregunta):
q = f"query: {pregunta}" if self.is_e5 else pregunta
return self.embedder.encode([q], convert_to_numpy=True,
normalize_embeddings=True).astype("float32")
def recuperar(self, pregunta):
"""Devuelve (contextos_top_k, similitudes)."""
qv = self._encode_query(pregunta)
sims, idxs = self.index.search(qv, self.k)
ctxs = [self.contextos[i] for i in idxs[0]]
return ctxs, sims[0].tolist()
def responder(self, pregunta):
"""Pipeline RAG completo: recupera contexto y genera respuesta. Devuelve (respuesta, contextos)."""
ctxs, _ = self.recuperar(pregunta)
contexto = "\n".join(ctxs)
prompt = (
"Sos el asistente de una biblioteca. Respondé en español, de forma breve y "
"amable, usando SOLO la siguiente información:\n"
f"{contexto}\n\n"
f"Pregunta: {pregunta}\nRespuesta:"
)
return self.gen_fn(prompt), ctxs
# 🧠 Prompt: "cargá los dos modelos de embeddings con SentenceTransformer e instanciá dos ChatBot (uno por embedder) sobre la base de conocimiento de la biblioteca"
print("Cargando embedders...")
emb_minilm = SentenceTransformer(EMB_MINILM, device=DEVICE)
emb_e5 = SentenceTransformer(EMB_E5, device=DEVICE)
bot_minilm = ChatBot(kb_contextos, emb_minilm, EMB_MINILM, generar, k=3)
bot_e5 = ChatBot(kb_contextos, emb_e5, EMB_E5, generar, k=3)
print("Dos chatbots RAG instanciados (MiniLM y E5).")
<!-- 🧠 Prompt: "presentá la consigna d): probar el chatbot con el dataset de evaluación comparando los dos embeddings, mostrando la respuesta generada por cada uno y el contexto top-1 recuperado" -->
5. d) Prueba del chatbot y comparación de embeddings
> Consigna d). Probar el chatbot con las preguntas del dataset a), comparando al menos dos modelos de embeddings. Justificar cuál se elegiría.
Corremos las 10 preguntas de evaluación por ambos bots y mostramos: la respuesta generada por cada uno y el contexto top-1 que recuperó cada embedder.
# 🧠 Prompt: "para cada pregunta de evaluación, generá la respuesta con bot_minilm y bot_e5, guardando contexto recuperado y respuesta, y armá un DataFrame comparativo"
filas = []
ctx_minilm_all, ctx_e5_all = [], []
ans_minilm_all, ans_e5_all = [], []
for q, esperada in zip(eval_preguntas, eval_respuestas_esperadas):
ans_m, ctx_m = bot_minilm.responder(q)
ans_e, ctx_e = bot_e5.responder(q)
ctx_minilm_all.append(ctx_m); ctx_e5_all.append(ctx_e)
ans_minilm_all.append(ans_m); ans_e5_all.append(ans_e)
filas.append({
"pregunta": q,
"ctx_top1_MiniLM": ctx_m[0][:55],
"ctx_top1_E5": ctx_e[0][:55],
})
pd.set_option("display.max_colwidth", 60)
pd.DataFrame(filas)
# 🧠 Prompt: "imprimí de forma legible, para 4 preguntas de evaluación, la respuesta esperada y las respuestas generadas por MiniLM y por E5 para compararlas a ojo"
for i in [0, 1, 4, 7]:
print("=" * 78)
print("PREGUNTA :", eval_preguntas[i])
print("ESPERADA :", eval_respuestas_esperadas[i])
print("MiniLM :", ans_minilm_all[i])
print("E5 :", ans_e5_all[i])
print("=" * 78)
<!-- 🧠 Prompt: "redactá el análisis comparativo MiniLM vs E5: qué mirar (si recuperan el contexto correcto), trade-offs, y la elección justificada del embedding para esta aplicación" -->
5.1 Análisis comparativo y elección
Qué observar al correr las celdas anteriores:
- Recuperación (lo que más impacta en RAG): revisamos si el contexto top-1 es el correcto para cada pregunta. E5 suele recuperar mejor en *paraphrase/retrieval* porque fue entrenado para esa tarea y usa el esquema
query/passage; MiniLM es algo más rápido pero puede confundir preguntas del mismo tema.
- Generación: como el LLM es el mismo (flan-t5), las diferencias en la respuesta final vienen casi siempre de qué contexto se recuperó: si el embedder trae el pasaje correcto, el LLM responde bien; si trae uno equivocado, el LLM "alucina" sobre el contexto incorrecto. Esto confirma la regla de oro de RAG: la calidad final depende sobre todo del recuperador y de la base de conocimiento.
- Trade-off: ambos son de 384 dimensiones y tamaño similar; E5 agrega un pequeño costo por los prefijos pero mejora la precisión de recuperación.
Elección: para esta aplicación de biblioteca elegimos multilingual-e5-small, porque la calidad de recuperación es lo determinante en RAG y E5 la mejora sin un costo relevante. Si la prioridad fuera latencia máxima con recursos muy limitados, MiniLM sería una alternativa razonable. (La conclusión se confirma cuantitativamente con las métricas del punto e.)
<!-- 🧠 Prompt: "presentá el BONUS e): context precision y context recall implementadas a mano con similitud coseno entre embeddings y un umbral, explicando qué mide cada una" -->
6. e) BONUS — Context Precision y Context Recall
> Consigna e). Evaluar el chatbot para ambos embeddings con context precision y context recall (con ragas o implementación propia).
Implementamos las métricas a mano con similitud del coseno (sin ragas, para no depender de claves de API):
- Context Precision: de los k contextos recuperados, ¿qué fracción es relevante para la respuesta esperada? (relevante = coseno(contexto, respuesta_esperada) ≥ umbral). Mide *"no traer basura"*.
- Context Recall: ¿el contexto correcto está entre los recuperados? Aproximamos: máxima similitud entre los contextos recuperados y la respuesta esperada (¿se "cubrió" la info necesaria?). Mide *"no perderse lo importante"*.
# 🧠 Prompt: "implementá context_precision (fracción de contextos recuperados con coseno >= umbral respecto de la respuesta esperada) y context_recall (máxima similitud entre contextos recuperados y la respuesta esperada), usando un embedder para medir; luego promediá sobre el dataset de evaluación para MiniLM y E5"
def _sim(a, b, embedder):
va, vb = embedder.encode([a, b], convert_to_numpy=True, normalize_embeddings=True)
return float(np.dot(va, vb))
def context_precision(contextos, esperada, embedder, umbral=0.5):
rel = [1 if _sim(c, esperada, embedder) >= umbral else 0 for c in contextos]
return sum(rel) / len(rel) if rel else 0.0
def context_recall(contextos, esperada, embedder):
return max(_sim(c, esperada, embedder) for c in contextos) if contextos else 0.0
def evaluar_contexto(ctx_all, embedder):
precs, recs = [], []
for ctxs, esp in zip(ctx_all, eval_respuestas_esperadas):
precs.append(context_precision(ctxs, esp, embedder))
recs.append(context_recall(ctxs, esp, embedder))
return np.mean(precs), np.mean(recs)
# usamos MiniLM como "juez" de similitud para ambos (juez fijo => comparación justa)
juez = emb_minilm
p_m, r_m = evaluar_contexto(ctx_minilm_all, juez)
p_e, r_e = evaluar_contexto(ctx_e5_all, juez)
pd.DataFrame({
"embedding": ["MiniLM", "E5"],
"context_precision": [round(p_m, 3), round(p_e, 3)],
"context_recall": [round(r_m, 3), round(r_e, 3)],
})
<!-- 🧠 Prompt: "dejá un análisis de los resultados de precision/recall explicando que si E5 da mejor recall refuerza la elección de d), y aclarando que los valores dependen del umbral y del juez" -->
6.1 Análisis Context Precision / Recall
- Un context recall más alto indica que el embedder trae contextos más afines a la respuesta correcta: si E5 lo supera, refuerza la elección del punto d).
- La precision depende del umbral (0.5): subirlo la vuelve más exigente. Como recuperamos
k=3 y muchas preguntas tienen un único pasaje realmente relevante, la precision raramente llega a 1 (es esperable, no un error).
- Usamos un juez fijo (MiniLM) para medir similitud en ambos casos: así la comparación entre embedders es justa (no que cada uno se "autoevalúe").
<!-- 🧠 Prompt: "presentá el BONUS f): answer relevancy y faithfulness implementadas a mano, explicando qué mide cada una, evaluando el LLM con el mejor embedding (E5)" -->
7. f) BONUS — Answer Relevancy y Faithfulness
> Consigna f). Evaluar el LLM con el mejor embedding de d) usando answer relevancy y faithfulness.
- Answer Relevancy: ¿la respuesta generada es pertinente a la pregunta? La medimos como coseno(pregunta, respuesta_generada).
- Faithfulness (fidelidad): ¿la respuesta se apoya en el contexto recuperado (no inventa)? La medimos como coseno(respuesta_generada, contexto_recuperado): si es alta, el LLM no se fue de tema respecto del contexto.
Evaluamos el mejor pipeline (LLM flan-t5 + embeddings E5).
# 🧠 Prompt: "implementá answer_relevancy (coseno pregunta vs respuesta generada) y faithfulness (coseno respuesta generada vs contexto recuperado concatenado) y promediá sobre el dataset de evaluación usando las respuestas de E5"
def answer_relevancy(pregunta, respuesta, embedder):
return _sim(pregunta, respuesta, embedder)
def faithfulness(respuesta, contextos, embedder):
contexto = " ".join(contextos)
return _sim(respuesta, contexto, embedder)
rel_list, faith_list = [], []
for q, ans, ctxs in zip(eval_preguntas, ans_e5_all, ctx_e5_all):
rel_list.append(answer_relevancy(q, ans, juez))
faith_list.append(faithfulness(ans, ctxs, juez))
print(f"Answer Relevancy (promedio): {np.mean(rel_list):.3f}")
print(f"Faithfulness (promedio): {np.mean(faith_list):.3f}")
pd.DataFrame({
"pregunta": [q[:40] for q in eval_preguntas],
"answer_relevancy": np.round(rel_list, 3),
"faithfulness": np.round(faith_list, 3),
})
<!-- 🧠 Prompt: "redactá el análisis de answer relevancy y faithfulness y conectalo con las limitaciones del LLM flan-t5 en español" -->
7.1 Análisis Answer Relevancy / Faithfulness
- Answer Relevancy alta = las respuestas apuntan a lo que se preguntó; valores bajos suelen darse cuando el LLM responde de más o en inglés.
- Faithfulness alta = la respuesta se mantiene dentro del contexto recuperado (poca alucinación). Como forzamos en el prompt *"usá SOLO la siguiente información"*, esperamos fidelidad alta; cuando baja, suele ser porque el contexto recuperado era el equivocado (otra vez: el cuello de botella es la recuperación).
- Limitación del LLM:
flan-t5-base está entrenado mayormente en inglés; a veces redacta en español algo rígido o mezcla idioma. Para producción convendría un LLM instruido más fuerte en español (p. ej. modelos *instruct* multilingües), manteniendo el mismo pipeline RAG.
<!-- 🧠 Prompt: "redactá conclusiones generales del TP5: qué se construyó, RAG vs recuperación pura del TP4, hallazgos sobre embeddings y métricas, y aprendizajes" -->
8. Conclusiones
- Se construyó un chatbot RAG completo sobre el dominio biblioteca:
pregunta → embedding → FAISS (top-k) → prompt con contexto → LLM (flan-t5) → respuesta.
- Diferencia con el TP4: el TP4 devolvía la respuesta más parecida (recuperación pura); acá el LLM genera la respuesta a partir del contexto recuperado → respuestas más naturales y capaces de combinar información, a cambio de mayor costo y riesgo de alucinación.
- Comparación de embeddings: E5 (entrenado para *retrieval*, con esquema query/passage) tiende a recuperar mejor que MiniLM a igual tamaño; las métricas de *context recall* lo respaldan.
- Regla de oro confirmada: en RAG, la calidad final depende sobre todo del recuperador y de la base de conocimiento. Un mal contexto hace que hasta un buen LLM responda mal.
- Métricas propias (precision/recall, relevancy/faithfulness vía coseno) permiten comparar configuraciones sin depender de
ragas/API, aunque son una aproximación de las definiciones formales.
- Aprendizaje: lo más desafiante fue (1) hacer que E5 funcione con sus prefijos correctos y (2) traducir las métricas de RAG a similitud del coseno de forma que tengan sentido.
<!-- 🧠 Prompt: "armá las referencias del TP5: sentence-transformers, FAISS, flan-t5, los modelos de embeddings de HF, ragas, el notebook de la cátedra, y el placeholder de IA" -->
9. g) Referencias
- Ana Laura Diedrichs — *Procesamiento del Habla*, notebooks de clase sobre embeddings y RAG. https://github.com/anadiedrichs/procesamientoDelHabla
- Lewis et al. (2020) — *Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks*. https://arxiv.org/abs/2005.11401
- Sentence-Transformers. https://www.sbert.net/
- FAISS (Facebook AI Similarity Search). https://github.com/facebookresearch/faiss
- LLM google/flan-t5-base. https://huggingface.co/google/flan-t5-base
- Embeddings paraphrase-multilingual-MiniLM-L12-v2. https://huggingface.co/sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2
- Embeddings intfloat/multilingual-e5-small. https://huggingface.co/intfloat/multilingual-e5-small
- Ragas — métricas de evaluación de RAG. https://docs.ragas.io/
- Conversación con IA generativa (apoyo a la redacción/depuración): _[completar: link a conversación IA]_