feat(rag): adatta pipeline allo schema chunks AST-based + ottimizza system prompt

config.py:
- EMBED_MODEL: qwen3-embedding:0.6b → bge-m3 (multilingua, migliore su testi accademici)
- SYSTEM_PROMPT: lingua esplicita, anti-allucinazione rafforzata, citazione strutturata
  con percorso sezione, passaggi numerati per spiegazioni, fallback al plurale

ingestion/ingest.py:
- embed su content_for_embedding (prefisso header contestuale)
- store content_original in ChromaDB (testo pulito per retrieval)
- metadata aggiornati: header_path, chunk_index, content_type, flags, start/end_line

rag.py, retrieve.py:
- sostituisce sezione/titolo (schema vecchio) con header_path (schema AST)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-09 09:10:35 +02:00
parent 89d77d35d6
commit 2939d2f8ca
4 changed files with 40 additions and 34 deletions
+9 -5
View File
@@ -24,7 +24,7 @@ NO_THINK = True
# Modello di embedding usato da Ollama. # Modello di embedding usato da Ollama.
# Deve corrispondere al modello usato durante la vettorizzazione (ingest.py). # Deve corrispondere al modello usato durante la vettorizzazione (ingest.py).
# Se cambi questo, devi rieseguire ingest.py con --force. # Se cambi questo, devi rieseguire ingest.py con --force.
EMBED_MODEL = "qwen3-embedding:0.6b" EMBED_MODEL = "bge-m3"
# Caratteri massimi inviati al modello di embedding. # Caratteri massimi inviati al modello di embedding.
# Il testo viene troncato SOLO per il vettore; il documento completo # Il testo viene troncato SOLO per il vettore; il documento completo
@@ -45,8 +45,12 @@ OLLAMA_MODEL = "qwen3.5:4b"
# Mantieni le risposte ancorate al contesto per evitare allucinazioni. # Mantieni le risposte ancorate al contesto per evitare allucinazioni.
SYSTEM_PROMPT = ( SYSTEM_PROMPT = (
"Sei un assistente specializzato nell'analisi di documenti accademici. " "Sei un assistente specializzato nell'analisi di documenti accademici. "
"Rispondi alla domanda basandoti esclusivamente sul contesto fornito. " "Rispondi sempre in italiano. "
"Sii preciso e conciso; cita la sezione di riferimento quando è utile. " "Basati esclusivamente sui contesti numerati forniti: non aggiungere nozioni esterne, "
"Se il contesto non contiene informazioni sufficienti, rispondi: " "non inventare informazioni non presenti nel testo. "
"\"Non trovo questa informazione nel documento.\"" "Quando citi un risultato indica la sezione tra parentesi quadre usando il percorso "
"fornito (es. [Sezione > Sottosezione]). "
"Per spiegazioni strutturate usa passaggi numerati. "
"Se il contesto non contiene informazioni sufficienti rispondi esattamente: "
"\"Non trovo questa informazione nei documenti forniti.\""
) )
+15 -5
View File
@@ -139,18 +139,28 @@ def _ingest_stem(stem: str, collection: chromadb.Collection,
for i, chunk in enumerate(chunks, start=1): for i, chunk in enumerate(chunks, start=1):
t0 = time.monotonic() t0 = time.monotonic()
vector = embed(chunk["text"], model) vector = embed(chunk["content_for_embedding"], model)
t1 = time.monotonic() t1 = time.monotonic()
durations.append(t1 - t0) durations.append(t1 - t0)
hp = chunk.get("header_path", [])
flags = chunk.get("flags", {})
ids.append(f"{stem}__{chunk['chunk_id']}") ids.append(f"{stem}__{chunk['chunk_id']}")
embeddings.append(vector) embeddings.append(vector)
documents.append(chunk["text"]) documents.append(chunk["content_original"])
metadatas.append({ metadatas.append({
"source": stem, "source": stem,
"sezione": chunk.get("sezione", ""), "chunk_index": chunk.get("chunk_index", i - 1),
"titolo": chunk.get("titolo", ""), "content_type": chunk.get("content_type", ""),
"sub_index": chunk.get("sub_index", 0), "header_path": " > ".join(h["text"] for h in hp),
"start_line": chunk.get("start_line", 0),
"end_line": chunk.get("end_line", 0),
"chars": chunk.get("chars", 0),
"has_code": flags.get("has_code", False),
"has_table": flags.get("has_table", False),
"has_math": flags.get("has_math", False),
"is_overflow": flags.get("is_overflow", False),
}) })
avg = sum(durations) / len(durations) avg = sum(durations) / len(durations)
+5 -10
View File
@@ -85,7 +85,7 @@ def call_ollama(prompt: str, system: str = "") -> str:
def retrieve(collection: chromadb.Collection, question: str) -> list[dict]: def retrieve(collection: chromadb.Collection, question: str) -> list[dict]:
""" """
Genera l'embedding della domanda e recupera i TOP_K chunk più simili. Genera l'embedding della domanda e recupera i TOP_K chunk più simili.
Ritorna lista di dict con chiavi: text, sezione, titolo, distance. Ritorna lista di dict con chiavi: text, source, header_path, distance.
""" """
vector = embed(question) vector = embed(question)
results = collection.query( results = collection.query(
@@ -102,8 +102,7 @@ def retrieve(collection: chromadb.Collection, question: str) -> list[dict]:
chunks.append({ chunks.append({
"text": text, "text": text,
"source": meta.get("source", ""), "source": meta.get("source", ""),
"sezione": meta.get("sezione", ""), "header_path": meta.get("header_path", ""),
"titolo": meta.get("titolo", ""),
"distance": dist, "distance": dist,
}) })
return chunks return chunks
@@ -116,10 +115,8 @@ def build_prompt(question: str, chunks: list[dict]) -> str:
context_parts = [] context_parts = []
for i, c in enumerate(chunks, start=1): for i, c in enumerate(chunks, start=1):
header = f"[Contesto {i}" header = f"[Contesto {i}"
if c["sezione"]: if c["header_path"]:
header += f"{c['sezione']}" header += f"{c['header_path']}"
if c["titolo"]:
header += f" > {c['titolo']}"
header += "]" header += "]"
context_parts.append(f"{header}\n{c['text']}") context_parts.append(f"{header}\n{c['text']}")
@@ -140,11 +137,9 @@ def answer(question: str, collection: chromadb.Collection, verbose: bool) -> Non
if verbose: if verbose:
print("\n── Chunk recuperati ──────────────────────────────────────────") print("\n── Chunk recuperati ──────────────────────────────────────────")
for i, c in enumerate(chunks, start=1): for i, c in enumerate(chunks, start=1):
loc = c["sezione"]
if c["titolo"]:
loc += f" > {c['titolo']}"
sim = 1 - c["distance"] sim = 1 - c["distance"]
src = f"[{c['source']}] " if c.get("source") else "" src = f"[{c['source']}] " if c.get("source") else ""
loc = c["header_path"] or ""
print(f" [{i}] {src}{loc} (similarità: {sim:.3f})") print(f" [{i}] {src}{loc} (similarità: {sim:.3f})")
print(f" {c['text'][:120].replace(chr(10), ' ')}...") print(f" {c['text'][:120].replace(chr(10), ' ')}...")
print("──────────────────────────────────────────────────────────────\n") print("──────────────────────────────────────────────────────────────\n")
+3 -6
View File
@@ -65,7 +65,7 @@ def embed(text: str) -> list[float]:
def retrieve(collection: chromadb.Collection, query: str, top_k: int) -> list[dict]: def retrieve(collection: chromadb.Collection, query: str, top_k: int) -> list[dict]:
""" """
Genera l'embedding della query e recupera i top_k chunk più simili. Genera l'embedding della query e recupera i top_k chunk più simili.
Ritorna lista di dict con chiavi: rank, similarity, sezione, titolo, text. Ritorna lista di dict con chiavi: rank, similarity, source, header_path, text.
""" """
vector = embed(query) vector = embed(query)
results = collection.query( results = collection.query(
@@ -86,8 +86,7 @@ def retrieve(collection: chromadb.Collection, query: str, top_k: int) -> list[di
"rank": rank, "rank": rank,
"similarity": round(1 - dist, 4), "similarity": round(1 - dist, 4),
"source": meta.get("source", ""), "source": meta.get("source", ""),
"sezione": meta.get("sezione", ""), "header_path": meta.get("header_path", ""),
"titolo": meta.get("titolo", ""),
"text": text, "text": text,
}) })
return chunks return chunks
@@ -99,9 +98,7 @@ def print_results(chunks: list[dict], full: bool = False) -> None:
print(f"── {len(chunks)} chunk recuperati ─────────────────────────────────\n") print(f"── {len(chunks)} chunk recuperati ─────────────────────────────────\n")
for c in chunks: for c in chunks:
src = f"[{c['source']}] " if c.get("source") else "" src = f"[{c['source']}] " if c.get("source") else ""
loc = c["sezione"] loc = c["header_path"] or ""
if c["titolo"]:
loc += f" > {c['titolo']}"
print(f" [{c['rank']}] similarità: {c['similarity']:.4f} | {src}{loc}") print(f" [{c['rank']}] similarità: {c['similarity']:.4f} | {src}{loc}")
if full: if full:
print() print()