Praktiske eksempler på skræddersyede RAG-løsninger

Eksempler fra virkeligheden viser, hvordan skræddersyede RAG-systemer løser komplekse vidensproblemer i virksomheder.

I dag har RAG af og til et dårligt ry, enten fordi folk nu mener, at det er helt banalt (det er nemt at komme i gang med, men sværere at skalere), eller fordi de mener, at det er blevet overhalet af »agentbaserede systemer« (som i mange tilfælde hurtigt begynder at ligne RAG, hvis man kradser lidt i overfladen …)

I dette blogindlæg gennemgår vi et par praktiske eksempler på, hvordan vi håndterer nogle almindelige udfordringer, nemlig:

  • Håndtering af blandede tekst- og taldata, og hvorfor det får naiv RAG til at bryde sammen: Nøgleord overlapper, og tal har ingen semantisk betydning.

  • Derfor hjælper det at designe embeddings med resuméet først: Generér et kort, beskrivende resumé af hver blok, og opret embeddings og søg derefter ud fra resuméet.

  • Sådan genererer du kontekstuelle resuméer: Medtag kontekst fra det overordnede dokument, så statistikker med samme struktur fortsat kan skelnes fra hinanden.

  • Hvornår du bør bruge kode og Pydantic-modeller: Når ordret gengivelse er vigtig, kan du kombinere specialudviklet kode og/eller en Pydantic-model med LLM-kald for at opnå større driftssikkerhed.

Udvikling af skræddersyede RAG-løsninger

Det grundlæggende

RAG-systemer driver alt fra supportbots til interne vidensassistenter.

Under motorhjelmen gør du typisk følgende:

  1. Opdel dine kildedokumenter i blokke

  2. Indlejr hver blok i et vektorrum

  3. Hent de K højest rangerede blokke på forespørgselstidspunktet

  4. Generér et svar baseret på disse blokke

Populære værktøjssæt som LangChain, LlamaIndex og OpenAI’s Filestore gør disse trin næsten trivielle. Men i virkelige datapipelines møder du data, der ikke kun består af tæt tekst, og her kan grundlæggende RAG komme til kort. I de følgende afsnit viser vi konkrete eksempler på dataudfordringer og bygger gradvist løsningen ud, efterhånden som kompleksiteten stiger.

Når det bliver mere kompliceret

  1. Når dine data ikke kun består af tekst (hvilket faktisk ikke er usædvanligt)

Se på følgende datablok fra en spilkontekst:

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, } }} 

Embeddings fungerer på grund af de relationer mellem ord, som er indlært gennem semantisk betydning og grammatik. I dataene ovenfor har vi en blanding af tekst og tal, hvor tallene uden for netop denne kontekst ikke har nogen relation til ordene. Man kan derfor sige, at denne datablok stort set består af en kombination af nogenlunde beskrivende ord efterfulgt af nogle tilfældige tal.

Det ville faktisk ikke være et problem, hvis det var den eneste type data, vi havde, for vi kunne stadig hente dem via embeddings for de få tilgængelige beskrivende ord (eller blot bruge text-to-SQL). Men hvad nu, hvis denne blok er begravet mellem en masse teksttunge blokke, hvor de samme ord også forekommer? For eksempel:

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." } }}

Forestil dig nu, at vi vil hente svaret på »What is the attack range with Draconic Ascension?« Vi vil højst sandsynligt ikke kunne hente den relevante blok, fordi den drukner i støjen fra andre blokke med de samme nøgleord.

Det grundlæggende problem er, at vi ikke kan skelne ordentligt mellem disse datablokke, selv om de indeholder forskellige typer oplysninger om det samme emne. Kan vi på en eller anden måde berige eller forbedre dem? Selvfølgelig kan vi det :smile:

  1. Berig dine data ved at opsummere dem – ja, du læste rigtigt

I stedet for at oprette en embedding direkte for selve blokken kan vi først generere et resumé, der beskriver dataenes indhold, og derefter oprette embeddings og hente data ud fra resuméet. I genereringstrinnet bruger vi stadig de oprindelige data, der er knyttet til resuméet.

Til de to eksempler på blokke ovenfor kunne vi generere resuméer som disse:

  1. Angrebsstatistik for rækkevidde, hastighed og skade (som standard og med Draconic Ascension).

  2. Beskrivelse af og oplysninger om Draconic Ascension, herunder aktiveringsbetingelser, visuelle effekter og baggrundshistorie.

Derefter udvider vi også forespørgslen, så den »flugter« med resuméet. Vi kunne for eksempel ændre »What is the attack range with Draconic Ascension?« til »What is the statistics of attack range with Draconic Ascension?« Det er især vigtigt, når søgeforespørgslen kommer fra brugere uden teknisk baggrund, som spørger i ~~»fri stil«~~ almindeligt menneskesprog. De hverken ved eller bekymrer sig om, hvordan RAG fungerer, eller hvordan præcision og genfinding maksimeres.

Diagram, der illustrerer, hvornår det bliver mere kompliceret.

  1. Tag ikke ting ud af deres sammenhæng (et generelt godt råd i livet)

Et oplagt næste scenarie er at håndtere et stort antal datablokke, der ligner hinanden, som vist nedenfor:

Plain Text

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

Hvis vi fortsætter med samme tilgang, så forestil dig spørgsmålet »what is character X’s attack range?« Med de resuméer, vi netop har genereret, ville vi være overladt til et gættespil baseret på held, fordi de også ville ligne hinanden meget. Hvordan kan vi så skelne mellem dem?

Det enkle svar er at tilføje kontekst. Vi kunne ganske enkelt medtage en reference til blokkens overordnede dokument i datablokken, f.eks. {”character”: “X”} i dette tilfælde. Så ville vi kunne hente de rigtige data for Karakter X præcist, selv når vi har de samme data for Karakter Y og Z.

En bedre og mere generaliserbar tilgang ville dog være at generere et kontekstuelt resumé af blokken. I stedet for kun at generere et resumé af selve datablokken kunne vi med andre ord sende både det overordnede dokument og blokken med for at generere et kontekstuelt resumé. Her beskriver vi i resuméet, hvordan blokken passer ind i det overordnede dokument, f.eks.:

  1. Denne blok indeholder detaljeret statistik om … for Karakter X. Blokken passer ind i hele dokumentet ved at vise X’s styrke inden for angrebshastighed …

  2. Denne blok indeholder detaljeret statistik om … for Karakter Y. Blokken passer ind i hele dokumentet ved at vise Y’s forbedrede statistik, når hun bruger sin særlige evne …

  3. Denne blok indeholder detaljeret statistik om … for Karakter Z. Blokken passer ind i hele dokumentet ved at vise, at Z’s statistik gør figuren velegnet som tank i holdkampe …

Denne metode (som delvist er inspireret af Anthropic) kan virke overdreven i eksemplet ovenfor. Den er dog meget effektiv til blokke, der kan misfortolkes »ude af kontekst«, og giver samtidig en ensartet tilgang, der fungerer for alle blokke og sikrer en ryddelig teknisk pipeline.

Diagram, der illustrerer, hvornår det bliver mere kompliceret.

  1. Når du skal være ~~kontrolfreak~~ stringent

Normalt modtager vi data i hele enheder og opdeler dem i blokke til et RAG-system. I dette eksempel viser vi noget lidt andet: Dataene er opdelt i blokke, men det er dårlige blokke. De er tilfældige dele af en logisk blok, som egentlig skal samles igen. En logisk blok er indhold, der naturligt hører sammen, f.eks. et underafsnit i et dokument eller et sammenhængende tekstafsnit.

Diagram, der illustrerer, hvornår det bliver mere kompliceret.

Vores første forsøg med disse data er at sende det hele med i et LLM-kald, bede modellen gruppere delene, som den finder passende, og derefter returnere det grupperede indhold. En LLM burde være ret god til det, ikke? Både ja og nej.

Vi har også ved flere andre lejligheder konstateret, at LLM’er har tendens til at springe over, hvor gærdet er lavest, og er upålidelige, når man kræver det fulde og nøjagtige indhold – især med en lang kontekst. Hvilket giver god mening. Men det var en afgørende hindring i netop dette scenarie, fordi vi havde brug for indholdet ordret og fuldstændigt – uden resuméer og uden at springe dele af originalen over. Vi må ikke gå glip af en eneste detalje.

»Ja«-delen var naturligvis, at modellen var rigtig god til at forstå de brudte blokkes semantik og struktur. Bare den ikke nægtede at gengive det nøjagtige indhold. Pokkers også :/

Hvordan kunne vi så udnytte det, en LLM er god til, uden at være afhængige af det, den er upålidelig til? Vi vendte os mod vores gode gamle ven, kode (læs: en skræddersyet Python-funktion). Og en Pydantic-model, der ikke kunne være enklere. Her er løsningen:

  • Gennemgå afsnittene ét ad gangen, og vedligehold samtidig en aktuel logisk blok

  • Spørg LLM’en ved hvert afsnit: Tilhører dette afsnit den aktuelle logiske blok? Svar ja eller nej (i henhold til Pydantic-modellen).

  • Hvis ja, føjes afsnittet til blokken. Hvis nej, returneres den aktuelle logiske blok, fordi den er færdig, og der oprettes en ny med afsnittet.

Diagram, der illustrerer, hvornår det bliver mere kompliceret.

Vi bruger naturligvis lidt flere tokens her end ved én gennemgang af hele indholdet. Men i dette scenarie, hvor det vigtigste var at bevare det nøjagtige indhold, var den beskedne ekstraomkostning det hele værd.

Det er en meget enkel løsning, men den følger et vigtigt princip: Når der kræves stringens, bør vi ikke udelukkende stole på LLM’er, da de trods alt er probabilistiske.

Specialudviklet kode og funktioner samt Pydantic-modeller kan bruges til at opnå forudsigelige og pålidelige resultater, samtidig med at LLM’ernes potentiale udnyttes.

Afrunding

At udvikle en generativ AI-løsning er lige så meget en teknisk udfordring, som det er en AI-udfordring. Vi håber, at disse eksempler har inspireret dig til at tage fat på dine egne særlige udfordringer. Hvis du vil læse mere om teknikorienterede løsninger med generativ AI, kan du se vores blogindlæg om design af routerbaserede agentbaserede systemer.

Forfatter

Cynthia Yu