Сістэма пытанняў і адказаў з цытатамі па PDF цэнтральнага банка на RAG
Практычны RAG па квартальных справаздачах цэнтральнага банка: загрузка, пошук з улікам свежасці, адказы з цытатамі і рэальныя памылкі, знойдзеныя па дарозе.
Большасць навучальных матэрыялаў па тэхналогіі RAG працуюць з аптаем, створаным дадзенням. Рэальныя дакументы паводзяцца інакш, і разлік межаючы розумэннем тэхналогіі генеравання з дапамогай выкарыстоўвання дадзенняў і ўжыткам яе на практыцы стае зрозумелым толькі тады, калі хтось стварае цэлую сістэму на адной з бялканутых баз дадзенняў. Самэ гэта стало падставай для стварэння bspQA — сістэмы на адпаведзенні на запытанні, якая працуе з квартальнымі звярненнямі па манетарной палітыцы Банко Сентрал нг Піліпінас, центральнага банку Філіппінаў.
Корпус дакументаў ўтварае хорашую мету для навучэння. Кожны звярнення містыць 60–70 сторанакінтэнсівнага эканамічнага аналізу, прычым семнаццаць з іх належаць да перыяду калькі лет, і ўсе яны ёсць публічнымі. Мова ў яных спецыфічная для данай галіны, існуе багато файлаў, а проблема пошуку ёсць рэальной, адтаколькі ніхто не можа пераклацці семнаццаць звярнень у вікно кантэксту. Мета была скромная: запытацца чымось на кшталт «Якія былі прогнозы інфляцыі на 2026 год?» і за секунды павінна быць отрымана обоснованая, апавярнутая адпаведна адпаведзь, замест таго, каб трэба было шукаць у купе PDF-файлаў за дапамогою Ctrl+F.
Шаблон у аднам параграфе
RAG спрыяе таму, што адказы LLM асоціююцца з выбраным наборам дакументаў, а не з тым, што мадэль абсорбавала пад час навчання. Дакументы загадзя ператвараюцца на вектарныя эмбедынгі. Калі прыходзіць запіт, запрашуюцься фрагменты, якія знаходзяцца як можна бліжэй да яго, якія пасылаюцца модэлі як кантэкст, і модэлі прыказваецца адпавядаць толькі на адвагу гэтага кантэксту. Рэзультатам являецца сістэма, якая адмовляецца выдумваць тое, чаго няма ў дакументах.
Два працэса, якія знаходзяцца адзін ад другога
У дызайне намерна раздзеляюцца два заведаменні.
- Працэс прыемкі дакументаў выкананы афлайн, адна раз на кожны новы дакумент. Ён завантажае PDF-файлы, парсуе іх у звычны тэкст, дзельніцьвае тэкст, стварае эмбедынгі і зберагае все ў Qdrant.
- Працэс запітаў выкананы у рэальны час на кожны запіт. Ён стварае эмбедынг запіта, запрашуюць рэлевантныя фрагменты і генеруе адказ. У гэтым этапе нечыстыя PDF-файлы ніколі не чыніцца.
Стварэнне прыемкі дакументаў
Этап завантажэння
Файл manifest.json ёсць абсалютным адказам. Кожны справаздача мае ентры, у якіх указваецца яго URL, пазначэнне і дата публікацыі. URL-адрэсы былі вставлены вручную, і гэта настолькі ж непрывабліва, наскількі і звучыць.
{
"doc_id": "bsp-mpr-2023-q3",
"title": "Monetary Policy Report Q3 2023",
"period_label": "Q3 2023",
"publication_date": "2023-08-17",
"url": "https://www.bsp.gov.ph/..."
}
Програма для запошуків прыкідае файлы, якія ужо знаходзяцца на дыску, таму ўскладнення яе запуску няма, а дадзенне атласу означае прыўнесенне ўтолькі што аднаго елемента да спісу дакументаў.
Этап парсавання
Інструмент MarkItDown ад Microsoft ператварае PDF-файлы ў тэкст. Для документаў з велікай колькасцю тэксту ён выконвае прыемную працу: заголовкі застаюцца недыскрэтаванымі, перэрывы між абзацамі выходзяць чыстымі, а табліцы застаюцца цэлымі ледзь-ледзь.
MarkItDown таксама ўтварае значную залежнасць, якая прыносіць велікі чысла пакетаў. Паколькі парсаванне адбываецца толькі пад час прыймання дакументаў, а не пад час адпаведзення на запыты, ён належыць да групы неабавязковых залежнасцей, таму бэкенд, які рэндаруецца, не несе такой навантажэння. Варта як можна раней выклікнуць звычку: выбраць, якія пакеты належаць да асяроддзя прыймання дакументаў, а якія – да асяроддзя ўтрымання, таму што гэтыя два элементы не павінны збігацца. Этап чысткі таксама знаходзіцца між парсаванням і разбіванням на часткі, а прычына яго існавання будзе паказана ніжэй.
Разбіўка на часткі, вбудовванне і дапрацоўкі
Самыя важлівыя рашэнні прыймаюцца тут. Часткі формуюцца з 512 токенав скользячага вікна з перакрыццем у 64 токены, якія лічыцца за дапамою tiktoken.
def chunk_text_by_tokens(text, enc, chunk_size=512, overlap=64):
tokens = enc.encode(text)
stride = chunk_size - overlap
chunks = []
start = 0
while start < len(tokens):
window = tokens[start : start + chunk_size]
chunks.append(enc.decode(window))
if start + chunk_size >= len(tokens):
break
start += stride
return chunks
Перакрыцце гарантуе, што рэчы, пераканчаныя на межы частака, яшчэ застаюцца цэлымі прынеймна ў аднам частаку.
Эмбедынгі атрымліваюцца з модуля OpenAI's text-embedding-3-small, які ёсць недорогім і надзеяным для напісання дакладных тэкстаў. Весь праект коштаў або-і дзесяць долераў у вараблях, хоць мінімальная сума падзапісу тады станавіла пяць долераў, і гэтага цягма хапае для невялікага праекту.
Кожны вектар атрымлівае ідэнтыфікатор UUID5, які вылічваецца на адной з асноваў — з назвы вучальнага файла і індэкса частака. Гэта робіць практыку паўторнага вводу безпечной, адколі другі запуск заменяе існуючыя запісы, утрамбоўваючы дублікаты.
Кожны зберагачыты пакет містыць аднаго файла-выхадніка, індэкс часткі, сам тэкст, мадэль эмбедынгу, атрыбут перыоду, нормалізаваны ключ перыоду, напрыклад q3_2023, а таксама дату публікацыі у фармаце ISO 8601. Qdrant зберагачыць індэксы ключоўдзей для файла-выхадніка і ключа перыоду, а таксама індэкс часовых дат для даты публікацыі, таму фільтрацыя метаданых ведаецца быстра пад час запыту.
Адзысканне і падступка свежасці
Простая наладка вяртае K найбольш прыдатных рэзультатаў на аднойчыну схожасць. Гэта працуе пакуль корпус павяртаецца на калькі гадоў, а кто-небудзь запытаецца пра сучасныя прогназы інфляцыі. Калі банк вжыў практычна ідэнтычныя выразы пра інфляцыю ў першым квартале 2022 года і трэцім квартале 2024 года, частка 2022 года можа мець вышыю оцэнку завяршваючы на аднойчыну лепшыя формулюванні, нават як чытач, што ў 99% случаў, хочаць аб найсвежэйшай інформацыі.
Рэштэн — абярацкае запрашоўванне даных і параднай класыфікацыі. Сістэма запрашоўвае у тры разы больш кандыдатаў, чым было запрашана, а пасля занова оцінюе кожнага, выкарыстоўваючы сумешанне косай схожасці і нормаванага значэння свежасці.
RECENCY_WEIGHT = 0.85
def combined_score(hit):
relevance = hit.score / max_score
recency = (pub_date - min_date).days / date_span_days # 0.0 to 1.0
return (1 - RECENCY_WEIGHT) * relevance + RECENCY_WEIGHT * recency
Вага няўзгоднасці з пасярэднім часам у розмярэ 0.85 означае, што новейшыя дакументы атрымваюць высокія балы, калі толькі старэйшы дакумент не ўзьме значна больш значныя практычныя наследкі. У корпусе правілаў, дзе кожны справаздача фактычна заменяе пярэдні, такі баланс здаецца правільным.
Неабяжныя фільтры дазволяюць корыстніку сконцэнтраваць пошук на аднам перыядзе або на дыяпазоне дат:
# scope to Q1 2023 only
retrieve_chunks(query, period_label_key="q1_2023")
# scope to a date range
retrieve_chunks(query, publication_date_from="2023-01-01", publication_date_to="2023-12-31")
Без фільтраў сістэма працюе як адказчык для „пяршага працэсу перагляду“. З фільтрамі яна ператвараецца на інструмент для історычных даследжэнняў.
Генераванне
Генератор спецыяльна просты. Атрыманы фрагменты фарматуюцца як нумераваныя джэрэлы і перэдаюцца GPT-4o-mini па заданні сістэмы, якое, пераказанае простаю мовай, гласі: выкорыстоўваць толькі паданыя джэрэлы, не прыменяць жадных зовнішніх знанняў, адзначаць кожнае фактычнае тверджэнне безпосерэдна ў тэксце і паведамляць пра тое, калі джэрэлы не маюць достатню інфармацыю.
Гэтыя апошнія тэрміны заслуговуюць на серйозную увагу. Чыста адмежавана інструкцыя ёсць найэфектывнейшым спосабам запобежэння галюцынаціям. Калі пошук не вярнуў нічога рэлевантнага, сістэма адпавядае фіксаваным узворотным паведамленнем замест таго, каб імпровізаваць.
Адаптаванні API несу тэкст адказу плюс структураваныя спісы выканаў, у кожным з якіх указваецца перыяд, да якога ён належыць, час публікацыі і месца, дзе знаходзіцца PDF-файл выкана. На фронтэндзе гэтыя элементы ператвараюцца на клікабельныя цітаты, што дазволяе ведаць пра кожнае тверджэнне.
Проблемы, якія з’явіліся на практыцы
Элементы сторанкі, якія забруднююць часткі
Кожная сторанка кожнага адчытніка мае заголовак «Класыфікацыя: ЗАВУЖНАЯ» і ніжнія падпісы у фармаце «Адчытнік па грошовай політыцы - 3-й квартал 2023 г. | 47». Пасля парсінгу гэтыя рэчы выступаюць як звычны тэкст і практычна включаюцца ў часткі, вітрачаючы рэсурсы на непатрэбны матэрыял. Номер сторанкі ў такім ніжнім падпісе насправды мог бы дапамогчы стварыць болей тачныя цітаты, але гэя ідэя з’явілася толькі пазней.
Рашэннем ёсць працэс чысткі перад стварэнням частак:
def _clean_parsed_text(text):
text = text.replace("\x0c", "\n\n")
text = re.sub(r"Classification:\s*GENERAL\s*\n", "", text)
text = re.sub(r"Monetary Policy Report\s*[--][^\n]+\|\s*\d+\s*\n", "", text)
text = re.sub(r"\n{3,}", "\n\n", text)
return text.strip()
Пасцялей, калі аналізаваны тэкст ніколі не пераглядаецца пры ўключэнні, гэта стае зрозумелым, але лёгка ўпусціць з увагі. Сімптамы былі дзівныя: пад час тэставання адказаў сістэма, якая шукала інфармацыю, здавалася, выкарыстоўвала іншы фрагменты заместо таго, які меў найвышэйшую рангаванне. Падпісы роздзелялі параграфы пасерэд самага мыслення, чым тыха зменялася супакоўка, яку прэдставляў кожны фрагмент.
Актыўнае стэрэнне
Qdrant адказвае флагом recreate-collection, які знімае і перзбудоввае весь калекцыю. Гэта зручна пад час розрабоцтва, але катастрофічна ў працэсе выканання. Ёсць захад, які ігнаруе гэты флаг, калі сераўер працюе як у рэжыме выканання:
def _should_recreate_qdrant_collection():
if _is_production_environment():
return False
explicit = os.environ.get("QDRANT_RECREATE_COLLECTION", "").lower()
if explicit in ("1", "true", "yes"):
return True
app_env = (os.environ.get("ENVIRONMENT") or "").lower()
return app_env in ("development", "dev", "local")
Для праекта з адзінным корыстнікам захад можа здавацца занадто складным, але ў яго ўчынку практычна няма костоў.
Дуплікаты пасля паўтарнага абрабатвання
Пасля рэдагавання ентры маніфеста і паўтарнага абрабатвання для таго ж PDF існавалі два наборы чанкаў. Рашэннем ёсць выдаленне існуючых чанкаў вучорнага файла пры дадаванні новых:
client.delete(
collection_name=collection,
points_selector=models.Filter(must=[
models.FieldCondition(
key="source_file",
match=models.MatchValue(value=source_file)
)
]),
)
За дапамогай стабільных ідэнтыфікатораў UUID5 працэс паўтарной обработкі тэксту стаў ідэмпатным.
Пачатак работы на безплатнай версіі
Бэкэнд работае на безплатнай версіі Render, якая выключаецца, калі няма актывацыі. Першы запіт пасля пачатку работы можа зайняць 30–60 секунд, што дастаткова, каб візітары выніклі, што прыложэнне не працуе. Короткі эндпоінт /health рашае большасць проблем: фронтэнд запытае яго пад час завантажэння сторанкі, чым прыгожвалівае сервера пры першай жа запытцы. Гэта дапамагае захаваць додатковы HTTP-запыт на сесію. У інтэрфейсе таксама ёсць короткая адмова, якая пояснюе, што першы запыт можа зайняць паўмінуты да мінуты, пакуль бэкэнд прыгожваліваецца.
Выбір розмеру часткі
Значэнне 512 токэнаў было выканана эксперыментальна. Фрагменты з 128 да 256 токэнаў былі болей тачныя, але часта траплялася сітуацыя, калі ўтрачалася інфармацыя, неабходная для ўсаблівога ўзроумлення ўсіх фрагментаў. Фрагменты з 1024 токэнаў або больш прыносілі з сабой некаляжны тэкст. Надпакрыцце на 64 токэна ўблажвае рошчэныя на межах і ўзначыцья гэта додатковае запісванне.
Што змяніць наступны раз
- Спачатку створыце набор для ацэнкі. Існуе каталог evals, але перад налаштаваннем вагі свежасці або размеру фрагментаў не быў створаны набор ідэальных запытанняў, таму оба выбары ўзначываюцца толькі апрыксама. Рэальны набор для ацэнкі дапаможаў бы ўзначыць правільныя параметры.
- Дадаце прыстрой для перэранкавання. Адлік свежасці ў оцэнку ёсць толькі гістэрычным методам. Крос-кодэр, такі як Cohere Rerank, або локальны модэль, моглі б разам оцэніць запытанне і фрагменты і болей надзеямо адлікаваць ўзаемную адноснае значэнне.
- Спробуйце гібрыдны пошук. Жыццёвыя вектары можаць не знаходзіць точных тэрмінаў. Фразу на кшталт «7,00% стаўкі зворачнай праймарной прамышленніцы» сложна знайсці, калі слова ў пытанні разлічаюцца. Спалучэнне падбору ключоўых слоў BM25 з пошукам па вектарах могло бы запалаваць гэтыя прасоцы, і Qdrant адразу ж аддае гэту можлівасць.
- Трансляцыя адпаведзей. gpt-4o-mini трэбае два-пяць секунд, каб адпаведзець. Трансляцыя токэнаў па меры ўзнікнення робіць інтэрфейс быстрэйшым, нават калі абшчы час застаецца тым жа.
Павторны выкарыстоўванне дызайну
Архітэктура ёсць універсальной. Заменіце маніфест на іншы корпус дакументаў, апдэйтаваце апісанне домэнаў у сістэмным запрошэнні, і як рэзультат будзе створана функцыональная сістэма запытанняў і адпаведзей для будзь-якага набора PDF-файлаў. Складнейшыя аспекты — чыстая разбівка на часткі, адзыскванне інформацыі з урахоўваннем актуальнасці, фільтрацыя метададаў і безпечная перапрацоўка дакументаў — вялікай часткай вядомы.
Спаднія матэрыялы
- Практычныя прыемы: стварэнне локальнага каналу RAG высокага стандарта для прыменення — 100% безкоштовна, без — Паслядовыя інструкцыі па прыемах: стварэнне локальнага каналу RAG высокага стандарта для прыменення — 100% безкоштовна, без: контракты, перакантроль і месцы для коду для команд, якія вікорыстоўваюць гэты патэрн.
- Стварэнне сістэм RAG, якія функцыонуюць у рэальных умовах: ад прыема дадзэнняў да можлівасці аналізу — Ад разбівання дадзэнняў на часткі і гібрыднага пошуку да пераранжавання, кантролю доступу, набораў для ацэнкі і трэйсінгу: слоі, якія адзначаюць разліку між дэманстрацыяй RAG і сістэмай для прыменення.