English · Español
Lab 03 — RAG end-to-end: reader + CLI + faithfulness¶
Conectas el retriever híbrido a un reader — el grammar MiniGPT (Fase 17/24) con la LoRA de la Fase 28 si la tienes, o el modelo base si no. La consulta entra, los chunks salen, el reader genera la respuesta con citas. Mides faithfulness: cuántas respuestas se apoyan en chunks reales y no fabrican reglas.
Objetivo¶
Construir la CLI completa verb-tutor: toma una consulta en lenguaje natural, llama al retriever híbrido, pasa los top-k chunks + la consulta al reader MiniGPT, devuelve una respuesta más una lista de chunk_ids citados. Evaluar la faithfulness end-to-end sobre 30 consultas.
Setup¶
- Labs 00-02 hechos.
- El grammar MiniGPT de la Fase 17/24. El adaptador LoRA de la Fase 28 es opcional; sin él, el modelo base funciona (respuestas menos pulidas, mismo pipeline).
theory/04-evaluation.mdpara la definición de faithfulness.
Tareas¶
Parte A — La plantilla del prompt¶
data/kb/grammar-rules/reader_prompt.txt:
You are a bilingual grammar tutor. Use the rules below to answer the
student's question. Cite the rule by its chunk_id in square brackets,
like [chunk_id]. If the rules do not contain the answer, say so plainly
("I don't know from the provided rules") rather than guess.
Rules:
{rules_section}
Question: {query}
Answer:
Donde {rules_section} son los top-k chunks concatenados con este formato:
[en-pres-3sg-regular-s-rule-001] Present simple, 3rd-person singular: add -s
In English present simple, the 3rd-person singular form of a regular verb adds -s...
[en-past-all-irregular-go-001] Irregular past: go → went
The verb "go" has an irregular past form "went"...
Esta plantilla está congelada. Los cambios invalidan comparaciones entre ejecuciones.
Parte B — Implementar src/minirag/generate.py¶
from pathlib import Path
from .retrieve import HybridRetriever
class Reader:
"""Wraps the grammar MiniGPT (Phase 17/24) with a prompt-based interface.
Phase 29 doesn't fine-tune the reader — that's Phase 28's LoRA. The reader
here accepts a fully-formatted prompt and returns generated text.
"""
def __init__(self, model_path: Path, max_new_tokens: int = 128):
# Load the Phase-24 PyTorch port, or the Phase-17 NumPy model
self.model = ... # load
self.tokenizer = ...
self.max_new_tokens = max_new_tokens
def generate(self, prompt: str) -> str:
ids = self.tokenizer.encode(prompt, return_tensors="pt")
out = self.model.generate(ids, max_new_tokens=self.max_new_tokens,
do_sample=False, temperature=1.0)
return self.tokenizer.decode(out[0][ids.shape[1]:], skip_special_tokens=True)
class RAGPipeline:
def __init__(self, retriever: HybridRetriever, reader: Reader,
prompt_template: str, k: int = 5):
self.retriever = retriever
self.reader = reader
self.template = prompt_template
self.k = k
self.chunks = retriever.dense.chunks # chunk_id → Chunk
def answer(self, query: str) -> dict:
results = self.retriever.search(query, k=self.k)
rules_section = self._format_rules(results)
prompt = self.template.format(rules_section=rules_section, query=query)
text = self.reader.generate(prompt)
cited = self._extract_citations(text)
return {
"query": query,
"retrieved": [{"chunk_id": cid, "score": s} for cid, s in results],
"answer": text,
"cited_chunks": cited,
}
def _format_rules(self, results):
lines = []
for cid, _ in results:
c = self.chunks[cid]
lines.append(f"[{cid}] {c.title}\n {c.body}")
return "\n\n".join(lines)
@staticmethod
def _extract_citations(text: str) -> list[str]:
import re
return re.findall(r"\[([a-z][\w-]+)\]", text)
Parte C — La CLI¶
src/minirag/cli.py:
import argparse, json
from pathlib import Path
from .embed import Embedder
from .retrieve import DenseRetriever, HybridRetriever
from .bm25 import BM25, BM25Retriever
from .generate import Reader, RAGPipeline
from .chunk import load_chunks
def main():
parser = argparse.ArgumentParser()
sub = parser.add_subparsers(dest="cmd")
ask = sub.add_parser("ask")
ask.add_argument("query", type=str)
ask.add_argument("--k", type=int, default=5)
args = parser.parse_args()
if args.cmd == "ask":
embedder = Embedder()
dense = DenseRetriever(Path("data/kb/grammar-rules/chunks.jsonl"), embedder)
bm25 = BM25Retriever(Path("data/kb/grammar-rules/chunks.jsonl"))
retriever = HybridRetriever(dense, bm25)
reader = Reader(Path("models/grammar-minigpt"))
template = Path("data/kb/grammar-rules/reader_prompt.txt").read_text()
pipeline = RAGPipeline(retriever, reader, template, k=args.k)
out = pipeline.answer(args.query)
print(json.dumps(out, indent=2, ensure_ascii=False))
if __name__ == "__main__":
main()
Registrada como verb-tutor en pyproject.toml:
Parte D — Evaluación de faithfulness¶
data/kb/grammar-rules/eval/faithfulness.jsonl — 30 consultas, cada una con:
{
"query_id": "q-001",
"query": "How do I conjugate 'work' for she in present tense?",
"expected_chunks": ["en-pres-3sg-regular-s-rule-001"],
"expected_answer_pattern": "works",
"must_cite": ["en-pres-3sg-regular-s-rule-001"]
}
La faithfulness tiene dos sub-métricas:
- Corrección de cita: el modelo cita al menos un chunk de
must_cite. Puntúa 1 si sí, 0 si no. - Coincidencia de patrón: la respuesta del modelo contiene
expected_answer_pattern(substring sin distinción de mayúsculas). Puntúa 1 si sí, 0 si no.
Score de faithfulness = media sobre las consultas de (citation_correct AND pattern_match).
DoD: faithfulness ≥ 0.70.
def evaluate_faithfulness(pipeline, queries):
rows = []
for q in queries:
out = pipeline.answer(q["query"])
citation_ok = any(c in q["must_cite"] for c in out["cited_chunks"])
pattern_ok = q["expected_answer_pattern"].lower() in out["answer"].lower()
rows.append({
"query_id": q["query_id"],
"answer": out["answer"],
"cited_chunks": out["cited_chunks"],
"citation_ok": int(citation_ok),
"pattern_ok": int(pattern_ok),
"faithful": int(citation_ok and pattern_ok),
})
return rows
Parte E — Revisión cualitativa manual¶
Tras calcular la métrica, lee las 5-10 peores respuestas. Categoriza los modos de fallo:
- Cita errónea, respuesta correcta: el reader produjo la forma correcta pero citó la regla equivocada. Problema del retriever.
- Cita correcta, respuesta errónea: el reader tenía la regla correcta pero generó texto incorrecto. Problema del reader.
- Sin cita, sin respuesta: el reader dijo "no sé" o se negó. Podría ser el comportamiento correcto (la KB no tiene la regla) — verifícalo a mano.
- Cita errónea, respuesta errónea: ambos rotos. Peor caso.
- Cita alucinada: el reader citó un
chunk_idque no existe en la KB. Malo. No debería ocurrir nunca con un reader bien prompteado.
Documenta en experiments/29-e2e/FAILURE_MODES.md.
Parte F — Informe end-to-end¶
experiments/29-e2e/REPORT.md:
- Diagrama del pipeline (query → retrieve → format prompt → reader → parse cites → answer).
- Métricas agregadas: hit@5, MRR (del retriever), precisión de citas, coincidencia de patrón, faithfulness.
- Comparativa lado a lado: 3 consultas con sus chunks recuperados + respuesta generada + veredicto de faithfulness.
- Tabla de modos de fallo (Parte E).
- Un párrafo: "qué funciona, qué no, y qué cambiaría yo."
- Lo que no cambiaría: llama explícitamente 1-2 cosas que el usuario podría pensar que hay que arreglar pero que en realidad se comportaron correctamente (p. ej., "el modelo se negó correctamente a responder consultas fuera del alcance §A13 — eso funciona según lo previsto").
Parte G — Tests en tests/minirag/test_e2e.py¶
test_prompt_format— dado un retriever de fixture que devuelve 2 chunks, el prompt renderizado contiene ambos títulos y ambos cuerpos.test_extract_citations— dado un texto"... see [en-pres-3sg-rule-001] and [es-past-001] ...",_extract_citationsdevuelve["en-pres-3sg-rule-001", "es-past-001"].test_faithfulness_aggregator— dados veredictos por consulta conocidos, el agregado se calcula correctamente.test_cli_smoke— ejecutaverb-tutor ask "test"y verifica que el JSON de salida tiene las cuatro claves requeridas (query,retrieved,answer,cited_chunks).test_no_hallucinated_citation— sobre las 30 consultas de eval, toda cita en la salida existe en loschunk_idsde la KB. (Fallo duro si no.)
Entregables¶
experiments/29-e2e/:
- REPORT.md — informe completo del pipeline.
- FAILURE_MODES.md — fallos categorizados.
- metrics.json — números agregados.
- per_query.jsonl — registros completos por consulta (query, retrieved, answer, cites, scores).
- manifest.json.
learners/borja/phase-29/reflections.md — 300-500 palabras sobre qué te aportó RAG comparado con un modelo closed-book entrenado solo, y dónde el pipeline muestra sus costuras.
Aceptación¶
- Pasan los 5 tests.
- Faithfulness ≥ 0.70 (objetivo DoD).
- Cero citas alucinadas en las 30 consultas de eval.
verb-tutor ask "what's the past participle of eat?"devuelve una respuesta sensata con un chunk real citado.
Trampas¶
- Reader que alucina
chunk_ids. Si el modelo fabrica ids (p. ej., cita[en-pres-3sg-make-001]cuando no existe tal chunk), tu prompt no está restringiendo lo suficiente. Añade la instrucción explícita: "Cite chunk_ids exactly as shown. Do not invent new chunk_ids." - Prompt demasiado largo para el contexto del modelo. El Grammar MiniGPT (Fase 17) se entrenó sobre secuencias cortas. Con 5 chunks × 600 caracteres + consulta + texto de envoltura, puedes exceder el rango del positional encoding del modelo. Baja a k=3 o usa un modo de resumen de chunks.
- Regex de cita demasiado glotona. Una regex como
\[(.+?)\]matcheará[any-arbitrary-text]. Restríngela:\[([a-z][\w-]+)\]para que solo coincidan ids en kebab-case. - Faithfulness medida solo por cita, no por corrección de respuesta. Un modelo que solo cite el chunk correcto pero genere una respuesta irrelevante puntúa 0.5 (citation_ok=1, pattern_ok=0). La combinación AND lo impide — deben darse ambas.
- El texto generado es determinista pero vacuo. Con
do_sample=False, el reader puede producir respuestas vacías o de una palabra. Ponmin_new_tokenspara forzar salida sustantiva, o muestrea a temperatura baja (T=0.3). must_citedemasiado estricto. Algunas consultas tienen genuinamente múltiples respuestas correctas (p. ej., una pregunta sobre "go" podría citar el chunk de pasado simple o el de participio pasado). Permite quemust_citesea un conjunto, y la métricacitation_okes "cualquier solapamiento conmust_cite".
Stretch¶
- Añade un cross-encoder re-ranker (teoría 03) entre el retrieval híbrido y el reader. Mide la mejora de faithfulness.
- Implementa HyDE (Hypothetical Document Embeddings): genera primero una respuesta falsa, embéela, recupera con la falsa. Compara con el embedding de consulta estándar.
- Añade una métrica de calidad del rechazo: para consultas que la KB no puede responder (fuera de alcance deliberadamente), el modelo debe negarse. Puntúa los rechazos correctos.
Fin de los labs de la Fase 29. Toca escribir PHASE_29_REPORT.md y preparar la Fase 30.
Siguiente: Fase 30 — Generación estructurada y decodificación restringida.