Darrerament, la RAG de vegades té mala fama, sigui perquè ara es considera totalment trivial (començar ho és, escalar-ho no tant) o perquè es creu que els «sistemes agèntics» l'han superada (tot i que, en molts casos, si grates una mica la superfície, de seguida s'assemblen molt a la RAG...).
En aquest article presentem un parell d'exemples pràctics per mostrar com afrontem alguns reptes habituals, concretament:
Gestionar dades mixtes de text i números i per què fan fallar una RAG ingènua: les paraules clau col·lideixen i els números no aporten significat semàntic.
Per què ajuda dissenyar embeddings basats primer en resums: genera un breu resum descriptiu per fragment i fes l'embedding i les consultes sobre aquest resum.
Com generar resums contextuals: inclou el context del document principal per distingir estadístiques amb formats semblants.
Quan cal recórrer al codi i als models de Pydantic: quan el contingut literal sigui important, combina codi personalitzat o un model de Pydantic amb crides a LLM per garantir la fiabilitat.
Conceptes bàsics
Els sistemes RAG impulsen tota mena d'eines, des de bots d'assistència fins a assistents interns de coneixement.
Per sota, normalment cal:
Dividir en fragments els documents d'origen
Incrustar cada fragment en un espai vectorial
Recuperar els K fragments principals en el moment de la consulta
Generar una resposta condicionada per aquests fragments
Eines populars com LangChain, LlamaIndex i Filestore d'OpenAI fan que aquests passos siguin gairebé trivials. Però, en fluxos de treball reals, trobareu dades que no són només text dens, i una RAG bàsica pot tenir dificultats. En els apartats següents mostrarem exemples concrets de reptes relacionats amb les dades i construirem gradualment la solució a mesura que n'augmenti la complexitat.
Quan les dades no són només text (força habitual, de fet)
Considerem el fragment de dades següent en el context d'un videojoc:
JSON
Els embeddings funcionen gràcies a les relacions apreses entre les paraules mitjançant el significat semàntic i la gramàtica. En les dades anteriors hi ha una barreja de text i números que, fora d'aquest context concret, no tenen cap relació amb les paraules. Per tant, podríem dir que aquest fragment de dades és, bàsicament, una combinació de paraules més o menys descriptives seguides d'uns quants números aleatoris.
Això no seria cap problema si fos l'únic tipus de dades que tinguéssim, perquè encara podríem recuperar-les mitjançant els embeddings de les poques paraules descriptives disponibles (o simplement utilitzar text-to-SQL). Però què passa si aquest fragment està enterrat entre molts fragments de text dens on també apareixen aquestes paraules? Per exemple:
JSON
Ara imaginem que volem recuperar "What is the attack range with Draconic Ascension?" És molt probable que no puguem recuperar el fragment pertinent perquè queda perdut entre el soroll d'altres fragments que contenen les mateixes paraules clau.
El problema de fons és que no podem diferenciar prou bé aquests fragments de dades, tot i que contenen tipus d'informació diferents sobre el mateix tema. Podríem enriquir-les o millorar-les d'alguna manera? És clar que sí:smile:
Enriqueix les dades resumint-les; sí, ho has llegit bé
En lloc de generar directament l'embedding del fragment, primer podem crear un resum que descrigui de què tracten les dades i, després, fer l'embedding i la recuperació sobre el resum. En el pas de generació continuaríem utilitzant les dades originals vinculades al resum.
Així doncs, per als dos exemples de fragments anteriors generaríem resums com aquests:
Estadístiques de l'abast, la velocitat i el dany de l'atac (per defecte i amb Draconic Ascension).
Descripció i detalls de Draconic Ascension, incloses les condicions d'activació, els efectes visuals i la història.
Després també ampliem la consulta perquè quedi «alineada» amb el resum. Per exemple, transformaríem "What is the attack range with Draconic Ascension?" en "What is the statistics of attack range with Draconic Ascension?" Això és especialment important quan la consulta de recuperació prové d'usuaris aliens als àmbits tècnics que pregunten en llenguatge humà ~~"freestyle"~~ normal, perquè, al capdavall, no saben ni els preocupa com funciona una RAG per maximitzar la precisió i l'exhaustivitat.


No treguis les coses de context (un consell aplicable a la vida en general)
Un altre escenari és haver de gestionar una gran quantitat de fragments de dades que s'assemblen, com els següents:
Plain Text
Si mantenim el mateix enfocament, imaginem que preguntem "what is character X's attack range?" Amb els resums que acabem de generar, hauríem de jugar a endevinar i confiar en la sort, perquè també s'assemblarien molt. Com podríem diferenciar-los, doncs?
La resposta senzilla és aportar context. Podríem incloure una referència al document principal del fragment dins del fragment de dades; en aquest cas, per exemple, {”character”: “X”}. Així podríem recuperar amb precisió les dades correctes del personatge X encara que també tinguéssim les mateixes dades dels personatges Y i Z.
Tanmateix, un enfocament millor i més generalitzable seria generar un resum contextual del fragment. És a dir, en lloc de generar un resum només del fragment de dades, podríem passar tant el document principal com el fragment per generar un resum contextual general que expliqués com encaixa el fragment en el document principal, per exemple:
Aquest fragment proporciona estadístiques detallades de... per al personatge X. El fragment encaixa en el document complet perquè mostra la força de X quant a velocitat d'atac...
Aquest fragment proporciona estadístiques detallades de... per al personatge Y. El fragment encaixa en el document complet perquè mostra les estadístiques millorades de Y amb la seva habilitat especial...
Aquest fragment proporciona estadístiques detallades de... per al personatge Z. El fragment encaixa en el document complet perquè mostra que les estadístiques de Z són molt adequades per fer de tanc en partides en equip...
Aquest mètode (inspirat parcialment en Anthropic) pot semblar excessiu per a l'exemple anterior, però és molt eficaç amb fragments que es podrien malinterpretar «fora de context». A més, ofereix un enfocament unificat que funciona amb tots els fragments i permet mantenir un flux d'enginyeria ordenat.


Quan cal ser ~~un maniàtic del control~~ rigorós
Normalment rebem les dades en peces completes i les dividim en fragments per a un sistema RAG. En aquest exemple mostrem un cas una mica diferent: dades dividides en fragments, però en fragments mal fets. Són seccions aleatòries d'un fragment lògic que, en realitat, cal tornar a agrupar. Un fragment lògic és un bloc de contingut que, per naturalesa, hauria d'anar junt, com ara una subsecció d'un document o un paràgraf coherent.


El primer intent amb aquestes dades consisteix a passar-ho tot en una crida a un LLM, demanar-li que ho agrupi com consideri adient i que retorni el contingut agrupat. Un LLM ho hauria de fer força bé, oi? Doncs sí i no.
En aquesta i diverses altres ocasions, hem comprovat que els LLM tendeixen a actuar amb mandra i no són fiables quan se'ls exigeix el contingut complet i exacte, sobretot si el context és llarg. I és del tot comprensible. Però això era inacceptable en aquest cas d'ús concret, perquè necessitàvem el contingut exacte, paraula per paraula: sense resums ni cap omissió del contingut original. No ens podem deixar cap detall.
Naturalment, la part positiva era que entenia molt bé la semàntica i les estructures dels fragments mal dividits. Sempre que no es negués a reproduir-ne el contingut exacte. Maleït sigui:/
Com podíem aprofitar allò que un LLM fa bé i evitar allò en què no és fiable? Vam recórrer al nostre vell amic, el codi (és a dir, una funció de Python personalitzada). I a un model de Pydantic «més senzill impossible». Aquesta és la solució:
Recórrer les seccions mantenint un fragment lògic actual
A cada secció, preguntar a l'LLM: aquesta secció pertany al fragment lògic actual? Respon sí o no (seguint el model de Pydantic).
Si respon que sí, afegir la secció al fragment; si respon que no, donar per acabat el fragment lògic actual i crear-ne un de nou amb la secció.


Naturalment, així fem servir una mica més de tokens que processant tot el contingut d'una sola passada, però, en aquest cas d'ús concret, en què conservar el contingut exacte era la màxima prioritat, el petit cost addicional valia molt la pena.
És una solució molt senzilla, però segueix un principi important: quan cal rigor, no convé confiar exclusivament en els LLM perquè, al capdavall, són probabilístics.
El codi i les funcions personalitzats, juntament amb els models de Pydantic, permeten obtenir resultats previsibles i fiables sense renunciar a les capacitats dels LLM.
Crear una solució d'IA generativa és tant un repte d'enginyeria com d'IA. Esperem que aquests exemples us hagin inspirat a abordar els vostres reptes particulars. Per saber més sobre solucions d'IA generativa centrades en l'enginyeria, consulteu el nostre article sobre el disseny de sistemes agèntics basats en encaminadors.