Recursive Language Models: il modello per grandi volumi di testo
Il problema della ricerca su documenti molto grandi rappresenta uno dei limiti strutturali più interessanti dei modelli linguistici di grandi dimensioni. Un LLM è straordinariamente capace nel comprendere, sintetizzare e ragionare su testo, ma opera sempre all’interno di una finestra di contesto finita. Anche quando questa finestra è ampia, resta comunque limitata rispetto alla scala reale dei dati su cui vorremmo interrogarlo: manuali tecnici di centinaia di pagine, archivi normativi, intere codebase software, collezioni di contratti o registri di log. Il problema non è soltanto che il modello “non può leggere tutto”, ma che, anche se potesse, non sarebbe necessariamente efficiente o affidabile farlo.
Quando si inserisce un testo estremamente lungo nel prompt, si producono diversi effetti indesiderati. L’attenzione del modello tende a distribuirsi su una massa informativa eccessiva; alcune parti rilevanti possono perdere salienza; la latenza e il costo crescono in modo significativo; il rumore aumenta e, con esso, la probabilità di generare inferenze debolmente supportate. Il nodo centrale diventa allora epistemologico prima che tecnico:
come permettere al modello di ragionare sull’intero documento senza obbligarlo a ingerirlo per intero?
Una prima risposta storica a questo problema è stata la strategia del chunking con retrieval. Il documento viene suddiviso in parti più piccole, indicizzate, e per ogni domanda si recuperano i frammenti ritenuti più rilevanti. Il modello riceve soltanto quei frammenti e produce la risposta. Questo approccio, che è alla base dei sistemi RAG, ha dimostrato grande efficacia pratica. Tuttavia, presenta un limite concettuale: il modello non governa realmente il processo di esplorazione del documento; riceve semplicemente ciò che il sistema di retrieval ha deciso essere rilevante.
il paradigma dei Recursive Language Models

È in questo contesto che emerge il paradigma dei Recursive Language Models, o RLM. Un RLM non è un nuovo tipo di architettura neurale nel senso classico, ma un modo di orchestrare l’inferenza di uno o più modelli linguistici. L’idea centrale consiste nello spostare il documento fuori dal prompt e trasformarlo in un ambiente esterno su cui il modello può operare in modo strategico. Il modello non è più un lettore passivo di testo fornito dall’esterno, ma diventa un agente che decide come interrogare il corpus.
Un RLM è tipicamente composto da almeno due livelli logici. Vi è un modello “root”, responsabile della pianificazione e dell’aggregazione, e uno o più modelli “worker”, incaricati dell’analisi locale dei frammenti selezionati. Il documento viene suddiviso in chunk e indicizzato, ma questa suddivisione è soltanto il primo passo. Quando l’utente pone una domanda, il modello root non produce immediatamente la risposta. Innanzitutto elabora un piano implicito o esplicito: quali parti del documento potrebbero contenere informazioni pertinenti, quali criteri di ricerca utilizzare, se sia necessario filtrare per termini specifici, sezioni, date o strutture sintattiche.
A questo punto entra in gioco il sistema di selezione dei chunk, che può basarsi su metodi statistici come BM25, su similarità semantica tramite embedding, o su combinazioni ibride. La differenza rispetto a un semplice RAG è che nel paradigma RLM la selezione non è l’ultimo passaggio prima della risposta, ma l’inizio di un processo iterativo. I chunk selezionati vengono passati ai worker, che non producono direttamente la risposta finale, bensì estraggono evidenze locali: fatti rilevanti, affermazioni, citazioni, elementi strutturali. Se un chunk non contiene informazioni utili, il worker può segnalarlo esplicitamente.
Il modello root riceve quindi un insieme di evidenze parziali. A questo livello avviene la sintesi globale. Il root integra, confronta, elimina ridondanze, risolve eventuali contraddizioni e costruisce una risposta coerente, spesso mantenendo riferimenti ai chunk di origine. Se le evidenze risultano insufficienti, il sistema può attivare un secondo passaggio: ampliare il numero di chunk considerati, riformulare la query di ricerca, oppure approfondire sezioni adiacenti. Questa capacità di rientrare nel corpus, ridefinire la strategia e iterare l’analisi è ciò che giustifica il termine “recursive”.
La ricorsione non va intesa come una ricorsione matematica nel senso stretto, ma come una struttura di delega e rientro. Il modello root può suddividere il problema in sotto-problemi, delegarli ai worker e poi ricomporre le soluzioni parziali in una soluzione globale. In questo modo, il documento non è più un blocco statico da comprimere nel prompt, ma una memoria esterna interrogabile dinamicamente.
La differenza con un sistema RAG basato su vectorstore diventa allora più chiara. Nel RAG classico, il flusso è lineare: la domanda viene convertita in embedding, si cercano i vettori più simili nel database, si recuperano i chunk corrispondenti e si passa tutto al modello per generare la risposta. Il modello non controlla realmente il processo di retrieval; non decide di esplorare ulteriormente se l’evidenza è insufficiente; non può combinare retrieval semantico con analisi strutturale, filtraggio logico o calcoli intermedi se non attraverso complesse estensioni.
Nel paradigma RLM, invece, il modello diventa il coordinatore dell’esplorazione. Il retrieval non è un semplice preambolo alla generazione, ma una fase integrata in un ciclo di pianificazione, analisi locale e sintesi globale. Inoltre, l’ambiente esterno può contenere non solo testo, ma strumenti computazionali: funzioni di ricerca, parser sintattici, database relazionali, moduli di calcolo. Il modello può decidere quando usare ciascuno di questi strumenti, trasformando l’atto di rispondere in un processo di investigazione guidata.
In termini epistemologici, si passa da un paradigma di “contesto statico” a un paradigma di “contesto dinamico”. Nel primo caso, il modello riceve un insieme di informazioni e produce una risposta. Nel secondo caso, il modello esplora un ambiente, raccoglie evidenze, le organizza e solo infine produce una risposta. Questa distinzione è cruciale quando si opera su documenti di scala molto grande, perché consente di mantenere controllo su costo, latenza e qualità dell’inferenza.
Un RLM, dunque, non elimina la necessità del chunking o del retrieval; li ingloba in una struttura più ampia di orchestrazione. È una strategia per scalare il ragionamento dei modelli linguistici oltre i limiti della loro finestra di contesto, trasformando il documento in una memoria esterna interrogabile in modo ricorsivo. In un mondo in cui i dati crescono più velocemente della capacità di inserirli in un singolo prompt, questa trasformazione da lettore passivo ad agente esplorativo rappresenta un passo concettuale fondamentale.
Esempio
Schema

Codice python
"""
rlm_pdf_openai_menu.py
Mini RLM su PDF con menu interattivo:
- Carica PDF (estrazione testo)
- Chunking + BM25 retrieval
- Worker LLM: estrazione evidenze
- Root LLM: aggregazione risposta con citazioni [chunk=N]
- Opzioni per cambiare parametri (chunk size, top-k, modelli, ecc.)
Install:
pip install openai pypdf
Run:
python rlm_pdf_openai_menu.py
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import List, Dict, Tuple, Optional, Any
import math
import re
from collections import Counter, defaultdict
from pypdf import PdfReader
from openai import OpenAI
# =========================
# 0) CONFIG (imposta qui)
# =========================
OPENAI_API_KEY = "INSERISCI_LA_TUA_API_KEY"
# Modelli (scegli quelli disponibili per il tuo account)
DEFAULT_ROOT_MODEL = "gpt-5.2"
DEFAULT_WORKER_MODEL = "gpt-5-nano"
# =========================
# 1) PDF -> TEXT
# =========================
def pdf_to_text(pdf_path: str) -> str:
reader = PdfReader(pdf_path)
parts: List[str] = []
for i, page in enumerate(reader.pages):
page_text = page.extract_text() or ""
page_text = page_text.strip()
if page_text:
parts.append(f"\n\n--- PAGE {i+1} ---\n{page_text}")
return "\n".join(parts).strip()
# =========================
# 2) CHUNKING
# =========================
@dataclass
class Chunk:
chunk_id: int
start: int
end: int
text: str
def chunk_text(text: str, chunk_chars: int = 2000, overlap: int = 250) -> List[Chunk]:
text = text.strip()
chunks: List[Chunk] = []
i, cid = 0, 0
n = len(text)
while i < n:
j = min(n, i + chunk_chars)
chunks.append(Chunk(chunk_id=cid, start=i, end=j, text=text[i:j]))
cid += 1
i = j - overlap
if i < 0:
i = 0
if i >= n:
break
return chunks
# =========================
# 3) BM25 (pure python)
# =========================
def tokenize(s: str) -> List[str]:
return re.findall(r"[a-zA-Zàèéìòù0-9_]{2,}", s.lower())
class BM25Index:
def __init__(self, chunks: List[Chunk], k1: float = 1.2, b: float = 0.75):
self.chunks = chunks
self.k1 = k1
self.b = b
self.N = len(chunks)
self.doc_freq: Dict[str, int] = defaultdict(int)
self.term_freqs: List[Counter] = []
self.doc_lens: List[int] = []
self.avgdl: float = 0.0
for ch in chunks:
tf = Counter(tokenize(ch.text))
self.term_freqs.append(tf)
dl = sum(tf.values()) or 1
self.doc_lens.append(dl)
for term in tf.keys():
self.doc_freq[term] += 1
self.avgdl = (sum(self.doc_lens) / self.N) if self.N else 0.0
def idf(self, term: str) -> float:
df = self.doc_freq.get(term, 0)
return math.log(1 + (self.N - df + 0.5) / (df + 0.5))
def score(self, query_terms: List[str], idx: int) -> float:
tf = self.term_freqs[idx]
dl = self.doc_lens[idx]
score = 0.0
for t in query_terms:
f = tf.get(t, 0)
if f == 0:
continue
idf = self.idf(t)
denom = f + self.k1 * (1 - self.b + self.b * dl / (self.avgdl or 1))
score += idf * (f * (self.k1 + 1)) / denom
return score
def top_k(self, query: str, k: int = 10) -> List[Tuple[int, float]]:
q_terms = tokenize(query)
scored = [(i, self.score(q_terms, i)) for i in range(self.N)]
scored = [x for x in scored if x[1] > 0]
scored.sort(key=lambda x: x[1], reverse=True)
return scored[:k]
# =========================
# 4) OpenAI wrapper (Responses API)
# =========================
class OpenAITextLLM:
def __init__(self, api_key: str, model: str):
self.client = OpenAI(api_key=api_key)
self.model = model
def respond(self, *, instructions: str, input_text: str, temperature: float = 0.2) -> str:
resp = self.client.responses.create(
model=self.model,
instructions=instructions,
input=input_text,
temperature=temperature,
)
return (resp.output_text or "").strip()
# =========================
# 5) RLM core
# =========================
@dataclass
class RLMConfig:
chunk_chars: int = 2000
overlap: int = 250
retrieve_k: int = 14
select_k: int = 6
worker_max_chars: int = 2400
min_evidence_chunks: int = 2
second_pass_extra: int = 10
temperature_root: float = 0.2
temperature_worker: float = 0.2
class PDF_RLM:
def __init__(self, root: OpenAITextLLM, worker: OpenAITextLLM, cfg: Optional[RLMConfig] = None):
self.root = root
self.worker = worker
self.cfg = cfg or RLMConfig()
self.chunks: List[Chunk] = []
self.bm25: Optional[BM25Index] = None
self.pdf_path: Optional[str] = None
self._cached_text_len: int = 0
def set_models(self, root_model: str, worker_model: str, api_key: str):
self.root = OpenAITextLLM(api_key=api_key, model=root_model)
self.worker = OpenAITextLLM(api_key=api_key, model=worker_model)
def ingest_pdf(self, pdf_path: str) -> None:
text = pdf_to_text(pdf_path)
if not text:
raise ValueError(
"Non ho estratto testo dal PDF. Se è un PDF scansionato serve OCR."
)
self.pdf_path = pdf_path
self._cached_text_len = len(text)
self.chunks = chunk_text(text, self.cfg.chunk_chars, self.cfg.overlap)
self.bm25 = BM25Index(self.chunks)
def _worker_extract(self, question: str, chunk: Chunk) -> str:
instructions = (
"Sei un assistente che estrae EVIDENZE da un chunk di documento.\n"
"Regole:\n"
"- Usa SOLO il testo del chunk.\n"
"- Rispondi in bullet.\n"
"- Se il chunk non contiene nulla di utile, rispondi esattamente: NO_EVIDENCE\n"
"- NON inventare.\n"
)
input_text = (
f"DOMANDA:\n{question}\n\n"
f"CHUNK_META: [chunk={chunk.chunk_id} start={chunk.start} end={chunk.end}]\n"
f"CHUNK:\n{chunk.text[: self.cfg.worker_max_chars]}"
)
return self.worker.respond(
instructions=instructions,
input_text=input_text,
temperature=self.cfg.temperature_worker,
)
def _root_aggregate(self, question: str, evidences: List[Tuple[Chunk, str]]) -> str:
instructions = (
"Sei il modello ROOT che produce la risposta finale usando SOLO le evidenze fornite.\n"
"Regole:\n"
"- Se un punto non è supportato da evidenze, NON dirlo.\n"
"- Mantieni riferimenti [chunk=N] vicino alle frasi supportate.\n"
"- Se mancano informazioni, spiega cosa manca.\n"
)
if not evidences:
evidence_block = "NO_EVIDENCE_FOUND"
else:
evidence_block = "\n\n".join(
f"[chunk={ch.chunk_id}]\n{ev}" for ch, ev in evidences
)
input_text = (
f"DOMANDA:\n{question}\n\n"
f"EVIDENZE:\n{evidence_block}"
)
return self.root.respond(
instructions=instructions,
input_text=input_text,
temperature=self.cfg.temperature_root,
)
def answer(self, question: str) -> Dict[str, Any]:
if not self.bm25:
raise RuntimeError("Prima carica un PDF (opzione menu).")
ranked = self.bm25.top_k(question, k=self.cfg.retrieve_k)
cand_ids = [i for i, _ in ranked][: self.cfg.select_k]
evidences: List[Tuple[Chunk, str]] = []
for idx in cand_ids:
ch = self.chunks[idx]
ev = self._worker_extract(question, ch)
if ev.strip() != "NO_EVIDENCE":
evidences.append((ch, ev.strip()))
used_second_pass = False
second_ids: List[int] = []
if len(evidences) < self.cfg.min_evidence_chunks:
used_second_pass = True
ranked2 = self.bm25.top_k(question, k=self.cfg.retrieve_k + self.cfg.second_pass_extra)
more = [i for i, _ in ranked2 if i not in cand_ids]
second_ids = more[: self.cfg.select_k]
for idx in second_ids:
ch = self.chunks[idx]
ev = self._worker_extract(question, ch)
if ev.strip() != "NO_EVIDENCE":
evidences.append((ch, ev.strip()))
final = self._root_aggregate(question, evidences)
return {
"question": question,
"pdf_path": self.pdf_path,
"text_len": self._cached_text_len,
"total_chunks": len(self.chunks),
"selected_round1": [self.chunks[i].chunk_id for i in cand_ids],
"used_second_pass": used_second_pass,
"selected_round2": [self.chunks[i].chunk_id for i in second_ids],
"evidence_chunks": [ch.chunk_id for ch, _ in evidences],
"final_answer": final,
}
# =========================
# 6) Menu UI
# =========================
def _read_int(prompt: str, default: int, min_v: int = 1, max_v: int = 10_000) -> int:
s = input(f"{prompt} [{default}]: ").strip()
if not s:
return default
try:
v = int(s)
if v < min_v or v > max_v:
print(f"Valore fuori range ({min_v}-{max_v}). Uso default {default}.")
return default
return v
except ValueError:
print(f"Non è un intero. Uso default {default}.")
return default
def _read_float(prompt: str, default: float, min_v: float = 0.0, max_v: float = 2.0) -> float:
s = input(f"{prompt} [{default}]: ").strip()
if not s:
return default
try:
v = float(s)
if v < min_v or v > max_v:
print(f"Valore fuori range ({min_v}-{max_v}). Uso default {default}.")
return default
return v
except ValueError:
print(f"Non è un float. Uso default {default}.")
return default
def print_status(rlm: PDF_RLM, cfg: RLMConfig, root_model: str, worker_model: str):
print("\n=== STATUS ===")
print("PDF:", rlm.pdf_path or "(non caricato)")
if rlm.pdf_path:
print("Testo estratto (caratteri):", rlm._cached_text_len)
print("Chunks:", len(rlm.chunks))
print("Root model:", root_model)
print("Worker model:", worker_model)
print("chunk_chars:", cfg.chunk_chars, "| overlap:", cfg.overlap)
print("retrieve_k:", cfg.retrieve_k, "| select_k:", cfg.select_k)
print("worker_max_chars:", cfg.worker_max_chars)
print("min_evidence_chunks:", cfg.min_evidence_chunks, "| second_pass_extra:", cfg.second_pass_extra)
print("temp_root:", cfg.temperature_root, "| temp_worker:", cfg.temperature_worker)
print("==============\n")
def menu():
if not OPENAI_API_KEY or OPENAI_API_KEY == "INSERISCI_LA_TUA_API_KEY":
print("ATTENZIONE: imposta OPENAI_API_KEY nel codice prima di usare i modelli.")
print("Puoi comunque leggere PDF e configurare, ma le chiamate LLM falliranno.\n")
cfg = RLMConfig()
root_model = DEFAULT_ROOT_MODEL
worker_model = DEFAULT_WORKER_MODEL
root = OpenAITextLLM(api_key=OPENAI_API_KEY, model=root_model)
worker = OpenAITextLLM(api_key=OPENAI_API_KEY, model=worker_model)
rlm = PDF_RLM(root=root, worker=worker, cfg=cfg)
while True:
print("=== MENU RLM PDF ===")
print("1) Carica PDF")
print("2) Fai una domanda al PDF")
print("3) Cambia configurazione (chunk/retrieval)")
print("4) Cambia modelli (root/worker)")
print("5) Mostra status")
print("6) Esci")
choice = input("Scegli un'opzione: ").strip()
if choice == "1":
path = input("Percorso PDF: ").strip()
if not path:
print("Percorso vuoto.\n")
continue
try:
rlm.ingest_pdf(path)
print(f"PDF caricato. Chunks creati: {len(rlm.chunks)}\n")
except Exception as e:
print(f"Errore caricamento PDF: {e}\n")
elif choice == "2":
if not rlm.pdf_path:
print("Prima carica un PDF (opzione 1).\n")
continue
q = input("Domanda: ").strip()
if not q:
print("Domanda vuota.\n")
continue
try:
res = rlm.answer(q)
print("\n--- RISPOSTA ---\n")
print(res["final_answer"])
print("\n--- DEBUG ---")
print("selected_round1:", res["selected_round1"])
print("used_second_pass:", res["used_second_pass"])
print("selected_round2:", res["selected_round2"])
print("evidence_chunks:", res["evidence_chunks"])
print()
except Exception as e:
print(f"Errore durante la domanda: {e}\n")
elif choice == "3":
print("\nConfigura parametri (invio per lasciare invariato):")
cfg.chunk_chars = _read_int("chunk_chars (caratteri per chunk)", cfg.chunk_chars, 500, 20_000)
cfg.overlap = _read_int("overlap (caratteri sovrapposti)", cfg.overlap, 0, cfg.chunk_chars - 1)
cfg.retrieve_k = _read_int("retrieve_k (candidati BM25)", cfg.retrieve_k, 1, 200)
cfg.select_k = _read_int("select_k (chunk analizzati dal worker)", cfg.select_k, 1, 50)
cfg.worker_max_chars = _read_int("worker_max_chars (testo max inviato al worker)", cfg.worker_max_chars, 300, 50_000)
cfg.min_evidence_chunks = _read_int("min_evidence_chunks (soglia per secondo pass)", cfg.min_evidence_chunks, 0, 50)
cfg.second_pass_extra = _read_int("second_pass_extra (candidati extra nel 2° pass)", cfg.second_pass_extra, 0, 500)
cfg.temperature_root = _read_float("temperature_root", cfg.temperature_root, 0.0, 2.0)
cfg.temperature_worker = _read_float("temperature_worker", cfg.temperature_worker, 0.0, 2.0)
# se c'è già un PDF caricato, ricostruisci indice con nuovo chunking
if rlm.pdf_path:
print("\nRicostruisco chunks/indice con nuova configurazione...")
try:
rlm.ingest_pdf(rlm.pdf_path)
print("Ok.\n")
except Exception as e:
print(f"Errore ricostruzione: {e}\n")
else:
print()
elif choice == "4":
print("\nCambia modelli (invio per lasciare invariato):")
rm = input(f"Root model [{root_model}]: ").strip()
wm = input(f"Worker model [{worker_model}]: ").strip()
if rm:
root_model = rm
if wm:
worker_model = wm
try:
rlm.set_models(root_model, worker_model, OPENAI_API_KEY)
print("Modelli aggiornati.\n")
except Exception as e:
print(f"Errore aggiornamento modelli: {e}\n")
elif choice == "5":
print_status(rlm, cfg, root_model, worker_model)
elif choice == "6":
print("Ciao!")
break
else:
print("Opzione non valida.\n")
if __name__ == "__main__":
menu()
Parti del codice e loro funzione
A) CONFIG (variabili globali)
OPENAI_API_KEY: la “password” per chiamare OpenAIDEFAULT_ROOT_MODEL/DEFAULT_WORKER_MODEL: modelli predefiniti
Funzione: definisce i parametri base prima dell’avvio del menu.
B) PDF extraction
pdf_to_text(pdf_path)
- Legge il PDF con
pypdf - Estrae testo per pagina
- Compone un’unica stringa con marker
--- PAGE N ---
Funzione: trasforma un file PDF in testo lavorabile dal sistema.
C) Chunking
Chunk (dataclass)
- contiene
chunk_id,start,end,text
Funzione: rappresenta un pezzo del documento, citabile e tracciabile.
chunk_text(text, chunk_chars, overlap)
- divide il testo in blocchi di dimensione fissa
- sovrappone i blocchi (overlap) per non tagliare concetti
Funzione: crea “unità di contesto” compatibili con l’analisi LLM.
D) Retrieval BM25
tokenize(s)
- normalizza e spezza in parole (semplice tokenizer)
Funzione: prepara testo e query per l’indice BM25.
BM25Index
- in
__init__costruisce:- frequenze dei termini per chunk
- frequenze globali
- lunghezze chunk
top_k(query, k)restituisce i chunk con miglior punteggio
Funzione: scegliere i chunk “più promettenti” senza usare embeddings.
E) Adapter OpenAI
OpenAITextLLM
- incapsula la chiamata
client.responses.create(...) - espone
respond(instructions, input_text, temperature)
Funzione: separa l’API (OpenAI) dal resto del programma, così puoi cambiare provider/modello senza rifare l’RLM.
F) Config runtime (RLMConfig)
RLMConfig
Contiene parametri controllabili dal menu:
- chunking:
chunk_chars,overlap - retrieval:
retrieve_k,select_k - worker:
worker_max_chars - qualità:
min_evidence_chunks - ricerca estesa:
second_pass_extra - temperature root/worker
Funzione: rende l’RLM tarabile senza cambiare codice.
G) Orchestratore (PDF_RLM)
Questa è la parte che “fa RLM”.
Stato interno
chunks: memoria esterna (testo spezzato)bm25: indice per selezione chunkpdf_path: quale documento è caricato_cached_text_len: per debug/status
Metodi principali
1) ingest_pdf(pdf_path)
- estrae testo
- chunkizza
- indicizza BM25
Funzione: prepara l’ambiente esterno (documento) su cui fare query.
2) _worker_extract(question, chunk)
- chiama il worker LLM
- obiettivo: estrarre evidenze o “NO_EVIDENCE”
Funzione: analisi locale del chunk, produce materiale utile all’aggregazione.
3) _root_aggregate(question, evidences)
- chiama il root LLM
- usa solo evidenze
- produce risposta con citazioni
[chunk=N]
Funzione: sintesi globale e risposta finale.
4) answer(question)
È la pipeline:
- retrieval round 1:
bm25.top_k(question, retrieve_k) - selezione: primi
select_k - worker su quei chunk → evidenze
- se evidenze <
min_evidence_chunks:- round 2: allarga candidati (
second_pass_extra) - worker su altri chunk
- round 2: allarga candidati (
- root aggrega
Funzione: implementa il principio RLM (selezione → evidenze → aggregazione + passaggio iterativo).
H) Menu CLI (interfaccia)
menu()
Fa da “controller” per usare il sistema:
- Carica PDF →
rlm.ingest_pdf() - Fai domanda →
rlm.answer() - Cambia configurazione → modifica
cfge ricostruisce indice se PDF presente - Cambia modelli →
rlm.set_models() - Status →
print_status() - Esci
Funzione: rende il programma utilizzabile senza modificare codice.
Flusso “narrativo” (cosa succede quando fai una domanda)
Quando selezioni “Fai una domanda”:
- il testo del PDF non viene inviato al modello per intero
- BM25 seleziona pochi chunk
- il worker estrae evidenze
- se poche evidenze, il sistema allarga la ricerca (second pass)
- il root produce risposta finale citando i chunk