RAGSpine
Concetti

Doppio canale

Un canale strutturato/numerico deterministico e un canale RAG narrativo, unificati da un unico agente router che scompone le domande composite ed esegue entrambi.

RAGSpine risponde a due tipi di domanda fondamentalmente diversi con due meccanismi fondamentalmente diversi — e un singolo agente decide di quale (o di entrambi) una domanda ha bisogno.

  • "Qual è il numero?" → il canale strutturato: una tabella dei fatti più function-calling. Deterministico, nessuna sintesi del valore.
  • "Perché / cosa è successo?" → il canale narrativo: recupero ibrido sui chunk dei documenti, reranking listwise opzionale, poi sintesi LLM con citazioni.

Il router risiede in agent/agent.py (answer_question); il parsing dell'intento e il gate di chiarimento risiedono in agent/intent.py.

I due canali non si fondono mai in un unico percorso "chiedi al modello". Il canale strutturato è l'unica cosa autorizzata a produrre un fatto numerico, e lo fa senza fidarsi della prosa del modello — vedi Prevenzione delle risposte inventate.

Come instrada l'agente

Ogni domanda viene prima analizzata in quattro slot di intento da un parser a regole guidato dalla configurazione e senza LLM (RuleIntentParser, sostituibile dietro il Protocol IntentParser):

Proprietà

Tipo

L'instradamento viene deciso a partire dagli slot analizzati e da indizi lessicali (parse_intent):

strutturato

Un intento numerico — è stato riconosciuto un metric, oppure un indizio numerico come “多少” / “what is”. La risposta è il numero, proveniente dal fact store.

narrativo

Un intento di attribuzione / regolamentazione / tendenza / “perché”. La risposta è sintetizzata dagli snippet recuperati, ciascuno citato.

composito

Entrambi allo stesso tempo — una metrica riconosciuta e un indizio narrativo (es. “perché il fatturato è calato?”). L'agente esegue il percorso strutturato, poi aggiunge una sezione di attribuzione.

Prima dell'instradamento, un gateway di chiarimento (clarify_scope) applica un'asimmetria deliberata:

  • Metrica mancante → chiedi prima (indovinare la metrica sarebbe un errore sostanziale).
  • Entità / periodo mancanti → rispondi esplicitando le assunzioni (per impostazione predefinita il gruppo di riferimento / l'ultimo esercizio fiscale completo, esplicita l'assunzione, offri un restringimento con un clic).
  • Entità fuori ambito / concorrente → rifiuta prima che qualsiasi canale venga eseguito (vedi Isolamento RESTRICTED e il gate di sicurezza deterministico).

Il canale strutturato: uno stato tripartito found / not_found / unrecognized

L'unica primitiva del canale strutturato che produce fatti è lo strumento query_metric (agent/query_tools.py). La sua funzione di esecuzione normalizza ogni parametro tramite il glossario, poi interroga lo store fact_metric. Restituisce uno di tre stati — mai una supposizione:

Il valore esatto esiste. Restituisce il valore, l'unità, tutti i codici di dimensione controllati e la lineage completa:

{
  "status": "found",
  "value": 1320,
  "unit": "USD_M",
  "metric_code": "REVENUE",
  "entity": "ACME_CN",
  "period_type": "FY",
  "period": "2024",
  "channel": "TOTAL",
  "source": { "doc": "ACME_FY2024_Review.pptx", "locator": "slide=2,table=1,row=REVENUE,col=FY2024" }
}

Ogni parametro è stato normalizzato, ma nessuna riga corrispondente nella tabella dei fatti. Nessuna interpolazione, nessuna inferenza — l'agente riscrive questo esito come un rifiuto onesto.

{
  "status": "not_found",
  "normalized": { "metric_code": "REVENUE", "entity": "ACME_CN", "period": "2025", "channel": "TOTAL" }
}

Un parametro non è stato normalizzabile in un codice controllato (il glossario restituisce None invece di indovinare). Il parametro incriminato e il suo valore grezzo vengono indicati.

{ "status": "unrecognized_param", "param": "entity", "raw": "some unknown company" }

I normalizzatori del glossario (normalize_metric / normalize_entity / normalize_period) restituiscono None per qualsiasi cosa che non riconoscono. Quel None diventa unrecognized_param — non viene mai forzato in un codice ipotizzato come "migliore approssimazione".

Il canale narrativo

Quando la rotta è narrative, l'agente chiama un NarrativeRetriever iniettato (_run_narrative). La catena predefinita (retrieval/) è:

Recupero ibrido — Okapi BM25 con supporto CJK + un canale vettoriale iniettabile (predefinito: nessuno = puro BM25), fusi con Reciprocal Rank Fusion (rrf_fuse, k=60), più riscrittura multi-query guidata dal glossario.

Reranking listwise — un giudice listwise LLM opzionale (listwise_rerank) riordina i migliori candidati; in caso di qualsiasi errore ripiega sull'ordine RRF.

Sintesi con citazioni — l'LLM risponde solo a partire dagli snippet forniti, e l'agente aggiunge forzatamente il nome di qualsiasi documento sorgente che il modello ha omesso di citare.

Se nessun retriever è collegato, o il recupero non restituisce nulla, il canale narrativo degrada onestamente ("non recuperato / non ancora collegato") invece di inventare una risposta.

Il percorso composito: esegui entrambi, confronta, unisci

Per una domanda composita, l'agente esegue prima il percorso strutturato, poi esegue il percorso narrativo e unisce i risultati:

<structured answer, with the number(s) + lineage>

归因分析:
<narrative answer, synthesized from cited snippets>

Le fonti di entrambi i canali vengono concatenate. Quando il lato strutturato si espande in più sotto-task (più metriche / entità / periodi elencati esplicitamente dall'utente), l'agente esegue ogni sotto-task query_metric deterministicamente senza l'LLM (_multi_subtask_answer) e, per esattamente due periodi comparabili, calcola la differenza da sé.

La prevenzione delle risposte inventate viene applicata per percorso, non in modo unificato: il percorso strutturato sintetizza deterministicamente la risposta dai valori dei fatti, il percorso multi-sotto-task non chiama mai l'LLM, e il percorso narrativo si fida della prosa del modello ma impone la citazione. Questa asimmetria è deliberata.

In questa pagina