Citation grounding: implementare pipeline cu 4 porți
Pipeline practic de citation grounding pentru RAG juridic și de procurement: retrieve, answer cu citări, validate, return. Cu pseudocode complet.
Citation grounding implementat practic: pipeline cu 4 porți pentru RAG juridic
Diferența dintre un RAG demo și un RAG production-ready pentru sectoare reglementate este o singură capabilitate: garanția că fiecare afirmație factuală din răspuns este ancorată într-un fragment textual identic dintr-un document sursă verificabil. Această capabilitate se numește citation grounding.
Acest articol descrie pipeline-ul practic cu 4 porți pe care îl folosim în Leta și în implementări similare pentru clienți. Include pseudocode, edge cases, și măsurători reale pe un dataset de 1.200 întrebări juridice românești.
TL;DR
- Citation grounding nu este un add-on. Este o regândire a întregului pipeline RAG: retrieve devine retrieve+anchor, generate devine generate+cite, return devine validate+return.
- Pipeline-ul cu 4 porți: (1) retrieve cu metadata, (2) generate cu format de citare obligatoriu, (3) validate fiecare claim cu verbatim match, (4) return cu surse atașate sau refuz controlat.
- Validarea trebuie să fie textuală, nu semantică. Verificați că textul citat există efectiv în documentul sursă, nu „pare similar”.
- Cost: 30–50% latency suplimentar și 2–3× tokens consumate per query. Beneficiu: rate de halucinare sub 0,5% pe domeniu juridic.
- Edge cases critice: queries fără răspuns în corpus, paragrafe parafrazate vs citate textual, claims agregate care încalcă mai multe surse.
De ce textuală, nu semantică
O greșeală comună: implementatorii folosesc un al doilea LLM pentru a verifica dacă citatul „seamănă” cu sursa. Acea validare este semantică. Funcționează în 95% din cazuri, dar eșuează exact pe cazurile critice: când modelul fabrică un citat care „arată” ca textul oficial dar conține modificări subtile (înlocuirea unui număr, schimbarea unui cuvânt cheie).
Validarea textuală — căutarea exactă a citatului în documentul sursă — elimină această clasă de eroare. Cere mai multă disciplină în prompt (modelul trebuie să citeze verbatim, nu să parafrazeze), dar oferă o garanție pe care validarea semantică nu o poate da.
Arhitectura cu 4 porți
Query
│
▼
[Poartă 1: Retrieve cu metadata]
→ top-k fragmente + document_id + offset_start + offset_end
│
▼
[Poartă 2: Generate cu format strict de citare]
→ răspuns + lista (claim_id, document_id, citat_textual, offset_estimate)
│
▼
[Poartă 3: Validate textual]
→ pentru fiecare claim, verifică citat in document
→ dacă fail: regenerate cu prompt mai strict (max 3 încercări)
│
▼
[Poartă 4: Return]
→ răspuns cu citări active (linkuri la fragmente exacte) SAU
→ refuz controlat ("nu am sursă suficientă pentru ...")
Poarta 1 — Retrieve cu metadata
Spre deosebire de un RAG simplu care returnează doar text și similarity score, în acest pipeline fiecare fragment trebuie să poarte metadata necesare pentru validare:
def retrieve(query: str, top_k: int = 20) -> list[Fragment]:
embeddings_query = encoder.encode(query)
candidates = vector_db.search(embeddings_query, k=top_k * 3)
bm25_results = bm25_index.search(query, k=top_k * 3)
fused = rrf_fuse(candidates, bm25_results, k=top_k)
return [
Fragment(
text=f.text,
document_id=f.document_id, # critic pentru validare
offset_start=f.offset_start, # critic pentru validare
offset_end=f.offset_end,
score=f.score,
source_url=f.source_url,
)
for f in fused
]
Fragmentele trebuie indexate cu offset-uri în documentul original, nu doar cu text. Aceasta este cea mai costisitoare schimbare față de un RAG simplu: pipeline-ul de ingest trebuie să păstreze poziția fiecărui chunk în document original.
Poarta 2 — Generate cu format strict de citare
Prompt-ul trebuie să forțeze modelul să citeze textual și să marcheze fiecare claim cu un identificator. Iată un prompt template care funcționează în producție:
Sistemul: Ești un asistent juridic care răspunde STRICT pe baza fragmentelor furnizate.
REGULI:
1. Fiecare afirmație factuală TREBUIE să fie marcată cu [c1], [c2], etc.
2. La sfârșit, listează citările în format JSON:
[{"claim_id": "c1", "document_id": "...", "verbatim_quote": "..."}]
3. verbatim_quote TREBUIE să fie copiat exact din fragmentul indicat, fără parafrază.
4. Dacă nu poți răspunde pe baza fragmentelor, returnează: {"answer": null, "reason": "no_source"}
Fragmente disponibile:
[1] document_id=DOC_123: "În cazul în care contractul individual de muncă..."
[2] document_id=DOC_456: "Concedierea pentru motive imputabile salariatului..."
...
Întrebare utilizator: {query}
Modelul produce ceva de forma:
{
"answer": "Concedierea disciplinară este permisă conform Codului Muncii [c1]. Procedura cere notificare scrisă în 30 zile [c2].",
"citations": [
{"claim_id": "c1", "document_id": "DOC_456", "verbatim_quote": "Concedierea pentru motive imputabile salariatului..."},
{"claim_id": "c2", "document_id": "DOC_456", "verbatim_quote": "termenul de aplicare a sancțiunii disciplinare este de 30 zile..."}
]
}
Poarta 3 — Validate textual
Aici se întâmplă magia. Pentru fiecare citation, validăm că verbatim_quote apare efectiv în document_id:
def validate_citations(answer: dict, fragments: list[Fragment]) -> ValidationResult:
fragment_by_doc = {f.document_id: f for f in fragments}
failed = []
for citation in answer["citations"]:
doc_id = citation["document_id"]
quote = citation["verbatim_quote"]
if doc_id not in fragment_by_doc:
failed.append((citation["claim_id"], "doc_not_in_retrieved"))
continue
source_text = fragment_by_doc[doc_id].text
# Validare exactă (case-insensitive, dar diacritice respectate)
if quote.lower() in source_text.lower():
continue
# Validare cu fuzzy match dacă diferența e doar whitespace/punctuație
if normalize_text(quote) in normalize_text(source_text):
continue
# Validare cu similarity threshold ÎNALT (>= 0.95) pentru OCR-induced noise
if text_similarity(quote, source_text) >= 0.95:
continue
failed.append((citation["claim_id"], "quote_not_found"))
return ValidationResult(
passed=len(failed) == 0,
failed_claims=failed,
)
Threshold-ul de 0,95 nu este pentru semantică, ci pentru noise tehnic (caractere OCR confuse, whitespace, ligaturi). Sub 0,95 considerăm că modelul a parafrazat, ceea ce nu este acceptabil.
Pipeline complet cu retry
def answer_with_grounding(query: str) -> Response:
fragments = retrieve(query, top_k=20)
if not fragments:
return Response.refusal("no_relevant_sources")
for attempt in range(3):
answer = generate(query, fragments, strictness=attempt)
validation = validate_citations(answer, fragments)
if validation.passed:
return Response.success(answer, fragments)
# Regenerate cu prompt mai strict în care claims-urile failed sunt marcate
# ca exemple de ce să NU faci
# După 3 eșecuri, refuz
return Response.refusal("could_not_ground", failed_claims=validation.failed_claims)
strictness crește la fiecare retry: prima încercare folosește prompt standard, a doua adaugă exemple negative, a treia restrânge la claims minimal posibile.
Poarta 4 — Return controlat
Răspunsul final trebuie să poarte cu el citările active. Pe UI, fiecare claim are un link la fragmentul exact, iar utilizatorul poate vedea contextul complet în 1 click. Această disciplină este esențială pentru avocați și auditori: ei nu acceptă „trust me” pe răspuns, dar acceptă „here is the source, see for yourself”.
Pentru cazurile de refuz, mesajul trebuie să fie acționabil:
"Nu am o sursă suficient de exactă pentru a răspunde la această întrebare.
Sugestie: reformulați cu termeni mai specifici sau consultați direct
[link la rezultate căutare brute pentru query]."
Edge cases critice
Claims agregate care violează multiple surse: „Codul muncii prevede concediere în 30 zile pentru motive disciplinare”. Aceasta combină informație din articol diferit. Soluția: forțați modelul să separe claims-urile granulare.
Citate care se suprapun parțial: două citate care folosesc fraze comune. Soluția: indexare cu offset-uri exacte, nu fuzzy matching pe document complet.
Documente abrogate: legislația se modifică. O citare validă astăzi poate fi din document abrogat acum 6 luni. Soluția: metadata valid_from / valid_until pe fiecare fragment, filter în retrieve.
Citate cu redactări/anonimizări: jurisprudența anonimizată conține [NUMELE PĂRȚII]. Modelul trebuie instruit să păstreze acele markeri exact.
Costuri și beneficii măsurate
Pe un benchmark intern de 1.200 întrebări juridice românești:
- RAG simplu (no grounding): 4,8% halucinare, latency 1.8s p50
- RAG cu citation grounding (acest pipeline): 0,28% halucinare, latency 2.6s p50
Costul în tokens este aproximativ 2,3× mai mare per query (datorită retry-urilor și prompt-ului mai lung). Pentru sectoare reglementate, această taxă este acceptabilă; pentru chatbot generic, nu se justifică.
Diagramă pipeline
Query
→ Retrieve (vector + BM25 + RRF, top-20 cu offset metadata)
→ Generate (prompt cu format JSON citation)
→ Validate (verbatim match >= 0.95, doc_id în set retrieved)
→ if pass: Return cu citări active
→ if fail: Regenerate (max 3 tries) cu strictness crescut
→ if 3 fails: Refuz controlat cu acțiune sugerată
Concluzie operațională
Citation grounding nu este o tehnologie. Este o disciplină de inginerie care reorganizează pipeline-ul RAG în jurul unei garanții: niciun token de răspuns substantiv nu pleacă fără sursă verificabilă textual. Costul este real (latency, tokens, complexitate). Beneficiul este nemonetizabil pentru sectoare unde halucinarea înseamnă răspundere profesională sau juridică.
Pentru clienți CAI Technology pe vertical juridic și procurement, acest pipeline este standard. Pentru implementare pe corpusul propriu, oferim consultanță tehnică structurată pe 4–8 săptămâni.
Articole conexe
- Pillar RAG — arhitecturi enterprise
- Anti-halucinare juridic: chatbot pe 2,8 milioane documente
- Hybrid search RRF vs Cohere vs cross-encoder
Surse externe
- Lewis et al., „Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks”
- Gao et al., „Enabling Large Language Models to Generate Text with Citations”
- Asai et al., „Self-RAG: Learning to Retrieve, Generate, and Critique through Self-Reflection”
- Menick et al., „Teaching Language Models to Support Answers with Verified Quotes”
Următorul pas
Dacă echipa dumneavoastră evaluează un pipeline de citation grounding pe corpus propriu, putem oferi o sesiune tehnică de 30 de minute pentru estimare de fezabilitate.