Praktiske eksempler på skreddersydde RAG-løsninger

Eksempler fra virkeligheten viser hvordan skreddersydde RAG-systemer løser komplekse kunnskapsproblemer i virksomheter.

I dag har RAG iblant et dårlig rykte, enten fordi folk nå mener at det er helt trivielt (det er enkelt å komme i gang, men vanskeligere å skalere), eller fordi de tror at «agentbaserte systemer» har tatt over (selv om mange av dem raskt begynner å ligne svært mye på RAG når man ser nærmere på dem …)

I dette blogginnlegget bruker vi et par praktiske eksempler til å vise hvordan vi håndterer noen vanlige utfordringer, nærmere bestemt:

  • Håndtering av blandede tekst- og talldata, og hvorfor dette får naiv RAG til å svikte: Nøkkelord kolliderer, og tall har ingen semantisk betydning.

  • Hvorfor det hjelper å utforme innebygginger med sammendraget først: Generer et kort, beskrivende sammendrag for hver del, og bruk deretter sammendraget til innebygging og søk.

  • Slik genererer du kontekstuelle sammendrag: Ta med kontekst fra det overordnede dokumentet, slik at statistikk med lignende struktur kan skilles fra hverandre.

  • Når du bør bruke kode og Pydantic-modeller: Når ordrett innhold er viktig, bør du kombinere egendefinert kode og/eller en Pydantic-modell med LLM-kall for å sikre pålitelighet.

Bygging av skreddersydde RAG-løsninger

Det grunnleggende

RAG-systemer driver alt fra kundestøtteroboter til interne kunnskapsassistenter.

I praksis gjør du vanligvis følgende:

  1. Deler opp kildedokumentene

  2. Bygger inn hver del i et vektorrom

  3. Henter de K mest relevante delene når et søk utføres

  4. Genererer et svar basert på disse delene

Populære verktøysett – LangChain, LlamaIndex og OpenAIs Filestore – gjør disse trinnene nesten trivielle. Men i reelle dataprosesser møter du data som ikke bare består av sammenhengende tekst, og da kan grunnleggende RAG få problemer. I de neste delene viser vi konkrete eksempler på datautfordringer og bygger gradvis ut løsningen etter hvert som kompleksiteten øker.

Når det blir mer komplisert

  1. Når dataene ikke bare er tekst (noe som faktisk er ganske vanlig)

Se på følgende datadel fra et spill:

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

Innebygginger fungerer på grunn av relasjonene mellom ord som er lært gjennom semantisk betydning og grammatikk. I dataene ovenfor har vi en blanding av tekst og tall. Utenfor akkurat denne konteksten har tallene ingen relasjon til ordene. Vi kan derfor si at denne datadelen stort sett består av noen beskrivende ord etterfulgt av tilfeldige tall.

Dette ville egentlig ikke vært et problem hvis det var den eneste datatypen vi hadde, for vi kunne fortsatt hentet den ved hjelp av innebyggingene til de få beskrivende ordene som finnes (eller bare brukt tekst-til-SQL). Men hva om denne delen ligger begravd blant mange teksttunge deler der de samme ordene 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." } }}

Tenk deg nå at vi vil hente svaret på «What is the attack range with Draconic Ascension?» Mest sannsynlig klarer vi ikke å hente den relevante delen, fordi den drukner i støyen fra andre deler som inneholder de samme nøkkelordene.

Grunnproblemet er at vi ikke klarer å skille disse datadelene godt nok fra hverandre, selv om de inneholder ulike typer informasjon om samme emne. Kan vi berike eller forbedre dette på noen måte? Selvsagt kan vi det:smile:

  1. Berik dataene ved å oppsummere dem – ja, du leste riktig

I stedet for å bygge inn selve delen direkte kan vi først generere et sammendrag som beskriver hva dataene handler om, og deretter bruke sammendraget til innebygging og henting. I genereringstrinnet bruker vi fortsatt originaldataene som er knyttet til sammendraget.

For de to eksemplene ovenfor kunne vi altså generert sammendrag som disse:

  1. Angrepsstatistikk for rekkevidde, hastighet og skade (standard og med Draconic Ascension).

  2. Beskrivelse av og detaljer om Draconic Ascension, inkludert aktiveringsvilkår, visuelle effekter og bakgrunnshistorie.

Deretter utvider vi også søket slik at det «samsvarer» med sammendraget. Vi kan for eksempel gjøre «What is the attack range with Draconic Ascension?» om til «What is the statistics of attack range with Draconic Ascension?» Dette er særlig viktig når søket kommer fra brukere uten teknisk bakgrunn som spør på ~~«fritt språk»~~ vanlig menneskespråk. Det er tross alt verken deres ansvar eller noe de trenger å vite hvordan RAG fungerer for å maksimere presisjon og gjenfinning.

Diagram som viser når ting blir mer kompliserte.

  1. Ikke ta ting ut av kontekst (et godt råd også ellers i livet)

Det neste scenarioet er at vi må håndtere svært mange datadeler som ser like ut, som nedenfor:

Plain Text

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

Hvis vi fortsetter med samme tilnærming, kan du tenke deg spørsmålet «what is character X’s attack range?» Med sammendragene vi nettopp genererte, ville svaret avhenge av ren flaks, fordi også de ville sett svært like ut. Hvordan kan vi da skille dem fra hverandre?

Det enkle svaret er å oppgi kontekst. Vi kan ganske enkelt ta med en referanse til delens overordnede dokument i datadelen, for eksempel {”character”: “X”} i dette tilfellet. Da kan vi hente riktige data om karakter X nøyaktig, selv når vi også har de samme dataene om karakter Y og Z.

En bedre og mer generaliserbar tilnærming er imidlertid å generere et kontekstuelt sammendrag av delen. I stedet for bare å generere et sammendrag av selve datadelen kan vi altså sende inn både det overordnede dokumentet og delen for å generere et generelt kontekstuelt sammendrag. Der beskriver vi hvordan delen passer inn i det overordnede dokumentet, for eksempel:

  1. Denne delen inneholder detaljert statistikk om … for karakter X. Delen passer inn i hele dokumentet ved å vise styrken til X når det gjelder angrepshastighet …

  2. Denne delen inneholder detaljert statistikk om … for karakter Y. Delen passer inn i hele dokumentet ved å vise den forbedrede statistikken til Y når hun bruker spesialevnen sin …

  3. Denne delen inneholder detaljert statistikk om … for karakter Z. Delen passer inn i hele dokumentet ved å vise statistikken som gjør Z godt egnet som tank i lagkamper …

Denne metoden (som delvis er inspirert av Anthropic) kan virke overdreven i eksempelet ovenfor. Den er imidlertid svært effektiv for deler som kan mistolkes «utenfor kontekst», og gir dessuten en enhetlig tilnærming som fungerer for alle deler og sikrer en ryddig utviklingsprosess.

Diagram som viser når ting blir mer kompliserte.

  1. Når du må være ~~kontrollfrik~~ grundig

Vanligvis mottar vi data som hele enheter og deler dem opp for et RAG-system. I dette eksempelet viser vi noe litt annerledes: Dataene er delt opp, men på en dårlig måte. Delene er tilfeldige utsnitt fra en logisk enhet som egentlig må settes sammen igjen. En logisk del er innhold som naturlig hører sammen, for eksempel et underavsnitt i et dokument eller et sammenhengende tekstavsnitt.

Diagram som viser når ting blir mer kompliserte.

I det første forsøket mater vi alle dataene inn i et LLM-kall, ber den gruppere dem slik den mener er riktig, og deretter returnere det grupperte innholdet. En LLM burde være ganske god til dette, ikke sant? Både ja og nei.

Her og i flere andre tilfeller har vi oppdaget at LLM-er ofte tar snarveier og er upålitelige når hele det nøyaktige innholdet må gjengis, særlig når konteksten er lang. Og det er helt forståelig. Men akkurat i dette tilfellet var det uakseptabelt, fordi vi trenger innholdet nøyaktig ord for ord – uten sammendrag og uten at noe av originalinnholdet utelates. Ingen detaljer kan gå tapt.

«Ja»-delen var selvfølgelig at modellen var svært god til å forstå semantikken og strukturen i de oppdelte delene. Så lenge den ikke nekter å gjengi det nøyaktige innholdet. Pokker også:/

Hvordan kan vi da utnytte det en LLM er god til, uten å være avhengige av det den gjør upålitelig? Vi vendte oss til vår gode, gamle venn: kode (les: en egendefinert Python-funksjon). Og en Pydantic-modell som ikke kunne vært enklere. Her er løsningen:

  • Gå gjennom avsnittene samtidig som du opprettholder en gjeldende logisk del

  • For hvert avsnitt spør du LLM-en: Hører dette avsnittet til den gjeldende logiske delen? Svar ja eller nei (i samsvar med Pydantic-modellen).

  • Hvis svaret er ja, legger du avsnittet til delen. Hvis svaret er nei, lagrer du den gjeldende logiske delen fordi den er ferdig, og oppretter deretter en ny med avsnittet.

Diagram som viser når ting blir mer kompliserte.

Dette bruker selvfølgelig litt flere tokener enn én enkelt gjennomgang av hele innholdet. Men i dette tilfellet, der det er aller viktigst å bevare innholdet nøyaktig, var det vel verdt den beskjedne ekstrakostnaden.

Dette er en svært enkel løsning, men den følger et viktig prinsipp: Når vi må være grundige, bør vi ikke stole utelukkende på LLM-er, ettersom de tross alt er probabilistiske.

Egendefinert kode og egendefinerte funksjoner samt Pydantic-modeller kan brukes til å oppnå forutsigbare og pålitelige resultater, samtidig som vi utnytter egenskapene til LLM-er fullt ut.

Oppsummering

Å bygge en generativ KI-løsning er like mye en utviklingsutfordring som en KI-utfordring. Vi håper disse eksemplene har inspirert deg til å ta fatt på dine egne, unike utfordringer. Hvis du vil lese mer om utviklingsorienterte løsninger for generativ KI, kan du se blogginnlegget vårt om utforming av ruterbaserte agentbaserte systemer.

Forfatter

Cynthia Yu