Recupero
Il canale RAG narrativo — chunking a granularità di paragrafo, Okapi BM25 consapevole del CJK, un canale vettoriale iniettabile, fusione RRF, reranking listwise via LLM e l'adapter che rimuove i contenuti RESTRICTED prima che possano raggiungere un prompt.
Il dominio retrieval (src/ragspine/retrieval/) è il canale RAG narrativo di RAGSpine —
la metà che risponde alle domande "perché / cosa è successo" recuperando chunk di documenti,
fondendo segnali lessicali e (opzionalmente) vettoriali, effettuando il reranking e consegnando snippet citati
all'agent. È la controparte del canale strutturato deterministico; vedi
Doppio canale per come l'agent instrada tra i due.
Due proprietà sono qui non negoziabili e imposte a livello di codice:
- Ibrido semantico per impostazione predefinita, BM25 puro come fallback a zero dipendenze. Il canale vettoriale è
iniettabile. Con l'extra
[embed-onnx]installato, il recupero è genuinamente ibrido (BM25 + semantico ONNX → RRF) senza alcuna configurazione (RAGSPINE_EMBEDDING=auto); senza alcun backend di embedding collegato è puro Okapi BM25 + RRF — completamente offline, deterministico, zero SDK. - Isolamento RESTRICTED a due uscite. I contenuti con sensibilità
RESTRICTEDvengono rimossi in entrambe le uscitererank/elink/prima che possano raggiungere un prompt. Vedi Isolamento RESTRICTED.
Struttura
La pipeline si legge da sinistra a destra: chunking produce e versiona i chunk → lexical
(con vector opzionale) li valuta e li fonde → rerank riordina i candidati migliori →
link adatta il risultato per l'agent ed elimina i RESTRICTED.
Preset di prodotto correnti per il recupero
RAGSPINE_RETRIEVAL_MODE=auto mantiene il comportamento ibrido configurato. Gli alias hybrid e
vector consentono anch'essi il percorso embedding/vettoriale. economy, bm25 e lexical sono un preset esplicito
a zero embedding: l'assemblaggio del servizio non costruisce un backend di embedding né uno store vettoriale.
I predicati sui metadati supportano eq, ne, in, nin, gt, gte, lt, lte e between.
Sono operazioni di restringimento deterministiche che preservano l'ordine. L'estrazione automatica dei filtri è una
fase opzionale separata; un'estrazione fallita o assente non allarga mai silenziosamente oltre l'insieme di candidati
non filtrato.
Per librerie multiple, MultiIndexRetriever chiede a un router gli ID delle librerie, esegue ciascun
indice selezionato in modo indipendente e fonde le liste ordinate con RRF. I risultati portano con sé la provenienza library_id.
Se il routing fallisce, il fallback di disponibilità sicuro cerca in ogni libreria configurata.
chunking — chunker a granularità di paragrafo + store versionato
chunking/chunking.py trasforma il testo semplice di un documento in chunk per il recupero. Il budget
di token è approssimato dal conteggio dei caratteri (nessun tokenizzatore di terze parti), mantenendolo
offline e deterministico.
Proprietà
Tipo
Costanti: DEFAULT_CHUNK_CHARS = 480, DEFAULT_OVERLAP_CHARS = 80. I singoli paragrafi
sovradimensionati vengono suddivisi in base ai terminatori di frase (。!?;.!?;), poi tagliati forzatamente, senza sovrapposizione
tra i sotto-chunk — così il text di un chunk rimane sempre una sottostringa contigua del
sorgente, il che mantiene oneste le citazioni (vedi Provenienza).
chunk_document solleva ValueError se max_chars <= 0, overlap_chars < 0 o
overlap_chars >= max_chars.
chunking/chunk_store.py è lo store di chunk versionato (SQLite, che rispecchia lo store
dei fatti: schema esplicito, SQL parametrizzato, un punto di ingresso execute_read di sola lettura).
StoredChunk— ogni campo diChunk, inclusi i campi parent/window, più i metadati di ingestione:valid_as_of,ingested_at,version(default1),active(defaultTrue). Gli schemi SQLite più vecchi vengono migrati in modo additivo.ChunkStore(db_path)—init_schema()crea la tabellanarrative_chunked è idempotente.replace_doc_chunks(doc_id, chunks, valid_as_of="") -> inteffettua una sostituzione versionata: incrementaversion = max(version) + 1, marca le righe vecchie comeactive=0, inserisce i nuovi chunkactive=1e restituisce il numero di righe scritte. La re-ingestione è idempotente; passare una lista vuota ritira il documento dall'insieme attivo.iter_chunks(*, doc_id=None, topic=None, entity=None, geography=None, period=None, language=None, include_inactive=False) -> list[StoredChunk]— un pre-filtro sui metadati combinato in AND (per impostazione predefinita solo su elementi attivi), usato per restringere i candidati prima di qualsiasi valutazione.
lexical — Okapi BM25 (CJK uni+bigrammi) + fusione RRF
lexical/retrieval.py è il nucleo di scoring. È tutto Python puro — nessun rank-bm25, nessun
SDK.
tokenize(text) -> list[str]— converte in minuscolo; le sequenze alfanumeriche ASCII diventano parole; le sequenze CJK vengono emesse sia come unigrammi sia come bigrammi adiacenti. Questa doppia granularità è ciò che fa funzionare BM25 per il cinese senza un segmentatore.bm25_scores(query_tokens, docs_tokens, k1=1.5, b=0.75) -> list[float]— Okapi BM25 standard (DEFAULT_BM25_K1 = 1.5,DEFAULT_BM25_B = 0.75).rrf_fuse(rankings, k=60) -> dict[str, float]— Reciprocal Rank Fusion,score += 1.0 / (k + rank)con rango a partire da 1. La costante èDEFAULT_RRF_K = 60(il valore RRF standard).GlossaryQueryRewriter(max_queries=5)— un riscrittore multi-query deterministico basato su regole che espande una query usando i sinonimi di entità/metrica del glossario (zero LLM). La query originale è sempre prima.
Questi si compongono nelle classi di recupero:
Proprietà
Tipo
HybridRetriever.search(...) applica il pre-filtro sui metadati prima di qualsiasi valutazione o
embedding, calcola i vettori dei chunk in modo lazy (in cache per chunk_id) solo per i candidati
sopravvissuti e risolve i pareggi in modo deterministico tramite (-fused_score, chunk_id).
HybridRetriever espone anche .topology() -> PipelineGraph, un sottile delegato verso
l'esportatore di topologia della pipeline — così puoi renderizzare il cablaggio effettivo
di un retriever configurato come Mermaid / DOT / JSON.
vector — backend di embedding iniettabili (default: nessuno)
Il canale vettoriale è un punto di estensione, non un default. Il Protocol EmbeddingBackend
(definito in lexical/retrieval.py) ha un unico metodo:
class EmbeddingBackend(Protocol):
def embed_texts(self, texts: list[str]) -> list[list[float]]: ...Si inietta un'implementazione tramite il parametro keyword embedding_backend= su HybridRetriever,
NarrativeIndex e build_narrative_retriever. L'argomento predefinito a livello di libreria è
None — il canale vettoriale è disattivato e il recupero è puro BM25 + RRF. A livello di servizio,
però, RAGSPINE_EMBEDDING ora ha come default auto: il backend semantico ONNX quando
[embed-onnx] è importabile, altrimenti None — così un'installazione predefinita con l'extra è genuinamente
ibrida fin da subito, mentre un'installazione minimale rimane BM25 puro identico byte per byte.
vector/embedding_backends.py include tre backend concreti più una factory:
OnnxEmbeddingBackend
Il default semantico raccomandato (dietro [embed-onnx], via fastembed). Modello paraphrase-multilingual-MiniLM-L12-v2 (384 dimensioni, multilingue — cross-linguale ZH/EN), offline e deterministico. Selezionato da onnx / auto; scarica i pesi al primo utilizzo, poi opera offline.
DeterministicEmbeddingBackend
Backend offline a hash lessicale (bucketing dei token blake2b + normalizzazione L2). Zero rete/SDK. La sua docstring lo segnala come non semantico — altamente correlato con BM25, nessun vero guadagno di recall semantico.
SentenceTransformerEmbeddingBackend
Modello predefinito Qwen/Qwen3-Embedding-0.6B; device rilevato automaticamente (cuda → mps → cpu, sovrascrivibile via RAGSPINE_EMBEDDING_DEVICE). Il modello viene caricato in modo lazy al primo embed.
OpenAIEmbeddingBackend
Modello predefinito text-embedding-3-large; `import openai` lazy; incapsula gli errori dell'SDK come ProviderError.
from ragspine.retrieval.vector.embedding_backends import make_embedding_backend
# spec (case-insensitive; defaults to env RAGSPINE_EMBEDDING_BACKEND):
# None / "none" → None (pure BM25 + RRF, the zero-dep fallback)
# "auto" → OnnxEmbeddingBackend if [embed-onnx] importable, else None
# "onnx" / "fastembed" / "minilm" → OnnxEmbeddingBackend (semantic, offline, deterministic)
# "deterministic" → DeterministicEmbeddingBackend
# "openai" → OpenAIEmbeddingBackend
# "qwen3" / "st" / "sentence-transformers" → SentenceTransformerEmbeddingBackend
backend = make_embedding_backend("onnx")vector/store.py fornisce inoltre un Protocol VectorStore pluggabile
(upsert / query / delete / count) con un InProcessVectorStore a zero dipendenze
(coseno brute-force, risoluzione dei pareggi per id crescente). Nota che il suo query rispetta un filtro where ma
non elimina automaticamente i RESTRICTED — quella rimozione resta alle due uscite autoritative qui sotto.
rerank — reranker listwise via LLM (fallback RRF)
rerank/listwise_rerank.py riordina i candidati migliori con un giudice LLM, dietro il
Protocol ListwiseJudge:
class ListwiseJudge(Protocol):
def judge(self, query: str, candidates: list[str]) -> list[int]: ...Il punto di ingresso è listwise_rerank(query, results, judge, *, top_n=10) (DEFAULT_TOP_N = 10). Contano due comportamenti:
- Uscita RESTRICTED n. 1. I candidati il cui
chunk.sensitivityè uguale (senza distinzione tra maiuscole e minuscole) a"RESTRICTED"sono esclusi dall'insieme inviato al giudice e mantenuti nelle loro posizioni RRF originali — il testo RESTRICTED non raggiunge mai il prompt del giudice. Se ogni candidato è RESTRICTED, il giudice non viene mai chiamato. - Fallback RRF. In caso di qualsiasi eccezione del giudice o output malformato, il sottoinsieme aperto degrada
all'ordine identità (RRF).
listwise_reranknon solleva mai eccezioni.
Le funzioni pure di supporto — build_listwise_prompt(query, candidates) e
parse_listwise_response(text, n_candidates) (parsing robusto in una permutazione di lunghezza n,
con fallback all'identità) — rendono il reranking deterministico e testabile senza un modello reale.
link — adapter verso l'agent (rimuove i RESTRICTED all'uscita)
link/narrative_link.py è la cucitura tra questo dominio (la "linea B" del recupero) e
l'agent (la "linea A"). Adatta un NarrativeIndex al contratto
NarrativeRetriever dell'agent (che è definito sul lato agent, in agent/agent.py).
-
NarrativeIndexRetriever(index, *, retry_without_filters=True)— il suoretrieve(query, *, filters=None, top_k=50) -> list[dict]mappafiltersin kwargs di metadati, chiama l'indice sottostante, riprova una volta senza filtri se il risultato filtrato è vuoto e restituisce dict di snippet.Uscita RESTRICTED n. 2. Il valore di ritorno è costruito come una comprehension che elimina ogni chunk la cui sensibilità è uguale a
"RESTRICTED"prima che venga prodotto qualsiasi dict di snippet:return [ _to_snippet(r) for r in results if str(r.chunk.sensitivity).upper() != RESTRICTED_SENSITIVITY ]Così il testo RESTRICTED non raggiunge mai il prompt di sintesi dell'LLM — la stessa costante (
RESTRICTED_SENSITIVITY = "RESTRICTED") protegge entrambe le uscite. -
ProviderListwiseJudge(provider)— unListwiseJudgeconcreto basato sulLLMProviderdell'agent. Costruisce il prompt, effettua una singola chiamataprovider.chat(...)e analizza la risposta; gli errori del provider si propagano e vengono intercettati dalla degradazione dilistwise_rerank. -
build_narrative_retriever(chunk_db, provider=None, *, embedding_backend=None) -> tuple[NarrativeIndexRetriever, ChunkStore]— il punto di ingresso per il cablaggio CLI/servizio. Apre lo store di chunk, chiamainit_schemae assembla la catena predefinita: BM25 puro + RRF (nessun backend vettoriale per impostazione predefinita) + multi-queryGlossaryQueryRewriter+ (quando viene fornitoprovider) un rerankingProviderListwiseJudge. La chiusura dello store spetta al chiamante.
Un dict di snippet porta con sé la provenienza completa: text, doc_id, title, source_locator,
chunk_id, i campi dei metadati, sensitivity e un dict scores annidato
({"bm25", "vector", "fused"}).
Come collegarlo
from ragspine.retrieval.link.narrative_link import build_narrative_retriever
# Default: pure BM25 + RRF + glossary multi-query + (with a provider) listwise rerank.
retriever, store = build_narrative_retriever("data/chunks.db")
try:
snippets = retriever.retrieve("为什么营收下滑", filters={"entity": "ACME_CN"}, top_k=10)
# snippets is RESTRICTED-free and carries full lineage per item
finally:
store.close()Entrambe le uscite RESTRICTED (rerank/ e link/) devono rimanere. Sono la metà imposta a livello di codice
dell'invariante di isolamento RESTRICTED; rimuoverne una qualsiasi
lascerebbe che contenuti riservati raggiungano un prompt.
Lo stack di capacità opt-in (0.7.0+)
Tutto quanto sopra è il loop predefinito: offline, deterministico, BM25 + RRF (denso attivato
automaticamente quando [embed-onnx] è presente). Le release 0.7.0 e 0.8.0 aggiungono un ampio insieme di
tecniche RAG mainstream come layer opt-in sulle cuciture Protocol esistenti. Ciascuno è
disattivato per impostazione predefinita e identico byte per byte quando non selezionato, scelto da una factory make_* o dalla
corrispondente variabile d'ambiente RAGSPINE_*, e ciascuno eredita l'isolamento RESTRICTED a due uscite
e gli invarianti di provenienza — un nuovo layer non indebolisce mai la prevenzione delle risposte inventate. Si raggruppano
in base alla fase su cui agiscono.
Il default resta deterministico e offline; selezionare un layer è un opt-in deliberato. I numeri restano sempre nel canale strutturato — ogni layer qui sotto modella solo il recupero narrativo. L'elenco per release è nel Changelog.
Indicizzazione e chunking
- Contextual Retrieval (
RAGSPINE_CONTEXTUAL/make_index_text_fn) — antepone un header di contesto deterministico (title · entity · period · heading, vocabolario controllato, zero contenuti inventati) al solo testo di indice/embedding.chunk.text,source_locatore le citazioni restano identici byte per byte. - Layout-aware e parent-child (
RAGSPINE_CHUNKER=layout|parent_child) — suddivide sui confini strutturali invece che su un budget fisso di caratteri. I figli portanoparent_id,heading,window_texteparent_locator; lo store li persiste e il recupero espande il figlio selezionato inprompt_textseparati destinati alla sola generazione. - Sentence-window e semantico (
RAGSPINE_CHUNKER=sentence_window|semantic) — un chunk per frase con una finestra al momento della sintesi, oppure suddivisioni sui confini degli embedding (il semantico usa[embed-onnx]). - Preset di dominio — laws / qa / book (
RAGSPINE_CHUNKER=laws|qa|book) — sottili chunker layout-aware che cambiano solo il rilevamento delle intestazioni per una famiglia di documenti ciascuno: laws inizia una sezione a ogni clausola (第N条/款/项), book a ogni capitolo (第N章/节/篇, o un'intestazione markdown / numerata), e qa accoppia ogni domanda (Q:/问:/ una riga che termina con?) con i paragrafi di risposta che seguono sotto unparent_idcondiviso. Tutto il resto — il budget,parent_ide i locator — è ereditato dal chunker di base.
L'espansione parent/window non cambia mai l'identità della citazione. text, chunk_id e source_locator
rimangono il figlio corrispondente; il contesto espanso usa prompt_text. Un figlio RESTRICTED viene eliminato
prima di _to_snippet, insieme alla sua finestra, così un parent dall'aspetto sicuro non può reintrodurre
testo riservato.
- Albero multi-granularità RAPTOR (
make_raptor_retriever/RAGSPINE_RAPTOR*) — clustering a soglia deterministico ricorsivo; i riassuntiis_synthesisper cluster portano l'unione della provenienza dei loro membri e non sono mai fatti citabili. Il recupero può estrarre una foglia (dettaglio) o un nodo interno (tema).
Rappresentazione e reranking
- Default denso semantico (
RAGSPINE_EMBEDDING=onnx|auto,[embed-onnx]) — ilOnnxEmbeddingBackenddi cui sopra;autosi risolve in ONNX quando importabile, altrimenti in BM25 puro, così il loop fornito diventa genuinamente ibrido BM25 + denso → RRF senza alcuna configurazione. - Reranking cross-encoder locale (
RAGSPINE_RERANKER=cross_encoder|ce|auto,[rerank]) — il cervello di reranking offline (CrossEncoderReranker, ms-marco MiniLM); quando selezionato ha la precedenza sul giudice listwise LLM. - Late-interaction ColBERT (
RAGSPINE_RERANKER=colbert,[colbert]) — scoring MaxSim multi-vettore a livello di token, fornito come reranker. - Learned-sparse SPLADE (
RAGSPINE_RERANKER=splade,[splade]) — scoring neurale sparso a espansione di termini (interpretabile come BM25, più forte), fornito come reranker.
I reranker cross-encoder, ColBERT e SPLADE girano tutti dentro listwise_rerank, quindi
ereditano gratuitamente l'uscita RESTRICTED n. 1 — un candidato RESTRICTED non raggiunge mai il reranker.
ColBERT / SPLADE sono forniti come reranker; i loro backend di recupero multi-vettore / sparsi (indici)
sono un onesto sviluppo futuro.
Trasformazione delle query
- Decomposizione LLM (
RAGSPINE_QUERY_DECOMPOSE=llm) — fan-out multi-sotto-domanda; ogni sotto-domanda riesegue l'intera pipeline protetta e le sue risposte vengono fuse in modo deterministico. - HyDE · RAG-Fusion · step-back (
RAGSPINE_QUERY_TRANSFORM=hyde|rag_fusion|step_back) — trasformazioni di query via LLM sopra il retriever di base. RAG-Fusion riutilizzarrf_fuse; il documento ipotetico di HyDE è una sonda di recupero, mai un fatto citabile. - Adaptive-RAG (
RAGSPINE_ADAPTIVE) — un classificatore di complessità a euristica deterministica (o LLM opt-in) instrada tra il percorso single-shot e la decomposizione. - Recupero correttivo / CRAG (
RAGSPINE_CORRECTIVE) — promuove il solitario fallbackretry_without_filtersa un loop deterministico grade→act limitato (≤2) (drop-filters → rewrite → refuse); rifiutare un contesto debole è la scelta sicura per la prevenzione delle risposte inventate.
Ogni variante generata riesegue il gate di sicurezza deterministico prima del recupero — una sotto-query su un concorrente viene rifiutata, così i numeri interni non trapelano mai.
Post-recupero
- MMR · lost-in-the-middle · compressione (
RAGSPINE_POSTPROCESSOR, es."mmr,lost_in_middle") — una catenaNodePostprocessordeterministica che gira dopo il reranking e prima dell'assemblaggio del prompt: de-duplicazione per diversità MMR, riordinamento lost-in-the-middle (i risultati migliori in testa e in coda) e compressione estrattiva del contesto. La compressione scrive una chiaveprompt_textseparata che l'agent preferisce per il prompt, lasciandotexte ogni campo di riferimento identici byte per byte. Il compressore LLMLingua-2 / LLM è uno sviluppo futuro sulla cucitura.
Grafo e multi-hop
- Grafo di relazioni strutturato (dominio
graph/) — un grafo tipizzato deterministico sulle dimensioni controllate per roll-up delle controllate, confronto tra pari e tracciamento delle derivazioni (interamente citato), più un ProtocolGraphStore(RAGSPINE_GRAPH_STORE, default in-process + adapter[graph]networkx) e uno scheletro opt-in di estrazione / community GraphRAG narrativo (dietro[graph]+[llm]). È una superficie multi-hop autonoma, non un ramo del router — vedi Canali. - Slot relation-extractor (
RAGSPINE_RELATION_EXTRACTOR,make_relation_extractor) — uno slot opt-in accanto abuild_relation_graphper le relazioni che vivono solo nel testo narrativo. Il default (None) lascia il grafo di base identico byte per byte; l'estrattore deterministico per co-occorrenza aggiunge archico_occurs_witha lignaggio pulito; l'estrattore LLM (dietro[llm]) timbra ogni arcomodel-derived+unverified, filtra entrambi gli estremi attraverso ilSecurityGatee non lascia mai che il testo RESTRICTED raggiunga il modello. Vedi ADR 0015.
Multimodale
- Recupero di documenti visuali ColPali (
RAGSPINE_VISUAL_EMBEDDER=colpali,[colpali]) — late interaction page-as-image (MaxSim sui patch visivi, riutilizzando lo scorer ColBERT), per report densi di grafici e figure dove OCR→testo perde il layout. Opt-in e GPU; un hit visuale è un indizio con riferimento alla pagina (is_visual), mai un fatto citabile, e le pagine RESTRICTED vengono eliminate alla costruzione dell'indice. L'end-to-end su GPU reale è uno sviluppo futuro (colqwen2è l'alternativa di modello più permissiva).
L'opt-in è uniforme — inietta il risultato di una factory, oppure imposta la variabile d'ambiente e lascia che il servizio la colleghi:
from ragspine.retrieval.rerank.cross_encoder import make_reranker
from ragspine.retrieval.postprocess import make_postprocessor
from ragspine.retrieval.link.narrative_link import build_narrative_retriever
# Offline cross-encoder rerank + an MMR / lost-in-the-middle post-chain.
retriever, store = build_narrative_retriever(
"data/chunks.db",
reranker=make_reranker("cross_encoder"), # or RAGSPINE_RERANKER=cross_encoder
postprocessor=make_postprocessor("mmr,lost_in_middle"), # or RAGSPINE_POSTPROCESSOR="mmr,lost_in_middle"
)Vedi anche
Doppio canale
Come l'agent instrada tra il canale strutturato e quello narrativo.
Isolamento RESTRICTED
L'invariante di filtraggio a due uscite che questo dominio impone.
Agente
L'orchestratore che consuma un NarrativeRetriever e sintetizza risposte citate.
Punti di estensione
EmbeddingBackend, ListwiseJudge, NarrativeRetriever e gli altri Protocol.
Archiviazione
Il livello di persistenza sqlite — un archivio di fatti numerici e un archivio di chunk narrativi, entrambi con lineage completo delle fonti. La dataclass Fact, la chiave di upsert dim_key e letture deterministiche trovato/non-trovato.
Agent
Il livello di orchestrazione — parsing dell'intento a quattro slot, il gateway di chiarimento, un gate di sicurezza deterministico, l'instradamento a tre percorsi, il ciclo tool-use, la giunzione del provider LLM e la protezione di prevenzione delle risposte inventate per singolo percorso.