Hlavní navigace

Praktické příklady přizpůsobených řešení RAG

Příklady z praxe ukazují, jak přizpůsobené systémy generování rozšířeného o vyhledávání řeší složité problémy s podnikovými znalostmi.

RAG dnes někdy nemá nejlepší pověst. Někteří lidé ho považují za naprostou banalitu (začít je snadné, škálovat už méně), jiní zase za překonaný „agentními systémy“ (které se při bližším pohledu v mnoha případech velmi rychle začnou RAG nápadně podobat…).

V tomto článku si na jednom či dvou praktických příkladech ukážeme, jak řešíme některé běžné problémy, konkrétně:

  • Práce s kombinací textových a číselných dat a proč si s ní naivní RAG neporadí: klíčová slova se překrývají a čísla sama o sobě nenesou žádný sémantický význam.

  • Proč pomáhá navrhovat embeddingy založené na souhrnech: pro každý úsek vygenerujte krátký popisný souhrn a embedding i vyhledávání pak provádějte nad ním.

  • Jak generovat kontextové souhrny: zahrňte kontext nadřazeného dokumentu, aby bylo možné rozlišit podobně strukturované statistiky.

  • Kdy se spolehnout na kód a modely Pydantic: pokud záleží na doslovném znění, spojte vlastní kód nebo model Pydantic s voláními LLM, abyste zajistili spolehlivost.

Tvorba vlastních řešení RAG

Základy

Systémy RAG slouží jako základ všeho možného, od chatbotů zákaznické podpory po interní znalostní asistenty.

Na pozadí obvykle probíhají tyto kroky:

  1. Rozdělení zdrojových dokumentů na úseky

  2. Převedení každého úseku na embedding ve vektorovém prostoru

  3. Vyhledání K nejrelevantnějších úseků při zadání dotazu

  4. Vygenerování odpovědi na základě těchto úseků

Oblíbené nástroje, jako jsou LangChain, LlamaIndex a Filestore od OpenAI, tyto kroky téměř úplně zjednodušují. V reálných datových tocích ale narazíte i na jiná data než souvislý text a základní RAG s nimi může mít potíže. V následujících částech si ukážeme konkrétní příklady problémů s daty a s rostoucí složitostí budeme postupně vytvářet jejich řešení.

Když se situace začne komplikovat

  1. Když vaše data nejsou jen textová (což je ve skutečnosti docela běžné)

Podívejme se na následující úsek dat z herního prostředí:

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

Embeddingy fungují díky naučeným vztahům mezi slovy, které vycházejí ze sémantického významu a gramatiky. Ve výše uvedených datech se mísí text s čísly, která mimo tento konkrétní kontext nemají ke slovům žádný vztah. Dalo by se tedy říct, že tento úsek dat je v podstatě kombinací několika popisných slov a náhodných čísel.

Kdybychom měli pouze tento typ dat, nepředstavovalo by to problém. Stále bychom je mohli vyhledat pomocí embeddingů několika dostupných popisných slov (nebo jednoduše použít text-to-SQL). Co když je však tento úsek skrytý mezi mnoha úseky souvislého textu, ve kterých se objevují stejná slova? Například:

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

Představme si, že chceme vyhledat odpověď na otázku „What is the attack range with Draconic Ascension?“ Relevantní úsek se nám s velkou pravděpodobností nepodaří najít, protože zanikne v šumu ostatních úseků obsahujících stejná klíčová slova.

Jádrem problému je, že tyto úseky dat nedokážeme dobře rozlišit, přestože ke stejnému tématu obsahují různé typy informací. Mohli bychom je nějak obohatit nebo vylepšit? Samozřejmě že ano:smile:

  1. Obohaťte svá data jejich shrnutím. Ano, čtete správně

Místo přímého vytvoření embeddingu samotného úseku můžeme nejprve vygenerovat souhrn popisující obsah dat a embedding i vyhledávání pak provádět nad tímto souhrnem. Ve fázi generování bychom nadále používali původní data propojená se souhrnem.

Pro dva výše uvedené příklady úseků bychom tedy vygenerovali například tyto souhrny:

  1. Statistiky dosahu, rychlosti a poškození útoku (ve výchozím stavu a s Draconic Ascension).

  2. Popis a podrobnosti schopnosti Draconic Ascension včetně podmínek aktivace, vizuálních efektů a příběhového pozadí.

Následně upravíme také dotaz, aby se souhrnem lépe „ladil“. Například bychom otázku „What is the attack range with Draconic Ascension?“ změnili na „What is the statistics of attack range with Draconic Ascension?“ To je obzvlášť důležité, pokud vyhledávací dotazy zadávají lidé mimo technické obory v ~~„neformálním“~~ běžném lidském jazyce. Koneckonců nemusí vědět ani řešit, jak RAG funguje, aby maximalizoval přesnost a úplnost vyhledávání.

Diagram znázorňující, kdy se situace začne komplikovat.

  1. Nevytrhávejte věci z kontextu (to obecně platí i v životě)

Dalším scénářem je práce s velkým množstvím téměř totožných úseků dat, například těchto:

Plain Text

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

Pokud bychom zůstali u stejného přístupu, představme si dotaz „what is character X’s attack range?“ S právě vygenerovanými souhrny bychom se pouštěli do tipování založeného na náhodě, protože by si byly také velmi podobné. Jak je tedy můžeme rozlišit?

Jednoduchá odpověď zní: doplnit kontext. Do úseku dat bychom mohli jednoduše přidat odkaz na jeho nadřazený dokument, v tomto případě například {”character”: “X”}. Díky tomu bychom dokázali přesně vyhledat správná data pro postavu X, i kdybychom měli stejná data také pro postavy Y a Z.

Lepším a univerzálnějším přístupem by však bylo vygenerovat kontextový souhrn úseku. Jinými slovy, místo generování souhrnu pouze ze samotného úseku dat bychom mohli předat jeho nadřazený dokument i daný úsek a vygenerovat obecný kontextový souhrn. V něm bychom popsali, jak úsek zapadá do nadřazeného dokumentu, například:

  1. Tento úsek obsahuje podrobné statistiky … pro postavu X. Do celého dokumentu úsek zapadá tím, že ukazuje silnou stránku postavy X v rychlosti útoku…

  2. Tento úsek obsahuje podrobné statistiky … pro postavu Y. Do celého dokumentu úsek zapadá tím, že ukazuje vylepšené statistiky postavy Y při použití její speciální schopnosti…

  3. Tento úsek obsahuje podrobné statistiky … pro postavu Z. Do celého dokumentu úsek zapadá tím, že ukazuje statistiky postavy Z, díky nimž se dobře hodí do role tanku v týmových zápasech…

Tato metoda (částečně inspirovaná společností Anthropic) může u výše uvedeného příkladu působit přehnaně. Je však velmi účinná u úseků, které by bylo možné „mimo kontext“ chybně vyložit, a navíc nabízí jednotný přístup použitelný pro všechny úseky, takže datový tok zůstává technicky přehledný.

Diagram znázorňující, kdy se situace začne komplikovat.

  1. Když potřebujete být ~~posedlí kontrolou~~ důslední

Obvykle dostáváme data vcelku a pro systém RAG je rozdělujeme na úseky. V tomto příkladu si ukážeme něco trochu jiného: data už jsou rozdělená, ale na nevhodné úseky. Jde o náhodné části jednoho logického úseku, které je potřeba znovu spojit. Logickým úsekem se rozumí část obsahu, která přirozeně patří k sobě, například pododdíl dokumentu nebo souvislý odstavec.

Diagram znázorňující, kdy se situace začne komplikovat.

Při prvním pokusu jsme všechna tato data předali v jednom volání LLM a požádali ho, aby je podle svého uvážení seskupil a vrátil spojený obsah. LLM by v tom měl být docela dobrý, že? Ano i ne.

Při této i několika dalších příležitostech jsme zjistili, že LLM mají sklon si práci zjednodušovat a nejsou spolehlivé, když požadujete úplný a přesný obsah, zvlášť u dlouhého kontextu. Což dává naprostý smysl. Pro tento konkrétní případ použití to však bylo nepřijatelné, protože jsme potřebovali přesné znění slovo od slova – bez shrnutí a bez vynechání jediné části původního obsahu. Nesmíme přehlédnout žádný detail.

Tou kladnou stránkou samozřejmě bylo, že LLM skvěle porozuměl významu a struktuře rozdělených úseků. Tedy pokud zrovna neodmítl přesné znění zopakovat. Kruci:/

Jak tedy využít silné stránky LLM a zároveň se vyhnout oblastem, ve kterých není spolehlivý? Obrátili jsme se na starého dobrého přítele: kód (rozuměj vlastní funkci v Pythonu). A také na maximálně jednoduchý model Pydantic. Řešení vypadá takto:

  • Procházejte jednotlivé oddíly a průběžně uchovávejte aktuální logický úsek

  • U každého oddílu se zeptejte LLM: Patří tento oddíl do aktuálního logického úseku? Odpověz ano, nebo ne (podle modelu Pydantic).

  • Pokud ano, připojte oddíl k úseku. Pokud ne, uložte dokončený aktuální logický úsek a tímto oddílem začněte nový.

Diagram znázorňující, kdy se situace začne komplikovat.

Ve srovnání s jedním průchodem celého obsahu zde samozřejmě spotřebujeme o něco více tokenů. Pro tento konkrétní případ použití, kdy je nejvyšší prioritou zachovat přesný obsah, se však menší dodatečné náklady rozhodně vyplatily.

Jde o velmi jednoduché řešení, které se však řídí důležitou zásadou: když potřebujeme důslednost, neměli bychom se spoléhat pouze na LLM, protože jsou ze své podstaty pravděpodobnostní.

Vlastní kód, funkce a modely Pydantic nám umožňují dosáhnout předvídatelných a spolehlivých výsledků a přitom naplno využít možnosti LLM.

Závěrem

Vytvořit řešení s generativní AI je stejnou měrou technická výzva jako výzva v oblasti AI. Doufáme, že vás tyto příklady inspirovaly k řešení vašich vlastních jedinečných problémů. Další informace o technicky orientovaných řešeních s generativní AI najdete v našem článku o návrhu agentních systémů založených na směrování.

Autor

Cynthia Yu