Numera får RAG ibland dåligt rykte, antingen för att det anses vara helt trivialt (det är lätt att komma igång, men svårare att skala) eller för att det sägs ha blivit omsprunget av ”agentbaserade system” (som ofta snabbt börjar likna RAG om man skrapar lite på ytan …)
I det här blogginlägget går vi igenom ett par praktiska exempel som visar hur vi hanterar några vanliga utmaningar:
Hantera blandade text- och sifferdata och varför de ställer till det för naiv RAG: nyckelord krockar och siffror saknar semantisk betydelse.
Varför det hjälper att utforma inbäddningar med sammanfattningen i fokus: skapa en kort beskrivande sammanfattning av varje del och använd sedan den för inbäddning och sökning.
Så skapar du kontextuella sammanfattningar: ta med kontext från det överordnade dokumentet så att snarlik statistik går att skilja åt.
När du bör använda kod och Pydantic-modeller: när ordagrant innehåll är viktigt kan du kombinera anpassad kod och/eller en Pydantic-modell med LLM-anrop för högre tillförlitlighet.
Grunderna
RAG-system används till allt från supportrobotar till interna kunskapsassistenter.
Bakom kulisserna brukar du:
Dela upp källdokumenten i delar
Bädda in varje del i ett vektorrum
Hämta de K mest relevanta delarna vid sökningen
Generera ett svar baserat på dessa delar
Populära verktyg som LangChain, LlamaIndex och OpenAI Filestore gör dessa steg nästan triviala. Men i verkliga datapipelines stöter du på data som inte bara består av kompakt text, och då kan grundläggande RAG få problem. I följande avsnitt visar vi konkreta exempel på datautmaningar och bygger stegvis ut lösningen i takt med att komplexiteten ökar.
När dina data inte bara är text (vilket faktiskt är ganska vanligt)
Tänk dig följande datadel i en spelkontext:
JSON
Inbäddningar fungerar tack vare inlärda samband mellan ord, baserade på semantisk betydelse och grammatik. I exemplet ovan blandas text och siffror, och utanför just denna kontext har siffrorna inga samband med orden. Man kan alltså säga att den här datadelen i stort sett består av några någorlunda beskrivande ord följda av ett antal slumpmässiga siffror.
Det skulle egentligen inte vara ett problem om detta var den enda datatypen, eftersom vi fortfarande kunde söka via inbäddningarna för de få beskrivande ord som finns (eller helt enkelt använda text-to-SQL). Men vad händer om den här delen ligger begravd bland mängder av texttäta delar där samma ord också förekommer? Till exempel:
JSON
Tänk nu att vi vill hitta svaret på ”What is the attack range with Draconic Ascension?” Vi kommer sannolikt inte att hitta den relevanta delen eftersom den drunknar i bruset från andra delar med samma nyckelord.
Grundproblemet är att vi inte kan skilja datadelarna åt tillräckligt väl, trots att de innehåller olika slags information om samma ämne. Kan vi berika eller förbättra detta på något sätt? Självklart kan vi det:smile:
Berika dina data genom att sammanfatta dem – ja, du läste rätt
I stället för att bädda in själva delen direkt kan vi först skapa en sammanfattning som beskriver dess innehåll och sedan använda sammanfattningen för inbäddning och sökning. I genereringssteget använder vi fortfarande de ursprungliga data som är kopplade till sammanfattningen.
För de två exemplen ovan skulle vi alltså kunna skapa sammanfattningar som dessa:
Attackstatistik för räckvidd, hastighet och skada (som standard och med Draconic Ascension).
Beskrivning och detaljer om Draconic Ascension, inklusive aktiveringsvillkor, visuella effekter och bakgrundshistoria.
Sedan kompletterar vi även frågan så att den ”stämmer överens” med sammanfattningen. Vi kan till exempel omvandla ”What is the attack range with Draconic Ascension?” till ”What is the statistics of attack range with Draconic Ascension?” Detta är särskilt viktigt när sökfrågan kommer från användare utanför tekniska områden som frågar på ~~”fritt formulerat”~~ vanligt mänskligt språk. De varken vet eller bryr sig om hur RAG fungerar eller hur precision och täckning maximeras.


Ta inte saker ur sitt sammanhang (ett allmänt användbart livsråd)
I nästa scenario hanterar vi mängder av datadelar som ser likadana ut, som nedan:
Plain Text
Om vi fortsätter på samma sätt kan vi tänka oss frågan ”what is character X’s attack range?” Med sammanfattningarna vi just skapade skulle det bli en ren gissningslek, eftersom även de skulle se mycket lika ut. Hur kan vi då skilja dem åt?
Det enkla svaret är att tillföra kontext. Vi kan helt enkelt lägga till en referens till delens överordnade dokument i datadelen, i det här fallet exempelvis {”character”: “X”}. Då kan vi hämta rätt data för karaktär X även när vi har motsvarande data för karaktär Y och Z.
En bättre och mer generaliserbar metod är dock att skapa en kontextuell sammanfattning av delen. I stället för att bara sammanfatta själva datadelen kan vi alltså skicka både det överordnade dokumentet och delen för att skapa en kontextuell sammanfattning. Där beskriver vi hur delen passar in i det överordnade dokumentet, till exempel:
Den här delen innehåller detaljerad statistik om … för karaktär X. Delen passar in i det fullständiga dokumentet genom att visa X:s styrka när det gäller attackhastighet …
Den här delen innehåller detaljerad statistik om … för karaktär Y. Delen passar in i det fullständiga dokumentet genom att visa Y:s förstärkta statistik när hon använder sin specialförmåga …
Den här delen innehåller detaljerad statistik om … för karaktär Z. Delen passar in i det fullständiga dokumentet genom att visa att Z:s statistik gör karaktären väl lämpad som tank i lagmatcher …
Den här metoden (som delvis har inspirerats av Anthropic) kan verka överdriven för exemplet ovan. Den är dock mycket effektiv för delar som kan misstolkas ”tagna ur sitt sammanhang” och ger dessutom en enhetlig metod för alla delar, vilket håller den tekniska pipelinen välordnad.


När du måste vara ~~ett kontrollfreak~~ rigorös
Vanligtvis får vi data i kompletta stycken och delar upp dem för ett RAG-system. I det här exemplet visar vi något lite annorlunda: data som har delats upp i dåliga delar. De består av slumpmässiga avsnitt från en logisk helhet som egentligen måste sammanfogas igen. En logisk del är innehåll som naturligt hör ihop, till exempel ett underavsnitt i ett dokument eller ett sammanhängande stycke.


Vårt första försök är att skicka alla data i ett LLM-anrop, be LLM:en gruppera dem på lämpligt sätt och sedan returnera det grupperade innehållet. En LLM borde vara ganska bra på det, eller hur? Nja, både ja och nej.
Här och vid flera andra tillfällen har vi sett att LLM:er tenderar att ta genvägar och blir opålitliga när hela det exakta innehållet måste återges, särskilt vid lång kontext. Vilket är fullt begripligt. Men för just det här användningsfallet var det avgörande, eftersom vi behöver det exakta innehållet ord för ord – inga sammanfattningar och inget utelämnat från originalet. Vi får inte missa några detaljer.
Den positiva sidan var förstås att LLM:en var utmärkt på att förstå de uppdelade delarnas semantik och struktur. Om den bara inte vägrade att återge innehållet exakt. Attans:/
Hur kan vi då dra nytta av det en LLM är bra på och samtidigt undvika det som den hanterar mindre tillförlitligt? Vi tog hjälp av vår gamla goda vän koden (läs: en anpassad Python-funktion). Och en Pydantic-modell som inte kunde vara enklare. Här är lösningen:
Gå igenom avsnitten ett i taget och håll reda på den aktuella logiska delen
För varje avsnitt frågar du LLM:en: does this section belong to the current logical chunk, answer yes or no (following the pydantic model)?
Om svaret är ja lägger du avsnittet till delen. Om svaret är nej matar du ut den aktuella logiska delen, som då är färdig, och startar en ny med avsnittet.


Det går förstås åt lite fler token än om allt innehåll bearbetas i ett enda steg. Men i det här användningsfallet, där det viktigaste var att bevara innehållet exakt, var den lilla extrakostnaden väl motiverad.
Det här är en mycket enkel lösning, men den följer en viktig princip: när stringens krävs bör vi inte enbart förlita oss på LLM:er, eftersom de trots allt är probabilistiska.
Med anpassad kod, anpassade funktioner och Pydantic-modeller kan vi få förutsägbara och tillförlitliga resultat samtidigt som vi utnyttjar LLM:ernas fulla kapacitet.
Att bygga en generativ AI-lösning är lika mycket en ingenjörsutmaning som en AI-utmaning. Vi hoppas att exemplen har inspirerat dig att ta itu med dina egna unika utmaningar. Vill du läsa mer om teknikdrivna lösningar för generativ AI kan du läsa vårt blogginlägg om utformning av routerbaserade agentbaserade system.