RAG multilingv RO + EN: pattern de implementare cu BGE-M3
Cum construiți un RAG care răspunde în română pe corpus mixt RO+EN: cross-lingual retrieval, prompts adaptive, citation language match.
RAG multilingv RO + EN: pattern de implementare cu BGE-M3
Multe companii românești au corpusuri mixte: contracte și legislație în română, dar și manuale tehnice, white papers și standarde internaționale în engleză. Un asistent care servește această realitate trebuie să răspundă într-o singură limbă (de obicei româna utilizatorului), dar să poată recupera și sintetiza informație din ambele limbi.
Acest articol descrie pattern-ul tehnic pentru un RAG multilingv RO + EN, cu BGE-M3 ca encoder, focus pe cross-lingual retrieval, prompts adaptive și citation language match — provocările care apar în producție.
TL;DR
- BGE-M3 (multilingual) permite cross-lingual retrieval nativ: query în română returnează fragmente relevante și din documente engleze, fără traducere intermediară.
- Pattern-ul de bază: encoder unic multilingv + index unic + LLM cu instrucțiuni adaptive de limbă pentru output.
- Citation language match: dacă răspundeți în română, citările trebuie să fie din limba documentului original (română sau engleză), nu traduceri fabricate.
- Prompts trebuie adaptive: includ limba dorită de utilizator + limba fragmentului pentru a forța model să nu „traducă” greșit.
- Capcane: chunking strategy, normalizare diacritice, balansare de relevanță cross-lingual, evaluare benchmark RO + EN cu ground truth pe ambele.
De ce BGE-M3 pentru cross-lingual
BGE-M3 a fost antrenat explicit pe 100+ limbi cu cross-lingual alignment. Asta înseamnă că vectorul pentru „concediere disciplinară” în română și „disciplinary dismissal” în engleză sunt aproape în spațiul vectorial. Cosine similarity între acești vectori este tipic peste 0.85.
Practic, asta vă permite:
- Un singur index vectorial care conține fragmente RO și EN.
- Un singur encoder pentru queries.
- Cross-lingual retrieval automat: query în română returnează fragmente engleze relevante și viceversa.
Alternativele (a) traducerea queries la engleză înainte de retrieval, sau (b) doi indici separați cu alegere bazată pe language detection, sunt mai complicate, mai lente, și mai puțin precise.
Arhitectura pattern-ului
Query (RO sau EN)
│
▼
Language detection (opțional, pentru output instruction)
│
▼
BGE-M3 encoder → query vector (1024d)
│
▼
Vector search pe index unic mixt RO + EN → top-50
+
BM25 search per limbă (două BM25 indici, fuzionate cu RRF)
│
▼
Cross-encoder reranker multilingv (BGE-reranker-v2-m3)
│
▼
Top-10 fragmente (mix RO + EN)
│
▼
LLM cu prompt adaptive
│
▼
Răspuns în limba utilizatorului + citări în limba originală a documentului
Pasul 1 — Indexarea mixtă
Fragmentele se indexează cu metadata lang care marchează limba originală:
class Fragment:
text: str
lang: str # "ro" | "en"
document_id: str
document_title: str
offset_start: int
offset_end: int
embedding: list[float] # din BGE-M3, dimensiune 1024
Indexarea folosește același encoder (BGE-M3) pentru ambele limbi. Rezultatul: vectori comparabili cross-lingual.
Capcană: chunking-ul trebuie să respecte structura documentului în limba lui. Documentele juridice românești au o structură ierarhică diferită de un white paper englez. Folosiți chunking semantic per limbă.
Pasul 2 — BM25 per limbă
BM25 nu funcționează cross-lingual (este lexical, nu semantic). Soluția: două indici BM25 separați (unul RO, unul EN), interogați în paralel, fuzionați cu RRF împreună cu rezultatele dense.
def hybrid_search(query: str, lang_hint: str = None, top_k: int = 50):
dense_results = vector_db.search(encoder.encode(query), k=top_k)
bm25_ro = bm25_index_ro.search(query, k=top_k)
bm25_en = bm25_index_en.search(query, k=top_k)
fused = rrf_fuse([dense_results, bm25_ro, bm25_en], k=top_k)
return fused
Pasul 3 — Reranking cross-lingual
BGE-reranker-v2-m3 este multilingv prin design. Acceptă pereche (query RO, document EN) și produce scor relevant. Nu cere traducere intermediară.
În practică, am observat că scorurile rerankers pe fragmente cross-lingual sunt ușor mai conservatoare (-0.05 până la -0.10) față de same-language. Dacă vreți să balansați acest bias, aplicați un boost de +0.05 pe scoruri cross-lingual.
Pasul 4 — Prompt adaptive
Prompt-ul către LLM trebuie să conțină explicit:
- Limba dorită de output (de obicei limba query-ului).
- Limba fragmentelor — pentru fiecare fragment în parte.
- Instrucțiunea de a nu „traduce” citările, ci de a păstra textul original.
Exemplu:
Sistemul: Asistent juridic. Răspunzi STRICT pe baza fragmentelor.
REGULI:
1. Răspunsul TĂU trebuie să fie în {output_language}.
2. Citările (verbatim_quote) trebuie păstrate ÎN LIMBA originală a fragmentului.
3. NU traduce citările.
4. Adaugă o notă scurtă care indică limba fragmentului dacă diferă de limba răspunsului.
Fragmente:
[1] (lang=ro, doc_id=DOC_RO_123): "Concedierea disciplinară conform articolului 248..."
[2] (lang=en, doc_id=DOC_EN_456): "Disciplinary dismissal under EU framework..."
Întrebare utilizator (limba: ro): {query}
Pasul 5 — Citation language match
Aceasta este cea mai subtilă regulă. Fără ea, LLM-urile au tendința să traducă citările pentru „consistență”. Asta strică citation grounding.
În validation gate, verificați:
def validate_citation_language(citation, fragment):
if fragment.lang == "ro":
return is_likely_romanian(citation.verbatim_quote)
if fragment.lang == "en":
return is_likely_english(citation.verbatim_quote)
Capcane practice
Diacritice inconsistente. Documente RO publice au amestec de diacritice corecte și aproximative. Normalizarea diacriticelor înainte de indexare îmbunătățește retrieval-ul. Dar pentru validare textuală, păstrați ambele variante (originalul + normalized) pentru match.
Nume proprii. Numele de instituții, persoane, companii apar adesea identic în RO și EN. BM25 cross-lingual prinde uneori match-uri pe nume proprii.
Traducerile oficiale. Pentru directive UE, există versiuni oficiale paralele RO și EN. Dacă indexați ambele, RAG-ul va prezenta ambele ca surse, ceea ce poate dubla informația. Soluția: dedup pe (regulation_id, article_number).
Vocabulary mismatch. „Procurement” în engleză vs „achiziții publice” în română — ambele se referă la același concept, dar BM25 nu conectează automat. Dense retrieval cu BGE-M3 face conexiunea.
Evaluare. Benchmark-ul vostru intern trebuie să conțină perechi (query RO, ground truth fragmente RO + EN) și (query EN, ground truth RO + EN).
Diagramă pattern complet
Document RO (chunking semantic structurat)
↘
BGE-M3 encoder → vector unique multilingv
↗
Document EN (chunking adaptat)
Query (RO sau EN)
→ Language detect (output instruction)
→ BGE-M3 → dense top-50 cross-lingual
→ BM25 RO + BM25 EN (per limbă)
→ RRF fusion
→ BGE-reranker-v2-m3 (cross-encoder, multilingv)
→ Top-10 (mix RO + EN)
→ LLM cu prompt adaptive
→ Validation: citate în limba originală a fragmentului
→ Răspuns + citări active cu link la fragment
Concluzie operațională
RAG multilingv RO + EN nu este o complicație, este o cerință standard pentru corpusuri reale din România. Pattern-ul cu BGE-M3 + BM25 per limbă + cross-encoder rerank + prompt adaptive elimină majoritatea problemelor cross-lingual fără a impune dublă infrastructură.
Pentru clienții CAI Technology cu corpus mixt, acest pattern este implementarea recomandată.
Articole conexe
- Pillar RAG — arhitecturi enterprise
- BGE-M3 vs OpenAI embeddings pe queries românești
- Citation grounding: implementare 4 porți
Surse externe
- Chen et al., „BGE M3-Embedding”
- Reimers & Gurevych, „Making Monolingual Sentence Embeddings Multilingual”
- Conneau et al., „Unsupervised Cross-lingual Representation Learning at Scale (XLM-R)”
- Liu et al., „Cross-lingual Retrieval Augmented Generation”
Următorul pas
Pentru o evaluare a corpusului propriu cu pattern-ul multilingv RO + EN, putem rula un POC pe 1000 documente mixte și benchmark de calitate în 2 săptămâni.