Navigare principală

Exemple practice de soluții RAG personalizate

Exemple reale arată cum sistemele RAG personalizate rezolvă probleme complexe de gestionare a cunoștințelor în companii.

În ultima vreme, RAG are uneori o reputație proastă: fie pentru că acum pare ceva complet banal (este ușor la început, mai puțin la scară mare), fie pentru că se crede că a fost depășit de „sistemele cu agenți” (care, în multe cazuri, dacă privim dincolo de aparențe, încep rapid să semene foarte mult cu RAG...)

În acest articol prezentăm câteva exemple practice care arată cum abordăm unele provocări frecvente, mai exact:

  • Gestionarea datelor mixte, text și numere, și motivul pentru care acestea pun probleme unui RAG simplist: cuvintele-cheie se suprapun, iar numerele nu au sens semantic.

  • De ce ajută proiectarea încorporărilor pornind de la rezumat: generați un scurt rezumat descriptiv pentru fiecare fragment, apoi creați încorporarea și efectuați interogarea pe baza acestuia.

  • Cum se generează rezumate contextuale: includeți contextul documentului-sursă, astfel încât statisticile cu structuri similare să poată fi diferențiate.

  • Când să vă bazați pe cod și modele Pydantic: dacă redarea fidelă a conținutului este importantă, combinați codul personalizat și/sau un model Pydantic cu apeluri LLM pentru mai multă fiabilitate.

Crearea unor soluții RAG personalizate

Noțiuni de bază

Sistemele RAG stau la baza multor produse, de la boți de asistență la asistenți interni pentru gestionarea cunoștințelor.

În culise, de obicei:

  1. Împărțiți în fragmente documentele-sursă

  2. Încorporați fiecare fragment într-un spațiu vectorial

  3. Preluați primele K fragmente în momentul interogării

  4. Generați un răspuns pe baza fragmentelor respective

Instrumente populare precum LangChain, LlamaIndex și Filestore de la OpenAI fac acești pași aproape banali. Însă, în fluxurile de lucru din lumea reală, veți întâlni și date care nu sunt doar text dens, iar sistemele RAG de bază pot avea dificultăți. În secțiunile următoare vom prezenta exemple concrete de dificultăți legate de date și vom construi treptat soluția, pe măsură ce adăugăm complexitate.

Când lucrurile se complică

  1. Când datele nu sunt doar text (ceea ce se întâmplă destul de des)

Să analizăm următorul fragment de date dintr-un joc:

JSON

{ "Attack": { "Range": { "default": 5, "with_Draconic_Ascension": 5.5, }, "Speed": { "default": "2 seconds", "with_Draconic_Ascension": "2.2 seconds", }, "Damage": { "default": 10, "with_Draconic_Ascension": 12, } }} 

Încorporările funcționează datorită relațiilor dintre cuvinte învățate prin semnificație semantică și gramatică. Datele de mai sus combină text și numere, iar în afara acestui context exact, numerele nu au nicio legătură cu acele cuvinte. Prin urmare, am putea spune că acest fragment de date este, în esență, o combinație de cuvinte oarecum descriptive urmate de câteva numere aleatorii.

De fapt, acest lucru nu ar fi o problemă dacă am avea doar acest tip de date, deoarece am putea face în continuare preluarea pe baza încorporărilor celor câteva cuvinte descriptive disponibile (sau am putea folosi pur și simplu conversia text în SQL). Dar dacă acest fragment este ascuns printre multe fragmente cu text dens, în care apar aceleași cuvinte? De exemplu:

JSON

{"Draconic_Ascension": { "description": "Transform into dragon form. Attack range and speed are increased by 10%, damage is boosted by 20%.", "details": { "activation_conditions": "Can only be activated when HP is below 50%or Fury meter is full.", "visual_effects": "Wings unfurl, scales shimmer with embers, voice lines change to echoing growls.", "lore": "An ancient bloodline awakens. The bearer of the mark channels the soul of the last Flamewing Wyrm, becoming a living storm of fire and fury." } }}

Acum să presupunem că vrem să preluăm „What is the attack range with Draconic Ascension?” Cel mai probabil, nu vom reuși să preluăm fragmentul relevant, deoarece se pierde în zgomotul celorlalte fragmente care conțin aceleași cuvinte-cheie.

Problema de fond este că nu putem diferenția bine aceste fragmente de date, chiar dacă ele conțin tipuri diferite de informații despre același subiect. Am putea oarecum să îmbogățim sau să îmbunătățim datele? Desigur că putem:smile:

  1. Îmbogățiți datele prin rezumare; da, ați citit bine

În loc să încorporăm direct fragmentul, putem genera mai întâi un rezumat care descrie conținutul datelor, apoi putem crea încorporarea și face preluarea pe baza rezumatului. În etapa de generare, vom folosi în continuare datele originale asociate rezumatului.

Astfel, pentru cele două exemple de fragmente de mai sus, am genera rezumate precum:

  1. Attack statistics of range, speed, and damage (default and with Draconic Ascension).

  2. Description and details of the Draconic Ascension, including activation conditions, visual effects, and lore.

Apoi extindem și interogarea, astfel încât să fie „aliniată” cu rezumatul. De exemplu, am transforma „What is the attack range with Draconic Ascension?” în „What is the statistics of attack range with Draconic Ascension?” Acest lucru este deosebit de important când interogarea de preluare provine de la utilizatori fără pregătire tehnică, care întreabă în limbaj ~~„liber”~~ uman obișnuit. Până la urmă, nu trebuie să știe sau să-i preocupe cum funcționează un sistem RAG pentru a maximiza precizia și acoperirea.

Diagramă care ilustrează situațiile în care lucrurile se complică.

  1. Nu scoateți lucrurile din context (sfat valabil și în viață, în general)

Un alt scenariu este acela în care trebuie să gestionăm o mulțime de fragmente de date care arată la fel, ca mai jos:

Plain Text

# Chunk one{ "Attack": { "Range": { "default": 9, }, ... }}# Chunk two{ "Attack": { "Range": { "default": 6, }, ... }}# Chunk three{ "Attack": { "Range": { "default": 7, }, ... }}

Dacă păstrăm aceeași abordare, imaginați-vă că întrebăm „what is character X’s attack range?” Cu rezumatele pe care tocmai le-am generat, ar trebui să ghicim și să sperăm că avem noroc, deoarece și acestea ar arăta foarte asemănător. Așadar, cum le-am putea diferenția?

Răspunsul simplu: oferim context. Am putea include pur și simplu în fragmentul de date o referință la documentul-sursă al acestuia, de exemplu {”character”: “X”} în acest caz. Astfel, am putea prelua cu precizie datele corecte pentru personajul X chiar dacă avem aceleași date și pentru personajele Y și Z.

Totuși, o abordare mai bună și mai ușor de generalizat ar fi generarea unui rezumat contextual al fragmentului. Mai exact, în loc să generăm un rezumat doar pentru fragmentul de date, am putea furniza atât documentul-sursă, cât și fragmentul pentru a genera un rezumat contextual general, în care să explicăm cum se integrează fragmentul în documentul-sursă, de exemplu:

  1. This chunk provides detailed statistics of … for character X. The chunk fits into the full document by showing X’s strength in attack speed…

  2. This chunk provides detailed statistics of … for character Y. The chunk fits into the full document by showing Y’s boosted stats with her special ability…

  3. This chunk provides detailed statistics of … for character Z. The chunk fits into the full document by showing Z’s stats that’s well suited as a tank in team matches…

Această metodă (inspirată parțial de Anthropic) poate părea excesivă pentru exemplul de mai sus, însă este foarte eficientă în cazul fragmentelor care ar putea fi interpretate greșit „în afara contextului”. În plus, oferă o abordare unitară, valabilă pentru toate fragmentele, și menține un flux tehnic bine organizat.

Diagramă care ilustrează situațiile în care lucrurile se complică.

  1. Când trebuie să fiți ~~obsedați de control~~ riguroși

De obicei, primim seturi complete de date și le împărțim în fragmente pentru un sistem RAG. În acest exemplu prezentăm o situație puțin diferită: date împărțite în fragmente, dar în fragmente prost delimitate. Acestea sunt secțiuni aleatorii dintr-un fragment logic și trebuie, de fapt, regrupate. Un fragment logic este o secțiune de conținut care ar trebui să rămână în mod firesc unitară, cum ar fi o subsecțiune a unui document sau un paragraf coerent.

Diagramă care ilustrează situațiile în care lucrurile se complică.

Prima noastră încercare este să introducem toate aceste date într-un apel LLM, să-i cerem să le grupeze după cum consideră potrivit și apoi să returneze conținutul grupat. Un LLM ar trebui să se descurce destul de bine, nu? Ei bine, și da, și nu.

Am constatat, inclusiv în alte situații, că LLM-urile tind să aleagă calea cea mai ușoară și nu sunt fiabile atunci când aveți nevoie de conținutul integral și exact, mai ales dacă este vorba despre un context lung. Ceea ce este cât se poate de firesc. Dar, pentru acest caz de utilizare, problema era decisivă: aveam nevoie de conținutul exact, cuvânt cu cuvânt, fără rezumate și fără să fie omisă nicio parte din conținutul original. Nu ne putem permite să omitem niciun detaliu.

Desigur, partea bună era că înțelegea excelent semantica și structura fragmentelor divizate greșit. Dacă nu refuza să reproducă exact conținutul. La naiba:/

Cum am putea atunci să folosim capacitățile unui LLM, evitând totodată aspectele pentru care nu este fiabil? Am apelat la vechiul nostru prieten de încredere: codul (a se citi „funcție Python personalizată”). Și la un model Pydantic „mai simplu de atât nu se poate”. Iată soluția:

  • Parcurgeți secțiunile, păstrând în permanență un fragment logic curent

  • Pentru fiecare secțiune, întrebați LLM-ul: această secțiune aparține fragmentului logic curent? Răspunsul trebuie să fie da sau nu (conform modelului Pydantic).

  • Dacă da, adăugați secțiunea la fragment; dacă nu, returnați fragmentul logic curent, deoarece este complet, apoi începeți unul nou cu secțiunea respectivă.

Diagramă care ilustrează situațiile în care lucrurile se complică.

Desigur, folosim ceva mai mulți tokeni decât pentru o singură parcurgere a întregului conținut, dar, în acest caz de utilizare, păstrarea exactă a conținutului era prioritatea principală, așa că micul cost suplimentar a meritat pe deplin.

Este o soluție foarte simplă, dar respectă un principiu important: când este nevoie de rigoare, nu vrem să ne bazăm exclusiv pe LLM-uri, deoarece acestea sunt, în fond, probabilistice.

Codul și funcțiile personalizate, alături de modelele Pydantic, pot asigura rezultate previzibile și fiabile, valorificând în același timp capacitățile LLM-urilor.

În încheiere

Crearea unei soluții de IA generativă este deopotrivă o provocare tehnică și una ce ține de inteligența artificială. Sperăm că aceste exemple v-au inspirat să vă abordați propriile provocări unice. Pentru mai multe informații despre soluțiile de IA generativă axate pe inginerie, citiți articolul nostru despre proiectarea sistemelor cu agenți bazate pe rutare.

Autor

Cynthia Yu