Гібрыдны RAG для юрыдычных тэкстаў: разбіўка, BM25, BGE і пераранжаванне
Юрыдычны RAG-канвеер на Канстытуцыі Індыі: разбіўка parent-child, пошук BM25 разам з BGE, зліццё рэйтынгаў, пераранжаванне крос-энкодэрам і пашырэнне запытаў, правераныя на бенчмарках.
Крыніцы: Beyond Basic RAG: Building a Production-Style Legal Research Assistant (Part-I)
Прававыя тэксты ўскладнююць работу будзь-якай сістэмы пошуку. Возьмем Канстытуцыю Індыі: ў яй сотні статэй і пунктав, а таксама дапаможныя положэнні, праграмы, параграфы змян, табліцы і сетка перакіраных апісанняў. Адпаведны адзін параграф можа мець адзін рэчэнак, але правільнае яго чытанне можа залежаць ад суседней статэй, адпаведнага выключэння чы іншага положэння, на якое ён супаказвае. Таму корыстная сістэма павінна знаходзіць баланс межаў тачнасці і кантэксту, знаходзячы точны параграф, не аддзеляючы яго ад тэксту, які надае яму значэння.
Большынства пачатковых рэштрыктураў для генеравання з дапамогай адзысквання інфармацыі (RAG) следуюць простам алгорытму: выкарыстоўваецца тэкст з PDF, які падзеляецца на фрагменты фіксаванага размеру, яны вбудовваюцца і вяртаецца найбліжэйшыя паспадобныя рэзультаты. Гэта прыемны пачатковы пункт, але у працоўных сістэмах зазвычай трэбуе большага. Яны должны захаваць структуру дакумента, падтрымляць як точнае, так і сэмантычнае адзыскванне, перэранжаваць кандыдатаў, вярнаваць абакануючы кантэкст, праследаваць кожны рэзультат да яго выхаднага крыніцы і пераканвацца, чы рэальна былі адзысканы неабходныя доказы.
У данай статыі опісваецца научны праект, які відтварае такі працэсы працы у стыле прыменення тэхналогій у практыцы у форме, якая можа быць аналізавана паэтапна, выкорыстоўваючы Канстытуцыю Індыі як першы корпус дадзеных, адтолькі ўжо таму, што ўсунутая ў яй структура адкрывае множыцю слабасей звычайных вектарных пошукавых сістэм. Гэты праект не прынесены як готовы продукт. Гэта спроба выявіць, як разныя складовыя стандартнага прайсупутніка RAG работаюць разам, дзе воны выклікаюць проблемы, і як ацэнка можа парадкаваць наступныя рашэнні па дызайну.
1. Прыем дадзеных: ад правовага PDF да структураванага кантэнту
Перш чым можна будзе расследзіць варыянты фрагментавання, усадківання або выкарыстоўвання методаў адзыскання інфармацыі, выхідны PDF павінен стаць такім кантэнтам, які програмнае забезпечэнне можа надзейны чынам обрабатваць. PDF фіксуе выгляд дакумента, але не обавязкова тое, як програма павінна яго чытаць. Загаловкі, клазулі, прымечанні, спісы, табелі і межы сторонак можуць быць зрозумелыя для чалавека, але падчас простага выкарыстоўвання методаў адзыскання інфармацыі ператвараюцца на адсутныя ў зв’язку фрагменты тэксту. Калі такая структура знікне на этапе прыемкі дакумента, то пазнейшыя этапы прыведуць да стварэння фрагментаў з неправым порадкам чытання, адсутнымі загаловкамі або зламанымі табелямі.
У праекце быў выкарыстоўваны MinerU для ператварэння Констытуцыі у фармат Markdown, захаваўшы пры гэму важлівую інфармацыю пра макет. Яго выбралі таму, што, за словамі автара, ён болей надзейна обрабатваў табелі і спісы, чым іншыя падходы да адзыскання інфармацыі. Метай не было проста скарыстацца кожным словам, а захаваць зв’язкі межы словамі. Выкарыстоўваны кантэнт захаваў:
- як варуецца загаловкі,
- дзе пачынаецца і заканчваецца кожны абзац і клазула,
- спісы з крапкамі і номерамі,
- таблыцы,
- якае сторана ўзяла кожны фрагмент, і
- падтэксты разам з структурамі навакола іх.
Markdown падходзіць для гэтай ролі як проміжны фармат. Людзі можаць чытаць яго пад час ручнай перাপтывання, а ён мае структурныя знакі, якія можа обрабоцаваць Python. Загаловак у фармаце ## Фундаментальныя правы пазней можа служыць кераванням для стварэння вышэйшых частак, тады калі спісы і таблыцы можна ўпісаць і адночасна расследзіць як окрэмыя блакі.
Экстракцыя была адзейнавана ў Google Colab, а не на локальным камп’ютары. OCR і аналіз макета можу быць выкарыстоўваныя з вялікімі зусіллямі, боўш тады, калі мова йдзе пра довгі документ, заполнены табліцамі і складным фарматаваннем; ноутбук з обмежаным апаратным забезпечэннем меў труднасці з выкааннем такой працы. Google Colab запрошвае адособленую срэду з моцнейшым вычыслювальным засобам, таму яго варта рассмотрзець на гэтым этапе, калі ваша локальная апаратная частка ёсць скромной. Команда, якая была выкарыстоўвана ў зошыцы, была такой:
!mineru -p c.pdf -o output --dump-content-list -b pipeline -l en
2. Разбяване на частыні
Колкісна структура Markdown дозволяў роздаваць Констытуцію на частыні. Звычайна відповідзь — фіксованого розмеру частыні: роздавання пасля певноў колькасці сімвоў або токенів, часта з певнім перэкрыванням. Гэта проста методыка, якая добрэ працуўае для однаковіх тэкстыв, але юрыдычні документы організовуюцца за змыслом, а не за довжыною. Фіксовані межы можуць:
- відокрэмляты заголовок ад статты під нім,
- розрэзаты клauseвець, які яго опісуў,
- роздыляты спіс на частыні посерэд яго элементаў,
- роздаваты табліцю на неповні фрагменты, і
- зліты неповязані статты лішэ тому, што яны пасуюць у одне токенне вікно.
Большы частыні зберагаюць більшэ кантэксту, але робляць пошук менш точнім. Меншы частыні крашчэ відповідаюць, проце важліві навколішні умові можуць знікаты. Тому праект вікорыстав дварывневу стратэгію:
Structured document
↓
Heading-aware parent chunks
↓
Semantic child chunks
Чаму дапомагае два рэвэля
Пошук адбываецца сярод маленькіх дзецей, таму ўжо ў меры ўсьмешаныя даныя даўаюць BM25 і ўзбуджваюць чыстыя сігналы для адзыскання. Пасля таго, як выбіраецца дзецё, parent_id, які ён несе, паказвае шлях да большага рэвэля-родзіцы:
Query
↓
Retrieve focused child
↓
Read parent_id
↓
Return complete parent context
Гэта дзеліць два абавянні. Часткі-дзеці забезпечваюць тачнае адзысканне інфармацыі, а часткі-бацькі — полнасць кантэксту.
Часткі-бацькі, якія ведаюць прызначэнне загаловка
Алгорытм стварэння частак-бацькаў спынаецца на двух элементах, якія ствараюцца пад час прыемы дадзэнняў:
data/processed/cr.md
data/extracted/auto/blockpage.json
Файл у фармаце Markdown зберагчыць чыставаны тэкст і яго відымую структуру. Файл blockpage.json зберагчыць арранжаваныя блакі лейауту, створаныя пад час выкарыстоўвання, укладаючы інфармацыю пра ранг загалоў. Markdown застаецца главным істочнікам матэрыялу, а JSON служыць арранжаваным структурным кансэльтантом для паўтарнага спаявання загалоў у фармаце Markdown з выкарыстоўваным докумэнтам. Адбудова выконваліцца ў аднам скрыпце:
code/chunking_parent.py
Алгорытм працюе за фіксаваным порадкам.
Шаг 1: запрацоўваець оба артыфакта. Скрыпт чытае адчыстаны Markdown і таксама файл JSON з блакамі макету:
md_text = MARKDOWN_PATH.read_text(encoding="utf-8")
with BLOCKPAGE_PATH.open("r", encoding="utf-8") as file:
blockpage = json.load(file)
З выкарыстоўваннем обох элементаў запобiegаецца таму, што апарат для разбівання будзе спрыяць розгляду Markdown як звычнаг, неструктураванага тэксту. Markdown задае контэнт, а JSON дапамагае падтрымліваць парадак загалоў, якія былі знайдзены падчас выкарыстоўвання.
Шаг 2: разбіць Markdown на блакі. Разлінаванне віконваецца за дапамою порожніх ліній:
for raw_block in md_text.split("\n\n"):
Кожэн блок потым класіфікуўцягаець у одну з кялькіскаўках:
heading
paragraph
list
table
superscript
blank
Тэкст, які пачынаецца з #, ## або іншага рангу загалоўка, напрыклад, вважаецца загалоўкай:
HEADING_RE = re.compile(r"^(#{1,6})\s+")
Спісакі, таблыцы і блокі з надзначкамі маюць своія правіла адказвання, які дазволяюць ўтрымваць іх у зв’язку з раздзелам, да якога яны належаць.
Крок 3: сачыце за актыўным заглавнікам. Калі з’являецца заглавнік, яго тэкст дадаўся да чырвонага heading_path. Скрыпт таксама шукае адпаведны выявлены заглавнік у blockpage.json, праходзячы па JSON последоўна і не перасканаваючы значынку зноў з самага пачатку:
def find_heading(heading_text: str):
nonlocal search_pos
for index in range(search_pos, len(blockpage)):
block = blockpage[index] if (
block.get("type") == "text"
and block.get("text") == heading_text
and "text_level" in block
):
search_pos = index + 1
return block
Сканування вперэд дозволяе зберыгаты заголовкі у іхньому первісному парадку і запобігае дублюванню ранішэ створэніх ідэнтычніх заголовків.
Крок 4: збірання пов’язаного кантэнту. Абзацы, спіскі, табліці і іншы блокі додаюцца до поточноі бацьківськоі структуры. Колі новій заголовок з’являецца пасля того, як основній тэкст ужо було зібрано, поточна бацьківська структура закрываецца, і почынаецца нова. Концептуально гэта віглядае так:
Heading
├── Paragraph
├── Clause
├── List
└── Table
↓
Parent chunk
5-й крок: прызначыць стабільныя метаданы. Кожны завершаны элемент-родык зберагаецца з последоватэльным ID, ID дакумента, шляхам загалоўкаў і цэлым тэкстам:
{
"parent_id": f"parent_{len(chunks):04d}",
"doc_id": DOC_ID,
"heading_path": heading_path.copy(),
"text": "\n\n".join(stack),
}
Спрасцаваныя элементы-роды выглядаюць так:
{
"parent_id": "parent_0164",
"doc_id": "constitution_of_india",
"heading_path": ["PART XII"],
"text": "# PART XII\n\nFINANCE, PROPERTY, CONTRACTS AND SUITS..."
}
Адзін або адзіная бацька/мама можа выкорыстоваць калькіляванні, звязаныя з кількома аспектамі, напрыклад:
PART XII — Finance
Article 264 — Interpretation
Article 265 — Taxes not to be imposed without authority of law
Article 266 — Consolidated Funds and public accounts
Article 267 — Contingency Fund
У працэсе было створана 333 абсалютных чака.
Семантычныя дзецінскія чака
Абсалютныя чака зберагаюць кантэкст цэлым, але дзеяныя з іх ёсць супэрвышокія для точнага падпадання, таму большыя з іх дзелююцца на меньшыя семантычныя дзецінскія чака. Гэта роба знаходзіцца ў зошыце:
code/semantic-children.ipynb
Тэксты з мерай павяліней, якія па крытэрыю прыблізнага порога у 400 токенаў у зошыцы, залічваюцца як адна ентытная дзеціна, таму што разбіўка чаго-то, якое і так дастаткова компактна, каб служыць адзінай цэлай, не прыносіць нічога. Большыя тэксты обрабоцваюцца за дапамой прыстрою для разбівкі, які вырахоўвае структуру і парабяліць значэнне.
Шаг 1: разбіць тэкст на блакі. Тэкст раздзеляецца на загаловыя часткі, параграфы, спісы і табліцы. HTML-табліцы адгукваюцца за дапамой свайго сябе прызначанага шаблона:
table_pattern = re.compile(
r"(<table[\s\S]*?</table>)",
re.IGNORECASE
)
Звычный тэкст дзеліцца на абзацы за дапамою порожніх ліній, а таксаваны ў разрады за дапамою загалоўкаў і знакаў спісаў у Markdown, таму прозу, спісы і таблы можна обрабляць окрэма, замест таго каб прыпускаць, што адна структура падходзіць для всего.
Крок 2: прыяўляйце загалоўкі да ўсёго тэксту, які яны апісваюць. Загалоўка ніколі не павінна стаць самастоятэльным элементам. Апарат дзелення тэксту врэменна зберагае кожную загалоўку і дадвае яе да наступнага блока:
if pending_heading:
block.text = pending_heading + "\n\n" + block.text
pending_heading = None
Адзінакоўвана дзеціна такім чынам застаеёцца з загаловкам, які ідэнтыфікуе яе тэму. Аб’еднаныя блакі спіску таксама групуюцца перад семантычным порэвняннем, што дапамагае зберагчы логічны парадак клазу і перыякаваных элементаў.
Шаг 3: порэвняйце суседзяючыя параграфы. Для суседзяючых блакоў параграфаў зошыт стварае вкладкі BGE:
embedder = SentenceTransformer(
"BAAI/bge-base-en-v1.5"
)
Потым зошыт вычыляе косінусную схожасць между кожным абзацам і абзацам, які знаходзіцца даўжэй пры ўсёй лініі:
similarity = cosine_similarity(
emb1,
emb2
)[0][0]
Калі два блакіты маюць даснае значэнне, яны можаць застацца ў там самым падблакіце, а падыш у сэмплярнасці сведчыць пра тое, што наступны блакіт запускае новую тэму. Настаўлены порог являе ся такім:
Similarity threshold: 0.40
Адзінаковасць — гэта толькі адна з прамаў. Таксама беруцца ў расчыт тып і размер блака.
Шаг 4: застосаваць правілы размеру і структуры. Стандартныя настройкі семантычнага чанкера ўключаюць:
Preferred minimum size: 800 characters
Maximum combined size: 1,600 characters
Tiny-block threshold: 150 characters
Блок можа быць дадзены нынешнему падрабочаму элементу, калі гэты падрабочы элемент яшчэ не досягае паветранага мінімуму, калі суседзяючыя абзацы дастаткова супадаюць, калі адзін з блакоў ёсць дужа маленькім і не павінен стаяць самастойна, або калі спакойныя блакі належаць да таго ж спіску чы рэштры. Сумарная дужына падрабочага элемента зазвычай должна застацца ў межах 1,600 знакоў. Рэштра не будзе прыкладвана да звычнага тэксту проста для таго, каб дасягнуць мінімальных размераў:
if (
current_type == BlockType.TABLE
and block.type != BlockType.TABLE
) or (
block.type == BlockType.TABLE
and current_type != BlockType.TABLE
):
is_under_min = False
Ліктавыя чакі можна адзярваць у месцах падазначэння елементаў, тады як адзіны блаказец неяк чакаваць як адзын структурны блаказец, а не роздзяваць на частыкі. Рэчытак у тым, што рэзультат не е ні чысафічная, ні чысафічна адзярваная за размерамі. еў яе чакаваць як адзын структурны блаказец, а неяк чакаваць як адзын структурны блаказец.
Document structure
+
Semantic similarity
+
Minimum and maximum sizes
Шаг 5: захаваце рэлак наявнага элемента. Кожны створаны дзеціні элемент атрымле свой сабэй ID і захавае ID свайго наявнага элемента:
{
"parent_id": parent["parent_id"],
"child_id": f"{parent['parent_id']}_child_{index}",
"doc_id": parent.get("doc_id", ""),
"heading_path": parent.get("heading_path", []),
"text": chunk["text"],
"type": chunk.get("type", "mixed"),
"embedding": encode_text(chunk["text"])
}
Іспанаваець кампанію Finance як прыклад:
parent_0164
│
├── parent_0164_child_1
│ Article 264 and introductory Finance context
│
├── parent_0164_child_2
│ Articles 265 and 266 concerning taxation and public funds
│
└── parent_0164_child_3
Article 267 concerning the Contingency Fund
Усе трое дзяцей застаюцься:
{
"parent_id": "parent_0164"
}
Зберажана дзецячая картка выглядае так:
{
"child_id": "parent_0164_child_2",
"parent_id": "parent_0164",
"doc_id": "constitution_of_india",
"heading_path": ["PART XII"],
"type": "list",
"text": "265. Taxes not to be imposed save by authority of law...",
"embedding": [0.012, -0.034, 0.021]
}
У падзеце ўсьго знаходзіцца 871 дзецінскі элемент, які належаць 333 абяцаўскім элементам.
3. Гібрыдны спосаб выкарыстоўвання, ад запіту да кантэкста абяцы
Калі ўжо ёсць 871 дзецінскі элемент, падыходзіць пытанне пра тое, як знайсці правільныя доказы для запіту карыстальніка. Выкарыстоўваецца аналіз дзецінскіх элементаў, а не абяцаўскіх, таму што короткі фрагменты легча падходзяць да конкрэтной статыі, клазу чы ідэі. Большыя абяцаўскія элементы выкарыстоўваюцца толькі пасля таго, як вядомы найболей значныя дзецінскія элементы.
Чаму аднальны метод недастатковы
Прававыя запытанні бываюць рознымі. Запыт на кшталт "Што сказана ў статыце 23?" містыць номер статыцы, які таксама прымаецца ў аднаго з джэраў, таму для яго чыста падходзіць пошук за ключоўымі словамі. Корыстнік можа запытацца пра тое ж самае іншым спосабам: "Можа дзяржава заставіць каго-небудзь выконваць прымусовую службу?" У такім формулюванні не называецца статыца 23, таму сістэма должна з’яўляць звязак між фразай "заставіць каго-небудзь выконваць службу" і ідэяй прымусовай службы для гаспадарскіх цэлей. Для эўкалібравання гэтых двух сітуацыяў патрэбны разныя алгорытмы:
- буквальны пашук, для номераў статыцэ, прававых фраз, імен і нестандартных тэрмінаў, і
- пашук на адной значэнні, калі формулюванне запытання адхоўляецца ад тэксту.
Уместо выбору сістэма працюе з тым самым дадзеннем праз два адналежныя методы: BM25 для рыхлаго пошуку і BGE для ўжымленага пошуку.
Рэдкісныя галузі: BM25
BM25 ранжавае за словамі, якія запіт мае з кожным падгалункам. Пры стварэнні індэксу шляхі загалоў і тэкст падгалункоў перакладаюцца на малае літэры і токенізуюцца, а індэкс фіксуе, якія падгалунакі маюць кожны тэрмін, як часта ён з’являецца і насколькі рэдкісны ён у всім корпусе. Значэнне маюць тры фактары: частота з’явлення тэрміна запіту ў падгалунке, ступень рэдкіснасці гэтага тэрміна ў корпусе і дыявалюцыя дужыну падгалунка у порываў з сярэднім значэнням. Рэдкісныя тэрміны маюцы большое значэнне, чым звычайныя словы, што робіць BM25 ўжо особліва прыдатным для номераў статэй і спецыфічных прававых выразаў.
Напрыклад, падчас пошуку Article 23 compulsory service сільна прытлумленасць дзецей, які маюць 23, compulsory і service. BM25 не патрэбна ўсё глобальнае значэнне, адколі сама прымесцовасць эйтаноў ёсць сільным сігналам. Яго слабасцю ёсць тое, што рэлевантны абзац можа атрымаць нізкі бал, калі запіт і выхадны тэкст выкарыстоўваюць разную лексыку.
Жысткія методы: BGE і FAISS
Жысткія методы ведуць пошук па значэнні, а не толькі па перакрыццю слоў. Кожная дзець выражаецца праз ўсё шляхі загалоўкаў плюс яе тэкст:
embedding_text = heading_path + child_text
Уключэнне заголовка дае кантэкст, які дапамагае супарадным абзацам і запыткам знаходзіцца поблізу адзінаго другога ў прасторэ эмбедынгу.
Выбір модэлі эмбедынгу за дапамою крэйтарбанка
У першай версіі праекту быў выкарыстоўваны nomic-ai/nomic-embed-text-v1.5, але аўтар хацеў паўнастаць, чы гэта самая прыдатная модэль для гэтага корпусу. У контрольным тэсце на ўзяце адбылася параболка чатырох настаўк: Nomic без прыметак, Nomic з рэкамендаванымі прыметкамі для запыткаў і дакументаў, BAAI/bge-base-en-v1.5 і intfloat/e5-base-v2.
Кожны модэль шукаў у тым жа наборэ з 871 фіксаванага частака-дзеці, пры чым 383 запытанні для ацэнкі былі автаматычна супараджаны.
- MRR: дае большых балоў, калі першыя рэлевантныя частакі-дзеці знаходзяцца бліжэй да верху.
BGE парадзіў за рэзультатамі MRR, Recall@10, Recall@20 і mean coverage@10. Значэнне MRR вырасла з 0,624 у Nomic да 0,652, а Recall@20 — з 0,877 да 0,911. Гэта мяла значэнне, таму што пазнейшы працэсы злучэння і пераранжавання выкарыстоўваюць обмежаны пул кандыдатаў, і адпаведны элемент, які не быў знайдзены пад час інтэнсівнага пошуку, не можа быць вярнуты пазней.
Nomic паказаў нейкія лепшыя рэзультаты па показніку All-recall@10 (0,347 проты 0,334), што значыць, ён быў адносна лепшы у вярненні ўсіх пазначаных „дзецяў“ для запытанняў з колькіма дзецьмі. Тым не менш, выявленне хоць аднаго сільнага „дзеця“ раней мела большое значэнне для вярнення кантэксту „бацькі“. Час адпаведзення на запыт быў практычна ідэнтычны. Таму збераганыя эмбеджы „дзецяў“ Nomic былі заменены на эмбеджы BGE, пры чым былі захаваны існуючыя семантычныя межы, так што змена эмбеджаў не паўтарылася ў зв’язку з ранейшымі выборамі частак.
„Дзеці“ эмбеджуюцца як звычны тэкст без прадзеўніка-інструкцыі. Запытання ж, з іншай стороны, аплываюць інструкцыяй па вярненні даных, рэкамендаванай BGE:
query_text = (
"Represent this sentence for searching relevant passages: "
+ user_query
)
Той самыя модэль BGE адключае запіты і дзецоўскія вектары. Вектары нормалізуюцца па стандарту L2, таму скалярны дарабытак паводзіцца як косайнае сяродзесто, пры чым вышэйшыя значэння означаюць большое сяродзесто.
Індэкс FAISS
Нормалізаваныя дзецоўскія вектары падаюць у індэкс FAISS Hierarchical Navigable Small World:
index = faiss.IndexHNSWFlat(
embedding_dimension,
32,
faiss.METRIC_INNER_PRODUCT,
)
index.hnsw.efConstruction = 200
index.hnsw.efSearch = 64
index.add(embeddings)
HNSW аранжуе практыкі як навігабельную граф, таму пошук адбываецца праз перспектывных суседаў і не паўтарае порэванае з кожным вектарам. Повнае порэванае яшчэ можна було бы адмацаваць для 871 дзецінскага прыкладу. Выбраўся HNSW, каб дызайн заставаліся функцыональным, калі да корпуса дадаюцца новыя законы, правілы, рашэнні і падобныя дакументы. Дзецінскія прыклады былі практыкаваны занова за дапамою code/reembed_children_bge.py, а індэкс — перабудаваны за дапамою code/rebuild_dense_index.py. Падготовка тэстаў, апісанні модэляў, запыты і рэзультаты знаходзяцца пад пададрэсам code/dense_benchmark/v2/.
Спаўненне двух рангаванняў
BM25 і метод густага адзыскання ствараюць адзінаковыя спісы з рангаванням. Дзеця можа займаць высокія месцы ў обох, сільна — толькі ў аднам, або ўзагалі не быць у аднам з іх. Чыстыя балы нельга проста сумаваць, адтуды што балы BM25 і вектарная сэмліярнасць належаць да разных масштабаў. Гэтыя два спісы з рангаванням з’едынаюцца за дапамою методу Reciprocal Rank Fusion:
RRF score = Σ 1 / (k + rank + 1)
У гэтай схеме выкарыстоўваецца k = 60. Кожны учаснік зарабляе балы за сваё месца ў кожным з супакінанняў, а не за свой пачатковы рэйтынг. Учаснік, які займае высокую пазіцыю ў обох супакінаннях, аброшвае вклады з обох і зазвычай падыходзіць вышэй, тады як той, хто сільны толькі ў аднам супакінанні, можа застацца ў складзе кандыдаў. Спрасцаваны этап злучэння:
sparse_results = bm25_search(query)
dense_results = dense_search(query)
fused_candidates = reciprocal_rank_fusion(
sparse_results,
dense_results,
)
RRF стварае шырокі рэпертуар кандыдаў. Ён падвышае шансы на тое, што будуць застаўлены корысныя доказы, але не практыкуе дакладную параваноўку всіх запітоў з кожным абдзялкам. Гэта завадзіба рэранкера.
Рэранкаванне за дапамою крос-энкодэра
Жорсткая пашуковая сістэма ёсць дышэўная, адтуды што запіт і падзеянні кодуюцца окрэма, таму эмбеддынгі падзеянаў можна вырахаваць раз і воспаліваць для кожнага запіту. Але такая незалежная кодаванне обмежвае ступень, па якой модэль можа ацэніваць ўзаемную значымасць іх слоў. Крос-энкодэр выбірае іной падход і прымеўшы запіт, адразу таксама прымае кандыдаўскія падзеянні:
pairs = [
(query, child_text),
...
]
scores = reranker.predict(pairs)
Паколькі запіт і тэкст аддаюцца як адзін вхід, мадэль можа безпаслявно паглядзець, як іх токены асоціююцца межа сабой, і гэта зазвычай дае болей тачныя вынікі пра ступень адносу, чым методы BM25 або ўжоўтварэнага пошуку.
candidates = fused_candidates[:30]
reranked_children = cross_encoder.rank(query, candidates)
Сістэма занова ацэнюе кожны элемент і зноў сортавае кандыдатаў. Ранейшыя позыцыі BM25, dense і RRF толькі ствараюць кантэйнер з кандыдатаў, а рэшэнне крос-кодувальніка вялічынае фінальны парадак. Пасля чаго сістэма адбірае першыя 20 элементаў, якія былі перэацэненыя, і залішае тыя, якія маюць ацэнку ў меры не менейшую за 0,30:
selected_children = [
child
for child in reranked_children[:20]
if child.score >= 0.30
]
Гэты порог — гэта палітрык фільтрацыі для данага конкрэтнага модэлю і праграмныя ланцюгі. Яго не трэба спрыяваць як універсальную верыятнасць рэлевантнасці, а змена прыстрою перэранкавання значыць яго паўтарную ацэнку і калібраванне. Табліцам трэба ўсё больш адзяйнае ведчыме ставленне, таму што дзеяныя з іх перакрываюць максімальную кантэйнерную глыбіню прыстрою перэранкавання у 512 токэнаў. Перш чым адбываецца токэнаізацыя, рабочы працэс пазначае пары табліцаў, якія занадта дугія, ўсёлякім чынам утрымваючы іх ад выйсця канфліктам у індэксаванні чы ў таямнай знікненні.
Пашырэнне дзецячых елементаў у кантэкст абяцаючага элемента
Прыстрой перэранкавання продовжае працаваць з дзецячымі елементамі, таму што яны ўтвараюць аб’екты для сфармаванага порэвання. Аднак вярненне толькі гэтых дзецячых елементаў могла бы выключыць суседзяючыя клазулі, умовы, аднаковасці чы саюзныя статты. Таму кожны дзецячы элемент застаецца з parent_id, які быў прызначаны пад час разбівання на часткі:
{
"child_id": "parent_0082_child_1",
"parent_id": "parent_0082",
"text": "23. Prohibition of traffic in human beings and forced labour..."
}
Калі фільтрацыя па праге завершана, сістэма берае parent_id з кожнага выбранага дзецячага элемента і шукае весь абяцелюючы элемент у колекцыі parent-chunk:
parent_id = selected_child["parent_id"]
parent = parent_lookup[parent_id]
Кілька вібраніх дытэй можуць маты спільного бацькі, а повторне повернення дублюватыме кантэкст, тому дыты групуюцца за parent_id. Кожэн унікальній бацько віводыцца одын раз разом з дыцьмі, які до яго прыедналіся, а найвішчый бал дытыні стае балом того бацькі.
Selected child 1 ──┐
Selected child 2 ──┼── parent_0082
Selected child 3 ──┘
Маленькі дзеці, які маюць неабяжлівую мову статты 23, можаць запрашваць parent_0082, які тады ўтварае шырэй раздзел «Праўа проты эксплуатацыі», уключаючы супакойлевыя клазусы і адносаваны контэнт статты 24. Гэта палячыць непрызменным раздзеленне задач: часткі дзецей вяршаюць выбор неабяжлівых доказоў, а часткі родзічаў задаюць кантэкст, у якім яны з’яўляюцца.
Вернуты родзічавы істочнік таксама зберагае свой ID дакумента і шлях заглавня, ID адпаведных дзецей, іх тыпы блакоў і рэнкінгавыя балы, таму вернуты істочнік можна праследаваць праз весь працэс, каб зрозумець, чаму ён быў выбран. Эксперымент з гібрыдным адзыскам належыць у code/hybridrag.ipynb.
4. Пашырэнне запітаў і багатоэтапныя запыты: закрыць прыемкі ў слоўніку
Гібрыдны прынцып работы добра працаваў, калі формулаванне пытання карыстальніка ў значнай меры падражаў Канстытуцыю. Простае пытанне на кшталт «Што такое артыкул 21?» містыць точны апіс, які дае BM25 і методу ўзнікнення дакументаў сільны сігнал.
Канцэптуальныя пытання былі сложнейшыя. Выкарыстоўчыкі часта опісваюць прынцыпы за дапамогою тэрмінаў з рашэнняў, падручнікаў чы прававых коментараў, а не самай мовы Канстытуцыі. Пытанне пра «доктрыну базовай структуры» можа вымагаць адзінаваць статты 13 і 368, нават які фраза «базовая структура» не выклікаецца ні ў адной з іх. Іншымі словамі, у выкарыстоўчыка правыя ўявленні, але дакументы называюць іх інакш.
Што паказала ацэнка
Проблема была прытанаяя ў нотбуку Kaggle, які паводзіўся пра прымытые запыты і сложнейшы набор концэптуальных запытаў, якія адносіліся да праглядзяных статэй Канстытуцыі. Эксперымент прайшоў да таго, як была адбыта міграцыя на BGE. У той час 871 дзеці мелі эмбедынгі з nomic-ai/nomic-embed-text-v1.5, а запыты былі кодаваны за дапамою таго ж модэлю без якіх-леба прыфіксаў для аднаходжэння. Гэты дакладнасць мае значэнне, таму што эксперымент мяў на меце выявіць, як вплывае перапісваў запыту, а не ранжаваць модэлі эмбедынгаў; таму той жа гібрыдны прайсепт на адной базе Nomic апрацоўваў оба варыянты кожнага концэптуальнага запыту.
Запытанне лічыцца як праўільна адпаведзена толькі тады, калі знайдзены ўзлеглы кантэкст мітываў усе цялявыя статткі, якія з’яўляюцься ў зв’язку з яным; таму паказаны процэнт відображае ступень повнага адшукаўвання цяляў, а не фактычную точнасць якой-небудзь адпаведзі, напісанай LLM. Актуальныя порэвананні дзеяцца межы двумя канцэптуальнымі наборамі. Розшырэнне запытання падняло ступень праўільных адпаведзення з 56,0% да 74,5%, што ўрахоўвае падысканне на 18,5 процэнтных пунктаў. Гэта дазволіла адшукаць усі цялявыя наборы для ўсёх 37 дапаможных запытанняў і зменіць колькасць непраўільных адпаведзенняя з 88 да 51.
Рэзультаты паказалі, што багато непраўільных адпаведзенняя не былі вызваныя відсутнасцю дакументаў чы паслабленымі межамі фрагментаў. Доказы існавалі ў корпусе, але первісны запытанне не даваў адшукавальнай сістэме вокабуляру, неабходнага для ўсьведамлення іх.
Розшырэнне без змены намеру
Пашырэнне перакладвае концэптуальны вопыт словамі, якія больш верагатліва ўжываюцца ў адпаведным істочніку. Падазроўваючы, што вопыт ставіцься пра тое, што стрымлівае Парламент ад змены сутнаснага ідэнтытэта Канстытуцыі, расшырэная версія можа уключыць доктрыну базовай структуры — ядро Канстытуцыі, якое не можа быць параджанае, законы, якія суперасункуюцца з Фундаментальнымі правамі, артыкул 13, а таксама права на параджанне, наведзеныя ў статты 368. Тое, чаго хоча паўтаральнік, не змянілася, але дэльнейшы тэкст дадае сігналы для пошуку, такія як:
- звычайная назва доктрыны,
- связаная канстытуцыйная тэрміналогія,
- альтэрнатыўныя выразы той самай ідэі,
- атрыбуты інстытуцыйнага чы аб’ектуальнага лексыку, і
- чысткія номеры статэй, калі іх можна аднойчына вывесты.
Такое жа адаптаванне раскрывала пытанні пра можлівасць разлічэння элементаў, сутнасць і галоўных пунктав, судовага актываізму, а таксама пра тое, як фундаментальныя правыя гарантыі адносяцца да керуючых прынцыпаў.
У заходах Kaggle расшырэныя запіты былі напісаны загадзя, пры чым нават былі пераказаны верагодныя статты, таму іх трэба розглядаць як эксперымент з керованай пераформулаваннем запітаў, а не як ацэнку модэлі автаматычнага расшырэння. Ён паказвае, насколькі можа падняцца кантроль запытам, калі ў яму ёсць мова, якая адпавядае існуючым джэрэлам, але для рэальнага планувальніка запытам патрэбна аднаэчная ацэнка без доступу да цільных метак. У пазнейшым дэтальным тэставанні Nomic быў заменены на BGE, таму што BGE парадзіў у працы на фіксаваным корпусе дадзеных, пры чым рэзультаты Kaggle такі ж заставілі змяніць дызайн: лепшы модэль укладкі паспрабаваў падняць сэмантычную кантроль запытам, тады як пашырэнне запытам спрамавана на рашэнне іншай проблемы — розніцы ў слоўніку між корыстнікам і дакументам.
Пашырэнне запытам проты падзапытам
Пашырэнне дапамагае, калі адна з патрабоў да інфармацыі выказваецца на незнаёмай або калякватной мове, і гэта дае болей шырокую запітанне. Багатоэтапныя запітанні ўжо іншыя, таму што яны маюць калькі патрабоў да інфармацыі, якія можуць супаказвацца з рознымі часткамі дасягледу. Разглядзімо запіт пра параболенне заходаў, прызначаных согласна артыкуламі 32 і 226. Калі шуканне адбываецца як адна дзяўная запітанне, сігналы конкуруюць: выражнае рэзультат для статты 32 можа заполніць спіс кандыдатаў, тады як артыкул 226 застаецца маладастаткованай, а пераранжаванне сумарных кандыдатаў не гарантуе, што обе стороны застануцца.
Багатоэтапнае запітанне можа быць замест таго раздзелена на спецыялізаваныя падзапітанні:
- Какія заходы і юрысдыкцыя прызначае артыкул 32?
- Какія правы і юрысдыкцыя прызначае артыкул 226?
- Якія констытуцыйныя разлікі ёсць между артыкуламі 32 і 226?
Кожны падзеапытленне праходзіць праз усіэ цяпрыборы аднаходжэння дакументаў (BM25 і ўсеаспраўленая працэс аднаходжэння, пасля чаго — спаўненне рангу, потым — пераранжаванне за дапамой крос-кодэра, а пасля — розгледванне галоўных элементаў). Рэзультаты працы галоўных элементаў з’еднаюцца і адмахваюцца ад дублікаў, ствараючы ўжоўсёды адноцелыя доказы. Чыгунам гэта запобегае таму, каб адна частка складнага запіту выкорыстоўвала рэсурсы, прызначаныя для іншай, і спрыяе лёгкаму аналізу працы аднаходжэння, калі кожны варыянт запіту адпаведае конкрэтной патрабоўцы.
Рэзультатныя стратэгіі направлення
Аналіз паказаў, што жадная адна трансфармацыя запіту падходзіць не для кожнага запіту. Простыя запыты ужо маюць сільныя сігналы і павінны застацца такімі, якія ўжо ёсць. Канцэптуальныя запыты выгадваюць ад расшырэння слоўніка, а запыты з кальцамі патрэбуюць раздзелення, каб кожная патрабоўка мала свой варыянт запіту:
- Простыя фактычныя запыты або запыты, спрамованыя на конкрэтныя статты: адбываецца пошук з выйснім запытом.
Такі падход запобегае непатрэбным змянам у простых запытаннях, а таксама надае складным запытанням неабходную структуру. Пачатковая ацэнка на Kaggle рэалізавана ў code/hybrid_eval.ipynb, а пазнейшыя експерыменты з розгалужэнням запытанняў — у code/hybridrag.ipynb.
Урокі з праекту
Головній урок полягае ў тым, што лепшыя раштры RAG рэдка колі залежаць ад однаго ідэальнага мадэлю. Якасць отрымання інформацыі залежыць ад правільных рашынкаў ўзгляду на екстракцію, частковэ падзелленне, інтэграцыю даных, комбінацыю кандыдатаў, перэранкаванне і відновлення кантэксту. Працэс ацэнкі змяніў калькі выбораў на шляху. У ўмоцнаішых тэстах Nomic быў заменены на BGE, пры чым булі захаваны семантычныя межы; эксперыменты на Kaggle паказалі, што концэптуальныя запытанні лепшая рашваюцца за дапамогою тэрміналогіі, бліжэйшай да вачыннага тэксту, а проблемы з калькамі вадзілі да выкарыстоўвання спецыяльных падзапытанняў і незалежных шляхоў отрымання інформацыі.
Наступныя естэчныя крокі включаюць адгуктаванне цій сістэмы ў прыемнік-чатбота з структураваным бэкендам і фронтэндам для ставлення запытанняў і аналізу джэрэл, пасля чаго яе пакуюць за дапамой Docker і размешчаюць на платформе ў воблаку, дзе пытаннія вылучаюцца ў частцы завантажэння мадэлі, выкарыстоўвання памяці, затрымкі, стойкіх індэксаў, логавання, масштабавання і віток. Сістэма яшчэ развіваецца, і дзеяныя несправы застаюцься нерашанымі, і гэта ўсё-такі мета такіх віпробаванняў: прабаваць заставіць кожны этап падлежачы аналізу, знайсці, дзе губяцца доказы, і дазволіць ацэнкам кераваць наступнымі удосконаленнямі.