Галоўная / Артыкулы

Чаму RAG ламаецца ў прадакшне: разбіўка, эмбедынгі, пошук і ацэнка

Чаму RAG ламаецца ў прадакшне пасля дэма: межы фрагментаў, сляпыя плямы эмбедынгаў, ліміты top-K, састарэлыя даныя і этап ацэнкі, які каманды прапускаюць.

Выдавец: REACTAPP.TOP10 хв чытання2199 слоў

Крыніцы: RAG Without the Hype: What Retrieval-Augmented Generation Actually Takes to Work in Production

Стандартныя інструкцыі з RAG выконваюцца па простай схеме: завантажваецца PDF, які праздзеліваецца на фрагменты, гэтыя падключаюцца і зберагаюцца ў векторной базе дадзэнняў; пасля чаго запыт корыстніка таксама падключаецца, паўтараецца з адпаведнымі фрагментамі і вставляецца ў запрошэнне. Адпаведны адказ прыходзіць точны, і здаецца, што складная частка вядома.

У рэальных умовах все інакш: запыты часта абыходзяцца калькамі дакументаў, а значыцьцяя кантэксту зношваецца на межах фрагментаў. Сістэма знаходзіць фрагменты, якія за значэнням блізкія да запыту, але неадэкватныя з точкі зору кантэксту — у іх ёсць правыя словы, але неправы сэнс. Мадэль пасля чаго дае адказ на аднойчынцы, апыляючыся на стары, супераспакоўваючы матэрыял або на фрагменты, у якіх не хватае важлівых дэталяў, якія знаходзіліся за тры рэчэнні ад межы фрагмента. Найгорэе тое, што адказ здаецца правым, і ніч не свідчыць пра тое, што працэс пошуку праходзіў некорэктна.

Дэманстрацыю RAG лёгкая стварыць, але версію, якая надзеясна працаваць з рэальным корпусам дакументаў і рэальнымі корыстнікамі, — ні. Гэтыя разлікі выступаюць з трохоў мест: як дакументы дзельняцца на часткі, як эмбеддынгі разумеюць нюансы і як механізмы пошуку выбіраюць, што ляжыць у межах рэлевантнасці. Кожны з гэтых аспектаў мае спосабы збою, якія лёгкая працягнуць і якія каштуюць дорага пад час дыбаггірання пасля запуску.

Дзельненне на часткі: дзе вытыкаецца інфармацыя

По умове навучальнага матэрыялу выкорыстоўваецца рэкурсыўнае дзельненне на сімвалы з фіксаваным размерам і певным перакрыццем.

from langchain.text_splitter import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,
    chunk_overlap=200
)
chunks = splitter.split_documents(documents)

Гэта дастычна для дэманстрацыі. Яна не працюе, калі структура дакумента несе значэнне, якое не бераецца за ўвагу пад час пісання колькісці знакоў, і гэта выражаецца трохма распашчытнымі спосабамі.

  • Разлік кантэксту. У спецыфікацыі можа быць напісана, што максымальная падтрымваная колькісць знакоў — 500, і што гэты ліміт дыяўаць толькі ў рэжыме высокай доступнасці з актывацыяй настаўкі асновнага перэхаду. Калі колькісць знакоў досягае 1 000, гэта чысло можа апынуцца ў аднай частцы, а умова — у іншай. Людзі, якія спытваюцься пра ліміт высокай доступнасці, атрымваюць ту частку, дзе напісана 500 без жадных дапаможных пазначэнняў. Адказ існуе ў корпусе, але разлік частак нарадзіў перэрыв у зв’язку.
  • Загаловкі, які засталі самі. Загаловак уроку, напрыклад «Канфігурацыя безпекі», і першы параграф пад якім напісана, што для кожнага з’ўязку неабходны TLS, можу опінуцца ў разных частках. Сістэма, якая знаходзіць параграф, не мае інфармацыі пра загаловак, і структура рэчы не фіксуе, што яна належыць да налаштаванняў безпекі.
  • Зламаныя табліцы і спісы. Раздзелвачы фіксаванага размеру часта розрываюць табліцу посерэд рэчкі або спіс посерэд элемента. Загалоўкі столбцоў у одной частцы і даныя ў іншай не ўможна прачытаць окрэма.

Часткавае раздзеленне дакумента на высокі ступені якосты зазвычай выкарыстоўвае тры тэхнікі.

Раздзеленне з урахоўваннем структуры практыкуецца па сэрцах самага дакумента — загаловках, параграфах і роздзелах, а не па колькасці сімвоў. Markdown можна раздзеліць па загаловках другаго аб трэцьяга рангу, а HTML — па яго загаловковых, параграфовых аб роздзельных тагах.

from langchain.text_splitter import MarkdownHeaderTextSplitter

headers_to_split_on = [
    ("#", "document_title"),
    ("##", "section"),
    ("###", "subsection"),
]

splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers_to_split_on)
chunks = splitter.split_text(markdown_document)
# Each chunk now includes header metadata: {"section": "Security Configuration"}

Узбагачэнне кантэкста-родніка зберагае разам з кожным фрагментам шлях заголовка, які дае да яго доступ. Калі фрагмент атрымліваецца, у запрошэнні практыкавання паказваюцца назвы раздзелаў і падраздзелаў па боку тэксту, чым модэлю даёмаеся структура, якой не хацяць у просторэ эмбедынга.

Разбіўка на фрагменты-роднікі і-дзецей выкарыстоўвае маленькія фрагменты для точнасці атрымлення і большыя фрагменты-роднікі, якія іх маюць. Атрымленне падбірае маленькі фрагмент, а запрошэння практыкавання — фрагмент-роднік. Маленькія фрагменты болей точна імбеддуюць конкрэтныя ідэі, тады як фрагменты-роднікі супляюць абакануючую інфармацыю.

# Small child chunk for retrieval
child_chunk = "The maximum supported value is 500."

# Parent chunk injected into prompt when child is retrieved
parent_chunk = """
## High Availability Configuration
When operating in high-availability mode with primary failover enabled,
the following limits apply:
- Maximum supported value: 500
- Minimum heartbeat interval: 100ms
- Failover timeout: 30 seconds
"""

Этакі падходы касуюцца большых віткоў на рэалізацыю і зберагачванне, пры тым яны даюць значна лепшыя адпаведзі на запытанні, якім патрэбны кантэкстны даныя, якія стосуюцца большасці рэальных запытанняў.

Эмбедынгі і меры сэмантычнай супадання

Эмбедынгі размешчаюць тэкст у вектарнай прасторы, дзе сэмантычна супадаючыя значэнні знаходзяцца падалёку адзіна ад другога, а працэс адналёгчэння вяртае фрагменты, якія знаходзяцца найбліжэй да запытання. Гэта працуе тады, калі сэмантычная блізкасць можа служыць адпаведным показнікам рэлевантнасці. Гэта не працуе, калі значэнні супершчыцяюцца.

  • Адмова. Звесткі, якія пазначаюць, што сістэма не падтрымлівае IPv6, і тые, якія пазначаюць, што яна падтрымлівае IPv6, атрымліваюць практычна ідэнтычныя вектары, адтуды што обе гавораюць пра падтрымку IPv6. Адмова ледзь вплывае на вектар. Запытанне пра IPv6 можа адналёгчыць як оба фрагменты, так і неправільны, і модэль должна прыміроўваць суперсчыцанні або адпаведзець на запытанне на адной з неправільных частак, не з’явіўшыся пра гэта.
  • Несасоўнасць слоўніка. Паўтаральнік пісае «login», а ў дасягліх дапамогах указваецца «authentication» і «sign-in». Зазвычай гэтыя тэрміны знаходзяцца дастаткова блізка, ўсё ж іноды вылучаюцца экстрэмальныя сітуацыі, калі тэрмінологія паўтаральніка не збігаецца з тым, што напісана ў дасягліх дапамогах.
  • Канкрэтнасць. Запыт пра тое, як налаштаваць пул з’яўленняў, і запыт пра стандартны таймаут пула знаходзяцца дужа блізка адзін да другога, таму і той, і другі вяртаюць той самы набор фрагментаў. Калі значэнне таймауту знаходзіцца ў аднам параграфе сярод калькі параграфаў пра пулы, найкрашчы фрагмент не завжды стае першым.

Тры способы рашэння дапамагаюць падрыхтавацься да гэтых проблем.

Гібрыдныя пошук спалюе ўзаемна ўжоўванне ўплотненага адналёгчэння, якое фіксуе значэнне, і розрозненага адналёгчэння, такога як BM25 або TF-IDF, якое фіксуе точныя тэрміны. Запыт пра номер версіі, ключ налаштавання або код каштоўкі лепшая адпавядае за дапамогою падабенства ключоў, чым за дапамогою сэмантычнай супадання. Багаты вектарныя базы дадзеных сёньня прыступнае гібрыдныя пошук як стандартную функцыю, а прыклад ніжэй выкорыстоўвае сумешчанне розрозненага і уплотненага адналёгчэння ў Qdrant.

from qdrant_client import QdrantClient
from qdrant_client.models import SparseVector, NamedSparseVector

# Many vector databases now support hybrid search natively
# This example uses Qdrant's sparse + dense hybrid search
results = client.query_points(
    collection_name="docs",
    prefetch=[
        models.Prefetch(
            query=sparse_query_vector,  # BM25 sparse vector
            using="sparse",
            limit=20,
        ),
        models.Prefetch(
            query=dense_query_vector,   # Embedding dense vector
            using="dense",
            limit=20,
        ),
    ],
    query=models.FusionQuery(fusion=models.Fusion.RRF),  # Reciprocal rank fusion
    limit=5,
)

Розшырэнне запіту паўнейшае запіт пры тым, як яго уключаюць. Запіт пра аўтэнтыфікацыю можна расшырыць за дапамою тэрмінаў урочыстае заходжэнне, прапуск, аўтэнтыфікацыя і перакананне ў аідэнтычнасі, чыяму ён стае часткай болей насыценага прастору. Невялікі вызов LLM можа стварыць разныя варыяцыі, або статычныя карты сінонімав можа пакрыць тэрміны конкрэтнай галіны.

Выбір модэлю для уключэння таксама заслуговае на увагу. Багатыя команды зазвычай выбіраюць загальныя модэлі OpenAI, такія як text-embedding-ada-002 або text-embedding-3-small. Для медычных, правовых або тэхнічных корпусаў модэлі, прызначаныя для конкрэтной галіны або настроеныя спецыяльна, часта параджаюць за канцэнтрацыяй. Калі працэс выкарыстоўвання інформацыі застаецца слабым нават пасля правільнага разбівання на часткі, корыстна ў порównанні альтэрнатыў на прыкладнай выборцы запытанняў.

Проблемы выкарыстоўвання інформацыі, якія з’яўляюцца у масштабных сістэмах

Чанкінг і эмбедынгі вялікай часткай вялікіяюць, чы можна знайсці контэнт, а выкарыстоўванне методаў адзыскання вялікай часткай вялікіяюць, чы ён быў знайдзены для заданага запіту. Дзеяныя проблэмы прыходзяць да віда толькі пры вялікім корпусе даных або разнарадзітых запытах.

Выбір K. Большасць сістэм адзысквае К найболей схожых чанкаў па ступеню схожасці і падставляе ўсе іх у запыт. Маленькое K не можа прадаст всю неабходную інфармацыю, калі на адказ патрэбна падтрымка калькі чанкаў. Вялікое K запоўняе вікно кантэксту маргінальнымі чанкамі, якія слабяць сігнал, а ў дужа дзяўгіх запытах могу вывесці сапраўдзіва значымы чанк за межы эфектывай увагі модэлю.

Практычным падчынам ёсць захопленне занадта велікай колькасці рэзультатаў, напрыклад, 20 чы 30 кандыдатаў, а пасля ўпорадакаванне іх занова. Крос-кодэр аб’еднвае запіт і фрагменты тэксту і безпасяродна ацэнюе ўзаемную адноснае значэнне, замест таго каб паўставаць на аднаковасць двух незалежна вычысленых эмбеддынгаў. Гэта медленней, калі таму, што эмбеддынгі фрагментаў не можна вычысляць загадзя, але гэта набагато лепшая аддзеляе рэлевантныя рэзультаты ад тых, якія майжэ рэлевантныя. Швайныя апрыксіматыўныя методы скарочваюць колькасць варыянтаў, а крос-кодэр выбірае фінальны набор.

from sentence_transformers import CrossEncoder

reranker = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2")
# Retrieve broad candidate set
candidates = vector_store.similarity_search(query, k=20)
# Re-rank with cross-encoder
pairs = [(query, chunk.page_content) for chunk in candidates]
scores = reranker.predict(pairs)
# Sort by re-ranking score, take top 5
reranked = sorted(zip(scores, candidates), reverse=True)[:5]
final_chunks = [chunk for _, chunk in reranked]

Застарэласць. Хранальнік вектараў, які быў створаны шэсць месцаў тыму, зберагае дакументы, якія з тэх падчас змяніліся, а сам хранальнік пра гэта не ведае. Працэс выкарыстоўвання вярта застарэлы фрагмент з высокай паверыльнасцю, і мадэль адпавядае на адповіднае запытанне на адной з гэтых версій. Рашэннем являецца фільтрацыя метаданых: трэба зберагаць дату пасляльгвеннага змены кожнага фрагмента і фільтрацыяяго па дыяпазоне дат перад або пасля пошуку за сэмплінарнасцю. Для актываўна падтрымванай дакументацыі фрагменты, якія старэйшыя за выбраны порог, адхоўляюцца або стаюць у ніжэйшай прапорцыі.

from datetime import datetime, timedelta

# Filter to chunks updated in the last 90 days
cutoff = datetime.now() - timedelta(days=90)

results = vector_store.similarity_search_with_filter(
    query=query,
    filter={"last_modified": {"$gte": cutoff.isoformat()}},
    k=10
)

Неправдзіва абсалютна паверы. Мадэль дае адказ на адно, што яна прымея, нават калі атрыманыя фрагменты не стосуюцца запытання. Яна не ведае, што якосць атрымання інфармацыі была нізкая. Гэта найбольш параджуе довер'е корыстніка, калі адказ выглядае аўтарытэтным, хоць і ў кладзе. Рашэннем яўляецца наявнасць контроля адносу прыманых дадзеных да запытання пры стварэнні адказа. Можна ацэніць атрыманыя фрагменты і не ствараць адказ, калі найлепшая ацэнка будзе ніжэй за заданы порог, альбо паведаміць модэлі ў сістэмным запрошэнні, каб яна сказала, што у яе недастаткова інфармацыя, а пасля пераканацца, што яна правільна выкарыстоўвае гэты спосаб выйсця з сітуацыі.

SYSTEM_PROMPT = """
You are a documentation assistant. Answer questions using only the provided context.

If the context does not contain enough information to answer the question confidently,
respond with: "I don't have sufficient information in the available documentation to
answer this question reliably."

Do not speculate or answer from general knowledge when documentation context is insufficient.
"""

Ацэнка, якія працоўныя групы прыхоўляюць

Сістэма RAG без ацэнкі мае невядомую якосць. Яна можа добра справляцца з дзеякімі запітамі, а паслаба — з іншымі, і ўзнікнуць можа толькі за дапамогай скаргі корыстувальніка. Ацэнка складаецца з трохоў частак.

Якосць выкарыстоўвання інфармацыі. Для тэстовых запітоў з вядомымі правільнымі джэрэламі дакументаў памерыце, як часта правільны документ з’яўляецца сярод першых K выкарыстоўваных дакументаў. Параметр «памяць» на 5 запытанняў паказвае, чы роўна правільны фрагмент знаходзіцца сярод першых пяці рэзультатаў, а параметр «серэдній взаімны ранг» паказвае, на якім месцы ён знаходзіцца. Перад запускам следуець працаваць з тэстовым наборам з 100–200 рэпрэзентатыўных запытанняў.

Адпаведнасць адпаведзі да кантэксту. Чы генераваная адпаведзь відпавядае выкарыстоўванаму кантэксту, не дадаючы тых твэрджэнняў, якіх няма ў фрагментах? Схема, у якой LLM выступае як суддзя, можа оцаніць гэта, прымеўшы запыт, выкарыстоўваныя фрагменты і адпаведзь, а таксаваўшы, насколькі добра кантэкст падтрымвае адпаведзь.

FAITHFULNESS_PROMPT = """
Given the following context and answer, rate whether the answer is fully
supported by the context on a scale of 1-5.

Context: {context}
Answer: {answer}

Score (1=not supported, 5=fully supported):
"""

# Automate across your evaluation set
scores = []
for query, context, answer in eval_set:
    score = judge_model.complete(
        FAITHFULNESS_PROMPT.format(context=context, answer=answer)
    )
    scores.append(score)
print(f"Average faithfulness: {sum(scores)/len(scores):.2f}")

Правільна працэўнасць ад пачатку да канца. Для запытанняў з вядомымі адпаведзямі, чы гэтая сістэма дае правільнае адпаведзенне? Гэта самая складная частка для автаматызацыі і найболей значамая. Нават ручна створаны набор з 50 запытанняў з перакананымі адпаведзямі, які запускаецца перад і пасля кожных змян у настройках фрагментавання, эмбеддынгу чы адзыскві, дапамагае выявіць рэгрэсіі.

Набор для ацэнкі функцыонуе як набор тэстаў на рэгрэсію. Новая мадэль эмбеддынгу, іншы размер фрагментаў чы зменены прыказ сістэме павінны кожны з іх даць вядомыя змены ў рэзультатах. Калі змена падымае сярэжны ступень правільнасці, але зніжвае рэкалі, чы іншае, такі компрэс павінен быць свядомай рашынкай, а не тыхамі размешчэннем.

Цэлы працэс — гэта продукт

Працэс навчання складаецца з чатырох крокаў і не мае механізмаў карэкцыі бягу. Працэс выкарыстоўвання мае шасць крокаў, у кожным з яых выконваецца выявленне бядаў, пры ўсіх кроках додаюцца метаданы, а перад кожным выпускам адбываецца ацэнка.

Бяды ў такім прыеме ёсць прэдскладнымі. Разбіўка на часткі без урахоўвання структуры ведае пра пакінуты кантэкст на краях. Спосаб адшукання на адной лініі працы пропускае точныя зусімтворы і паслаба працюе з паўтарэннямі. Адшукання без пераранжавання вяртае спрасаваныя рэзультаты, якія маскуюць найлепшыя варыянты. Генераванне без контролю за рэлевантнасцю дае неправільныя, але впэўненыя адпаведзі на адносна слабы кантэкст.

Нічога з гэтага нельга ўзгадзіць, і кожная змяна мае вядомую цэну. Разбіўка на часткі з урахоўванням структуры вымагае розумення фармата дакумента. Гібрыдны пошук означае выкарыстоўванне як ўжымлёных, так і розрэджаных індэксаў. Пераранжавання адзначаецца затрымкай у працэсе адшукання. Ацэнка вымагае стварэння і падтрымкі тэстовага набора.

Команды, у якіх сістэмы RAG працююць належначы, прымуць такія рашэнні намеравана ўсё раней, чым запускаць продукт, а не у адпаведнасці да некорэктных адказоў. У навучальных матэрыялах прыхоўдзяюцься да простыях частак, таму што дэма іх не патрабуе, але надзеяны сістэма — так.