Головна / Статті / Коли бот RAG цитує неправильну суму страхового відшкодування

Коли бот RAG цитує неправильну суму страхового відшкодування

Функція розбиття на фіксованого розміру ділила суму в доларах між рядками; необхідно було перебудувати процес аналізу та пройти чотири рівні обробки — від рекурсивних базових сценаріїв до отримання інформації з батьківського документа — перш ніж можна було довіряти отриманим відповідям.

2560 слів

Усе, що наведено нижче, — це складена наративна історія для навчання. Назви компаній, імена людей та суми в доларах є вигаданими; способи збоїв відповідають тим, з якими насправді стикаються системи RAG у сфері страхування та інших галузях з високими ризиками.

Пізно у вівторок керівник відділу обробки заявок звернувся до інженерів: асистент повідомив клієнту, що сума страхового внеску за повені становить 500 доларів, хоча у полісі було зазначено 5 000 доларів. Бот, посиленний здатностями до пошуку — „Atlas“ — працював вже три тижні з базою даних типу vector store, ефективною моделлю embedding, потужним LLM та оптимізованим інтерфейсом користувача. Однак у нього бракувало ефективної стратегії розділення інформації на частини.

Отриманий фрагмент виглядав так:

...applies to all covered perils. Deductible: $5
00 for flood events in Zone A, $500 for wind events, and $1,0

Під час вилучення даних з PDF число було розділено між рядками. Потім інструмент для фіксованого розділення на частини розрізав текст кожні 500 символів саме на місці цього розриву. Модель embedding індексувала фрагменти з написами „$5“ та „$500“. Модель відповіла впевнено — але неправильно.

Далі описано, як була перебудована система обробки даних: один етап парсингу, який визначає верхню межу, а потім чотири рівні поділу на частини, які поступово наближаються до цієї межі зі зростанням витрат.

Одна модель організації забезпечує чесність у розрахунках: майже весь час, необхідний для поділу на частини, витрачається на одноразову обробку для створення індексу. Ця обробка відбувається офлайн, ще до отримання будь-якого запиту від користувача. Єдиними витратами, пов’язаними з поділом на частини та які враховуються у показнику p95, є витрати на саму модель ембеддингу. Термін „дорогий“ для вищих рівнів означає дорогу обробку один раз на документ, а не за кожен запит.

Етап 0: Ви не можете поділити на частини структуру, яку знищили під час парсингу

Перший інстинкт — це використання більш розумного розділювача на частини. Краще поставити таке перше запитання: показувати текст, який розділяється, а не PDF. У початкових версіях Atlas текст складався з безкінечного ряду символів. Заголовки зливалися з основним текстом, а таблиці зникали, перетворюючись на один довгий рядок, де значення з різних рядків стояли поруч одне з одним. Текст на сторінці (“Policy Form HO-3 — Page 14 of 62”) повторювався кожні кілька тисяч символів.

Жоден розділювач не може відновити межі, які вже були знищені парсером. Якщо заголовки відсутні, у розділювача на основі markdown-заголовків немає чого розділяти. Якщо таблиці зникли, ніщо не зберігає рядки в цілості.

Підберіть кожен тип вхідних даних до парсера, який генерує структуру, придатну для використання розділювачем:

Оригінальні PDF та DOCX (форми політики, підтвердження, посібники). Варто використовувати парсери, які розуміють макет — LlamaParse, Unstructured.io, Docling, Azure Document Intelligence — замість простих конвертацій тексту. Необхідно мати markdown із вказанням типів елементів (заголовок, таблиця, список), а не просто порожній рядок.

Скановані PDF, зображення та факси. Слід використовувати OCR (Tesseract є найслабшим; PaddleOCR, Textract, Document AI, Azure — кращі). Потрібен текст разом із блоками макету та рівнем впевненості для кожного блоку. Рівень впевненості дозволяє пізніше позначити, що “ця цифра невизначена — не використовувати її для розрахунку страхових відшкодувань”.

HTML та внутрішні вікі. Слід прибрати зайвий контент та отримати чистий markdown із збереженими заголовками.

Презентації та таблиці. Потрібно зберегти межі слайдів чи сторінок, щоб жоден фрагмент не містив непов’язаних слайдів.

Аудіо та відео (дзвінки зі скаргами, вебінари). Whisper, Deepgram чи AssemblyAI мають генерувати транскрипт із позначенням того, хто говорив та коли. Якщо спікери не розділені, протилежні твердження адміністратора та клієнта можуть злитися в один оманливий абзац.

Після перепарсингу з урахуванням структури вигляд розділу з деталей витрат був таким:

## Section 4 — Deductibles

### 4.2 Peril-specific deductibles

| Peril          | Zone A  | Zone B  |
|----------------|---------|---------|
| Flood          | $5,000  | $2,500  |
| Wind / hail    | $500    | $500    |
| Named storm    | $1,000  | $1,000  |

Таблиця була повернена. Дерево заголовків також було повернено. „$5,000“ знову був одним токеном. Код на чанкінг ще не змінився, тож отримання даних вже було безпечнішим.

Рівень 1: Синтаксичний чанкінг — дешево, швидко та саме з цього варто почати

Фіксованого розміру: прототип, який випустився випадково

Розділяйте кожні N символів або токенів (CharacterTextSplitter, tiktoken). Витрати на індексацію майже нульові. Це підходить для тимчасових сплесків, але фатально у продакшені. Цей підхід розриває тексти посеред речення, таблиці чи числа — саме так виникають інциденти, що потребують виправлень. Правило після тієї ночі: системи фіксованого розміру ніколи не повинні використовуватися.

Команди постійно знову і знову виявляють одну й ту саму закономірність: невеликий набір демо-текстів з коротких блог-постів витримує розділення на символи, але як тільки з’являється перший багатоколонковий PDF чи документ, отриманий за допомогою OCR, якість відповідей миттєво падає. Розглядайте фіксовані окна як інструменти для локальних експериментів, обов’язково включивши до списку контролю пункт про їх заміну перед початком роботи з зовнішніми користувачами.

Перекриття: регулятор, а не стратегія

chunk_overlap повторює кінець частини N на початку частини N+1. Зазвичай використовується 10–15 відсотків (наприклад, 50 з 500 токенів у частині). Надлишкове збігання є недорогим захистом для рівнів 1–2; воно не виправляє погану межу — лише дублює її. Очікуйте приблизно на 10% більше векторів та дублікатних результатів у методі top-k, коли збіг знаходиться в зоні надлишкового збігання.

Коли дублікати домінують у списку сусідніх елементів, інструменти переранжування та ШІ витрачають ресурси вікна контексту двічі на одне й те саме речення. Видалення дублікатів за допомогою ідентифікатора батька або хешів тексту, майже ідентичних один одному, після отримання даних є недорогим способом мінімізації проблеми, якщо збігання мусить залишатися високим з інших причин.

Речення/параграф: дотримується граматики, ігнорує документ

Збирайте цілі речення в один блок (NLTK, spaCy, LlamaIndex SentenceSplitter). Ніколи не розрізайте речення посередині — інакше це завадить правильному розміщенню чисел у рядках — але інструмент не розпізнає заголовки, таблиці чи списки виключень. Чудово підходить для транскрипцій дзвінків; посередньо — для структурованих форм політик.

Рекурсивний символ: стандартний варіант, який використовується спочатку

RecursiveCharacterTextSplitter пробує роздільники у порядку пріоритету — порожній рядок, символ нового рядка, речення, слово — і звертається до альтернатив лише тоді, коли фрагмент все ще занадто великий, після чого об’єднує менші фрагменти до розміру chunk_size. Типовим значенням є 500 токенів із 50% перекриттям:

from langchain_text_splitters import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,
    chunk_overlap=50,
    separators=["\n\n", "\n", ". ", " ", ""],
)
chunks = splitter.split_text(policy_markdown)

У перепроаналізованому розділі з показниками, які можна вирахувати, було збережено все з рівня 4.2 — включаючи таблицю, — оскільки порожні рядки утворювали природну одиницю обсягу менше 500 токенів. Баг зник. Надсилайте рішення recursive-500/50 як базовий показник для порівняння, а не як готовий дизайн.

Запишіть цей базовий показник на дошці та відмовляйтеся від пропозицій щодо «розумніших» інструментів для розділення тексту, які не можуть перевершити його при розв’язанні тих самих типових завдань. Багато дорогих ідей здаються чудовими, поки їх не порівняють із простим рекурсивним інструментом для розділення тексту у форматі Markdown.

Рівень 2: Розділення тексту з урахуванням структури — робіть це там, де у документі вже є межі

За допомогою набору з приблизно 80 реальних запитань рішення recursive-500/50 досягло приблизно 71%. Невдачі виникали у довгих розділах та у відповідях, де модель не могла визначити, з якої політики чи розділу походить результат.

Розділення заголовків у форматах Markdown / HTML

MarkdownHeaderTextSplitter / HTMLHeaderTextSplitter розділяють текст за ієрархією заголовків та включають шлях до заголовка у метадані:

from langchain_text_splitters import MarkdownHeaderTextSplitter

header_splitter = MarkdownHeaderTextSplitter(
    headers_to_split_on=[("#", "h1"), ("##", "h2"), ("###", "h3")]
)
sections = header_splitter.split_text(policy_markdown)
# then apply the recursive splitter inside each section

Кожен фрагмент, отриманий з підрозділу 4.2, має містити „сліди переходу“, наприклад: форма HO-3 → страхові вилучення → таблиця для конкретного ризику. Ці сліди переходу потрібно приєднати до тексту перед його вставкою, щоб вектор кодував як місцезнаходження, так і числа. Через помилки типу „Який розділ?“ дані втрачаються; у посиланнях можна зазначити „згідно з розділом 4.2“. Витрати залишаються майже нульовими — але лише за умови, що на етапі 0 був створений справжній markdown. У спрощеному вигляді текст тихо перетворюється на один величезний фрагмент. Цей підхід найкраще підходить для вікі, технічної документації та документів із заголовками.

Метадані мають супроводжувати вектор: ідентифікатор джерельного документа, шлях до розділу, дата набрання чинності та юрисдикція, якщо правила відрізняються за регіонами. Ці поля дозволяють використовувати фільтри („лише HO-3, чинний після 2024-01-01“), які неможливо реалізувати за допомогою звичайного пошуку за схожістю.

З урахуванням макету / за назвою

Неструктурований метод chunk_by_title (та аналогічні у LlamaParse/Docling) дотримується меж елементів-аналізаторів: таблиці залишаються цілими, назви стають початком блоків, списки зберігають своє вступне речення. Ідеальний інструмент для контрактів та складних PDF-файлів. API для аналізу коштують грошей лише під час створення індексу. Дешевий аналізатор знижує вартість послуг — не купуйте розкішне кермо для автомобіля без двигуна.

З урахуванням коду (AST)

Не є важливим для корпусів політик, але необхідним для Terraform/Python RAG. Розділення рядків відокремлює підписи від основного тексту. tree-sitter чи інші рекурсивні розділювачі, здатні аналізувати мову, роблять розріз на вузлах функцій/класів. Це стандартний варіант, коли корпус складається з репозиторіїв чи коду інфраструктури.

Змішування правил прози та вихідного коду в одному індексі без використання розділювачів зазвичай призводить до найгірших наслідків: функції обриваються посеред тіла, а таблиці правил руйнуються через геуристики, орієнтовані на дужки.

Рівень 3: Чанкування на основі моделей — оплачуйте рішення лише там, де втрачена структура

Приблизно 84% у золотому наборі даних показали появу нової категорії помилок: неструктуровані транскрипції дзвінків без заголовків. Рекурсивні блоки по 500 токенів охоплювали зміни тем — спочатку запит про страхову виплату, потім адреса для доставки — тож ембеддинги в середньому відображали дві теми та погано підходили до жодної з них.

Семантичне чанкування

Вбудовуйте текст речення за реченням; припиніть процес там, де послідовна косинусна схожість опускається нижче певного порогу (SemanticChunker, будь-який придатний інструмент для генерації ембеддингів). Одна процедура генерації ембеддингів на кожному індексі. Метод є трансформативним для мовлення; пороги визначаються конкретним корпусом. Поріг для телефонних дзвінків розділив щільний текст політики на дрібні фрагменти — тому використовуйте семантичне членування лише для неструктурованого мовлення, а політичні тексти залишайте на другому рівні.

Розподіл за класом документа — мовлення, форми чи вікі — краще за пошук єдиного універсального інструменту для членування. Витрати на організацію полягають у використанні класифікатора чи простого переключення типу файлу під час завантаження; перевагою є менша кількість нечітких ембеддингів.

Членування на пропозиції/атомарні факти

Невеликий LLM переписує уривки на окремі твердження („Страхова сума за повінь для зони A за полісом HO-3 становить 5 000 доларів“). Точність підвищується, оскільки кожен елемент — це окремий факт із вбудованим контекстом. Вартість складає один виклик LLM на уривок, що є розумним для невеликих корпусів із високою точністю, таких як 40-сторінковий FAQ. Проблема: твердження можуть відрізнятися від оригінальних формулювань. Коли клієнти заперечують відповіді, дослівне цитування краще за парафразування. Використовуйте твердження як допоміжний засіб для пошуку та надавайте оригінальний уривок для цитування.

Організації, що займаються дотриманням правил та обробкою претензій, зрештою будуть вимагати зображення сторінки або PDF-файл із підсвіченими частинами відповіді. Заздалегідь створюйте ідентифікатори цитування, щоб індекси тверджень залишалися засобом прискорення роботи, а не основною системою зберігання даних.

Агентське чанкінг

Передайте цілий документ флагманській моделі для визначення меж. Цей підхід працює, але має низьку масштабованість: витрати зростають лінійно з розміром корпусу даних, тоді як користь — ні. Підходить для кількох сотень важливих документів; не підходить для мільйонів.

Рівень 4: Часткове оброблення за часом пошуку — шукати в малих одиницях, повертати більші

На рівнях 1–3 припускається, що одиниця, яка індексується, є тією самою, яка повертається. Це змушує робити хибний компроміс: маленькі частини дозволяють створювати чисті ембеддинги, але позбавляють ШІ контексту; великі частини забезпечують контекст, але спотворюють ембеддинги. Налаштування параметра chunk_size ніколи не дозволяє знайти універсально оптимальне рішення.

На рівні 4 ролі розділяються: одиниця пошуку, розмір якої підібраний для модуля створення ембеддингів, та одиниця передачі, розмір якої підібраний для ШІ. Критерій оцінки: якщо вам потрібен додатковий сховище (docstore, карта батьківських елементів, дерево), ви знаходитеся на рівні 4.

Батьківський документ / від малого до великого

Індекс маленьких дочірніх елементів (150–200 токенів). При пошуку повертайте більший батьківський елемент (цілу розділ 4.2 або підрозділ об’ємом близько 1 500 токенів):

from langchain.retrievers import ParentDocumentRetriever
from langchain.storage import InMemoryStore

parent_splitter = RecursiveCharacterTextSplitter(chunk_size=1500)
child_splitter = RecursiveCharacterTextSplitter(chunk_size=200)

retriever = ParentDocumentRetriever(
    vectorstore=vectorstore,
    docstore=InMemoryStore(),     # the "second store"
    child_splitter=child_splitter,
    parent_splitter=parent_splitter,
)
retriever.add_documents(policy_docs)

LlamaIndex AutoMergingRetriever пропонує схожу ієрархію. Додаткова вартість індексування майже дорівнює нулю — ви вбудовуєте той самий текст у менші фрагменти. Лише завдяки цьому на „золотому наборі“ показник Atlas покращився з приблизно 84% до ~91%; запит „what’s my flood deductible“ знаходив точний рядок таблиці, тоді як ШІ все ще бачив супутні примітки (включаючи визначення зони A). Операційні витрати: сховище документів, ключоване за ID батьківського елемента, яке підтримує синхронність під час оновлень.

Тут важливими є процедури видалення та версіонування: коли розділ 4.2 переглядається, дочірні та батьківські елементи мають оновлюватися разом, інакше система може знайти точний дочірній елемент, який посилається на застарілий батьківський. Обробку даних слід розглядати як транзакційну операцію з публікацією батьківського елемента, дочірніх елементів та їх вбудованих даних — а не як три окремі завдання.

Контекстуальне пошукове забезпечення

Перед вбудовуванням попросіть міні-LLM створити одне-два речення для контекстуалізації та додайте їх на початок; поєднайте це з алгоритмом BM25. Фрагменти на кшталт „Зона А: $5,000 / Зона B: $2,500“ стануть доступними для пошуку. Вартість складає один виклик LLM на кожен фрагмент — що є дуже дорого без кешування запитів; з кешуванням префікс документа залишається актуальним, а виклики на кожен фрагмент залишаються недорогими.

Пізнє розділення на фрагменти

Вбудуйте весь документ за допомогою інструменту для довгого контексту, а потім об’єднайте вектори токенів у кожному фрагменті. Вектори фрагментів передають контекст документа без необхідності окремих викликів LLM на кожен фрагмент. Цей підхід конкурує з методом контекстуального пошуку, але змушує використовувати моделі для довгого контексту.

Ієрархічний підхід / RAPTOR

Групуємо фрагменти, створюємо узагальнення, знову групуємо ці узагальнення у деревоподібну структуру; індексуємо кожен рівень. Загальні запитання стосуються узагальнень; конкретні — окремих елементів. Висока вартість роботи на 4-му рівні та складнощі під час змін документів — перебудова коштує дорого. Квартальні оновлення політик зробили RAPTOR оптимальним варіантом для Atlas.

Статичні бази знань — внутрішні енциклопедії, які оновлюються щороку — можуть переносити перебудову деревоподібної структури. Динамічні форми страхування — ні. Обирайте ієрархічні методи лише тоді, коли швидкість змін та бюджет на перебудову чітко визначені.

Що говорить оновлений план дій

Через шість тижнів після відпрацювання ситуації з пожежею в Slack Atlas досяг майже 93% ефективності на наборі з приблизно 300 запитань. Коротка версія для перегляду дизайну:

Почніть з рекурсивного розділення символів та розрізання з урахуванням структури при приблизно 500 токенах та 50% перекриття — це майже не коштує грошей та дає можливість отримати орієнтовні показники. Спочатку оновіть систему до пошуку у материнському документі, щоб розміри результатів пошуку та вихідних даних могли відрізнятися. Лише після цього інвестуйте у контекстуальний пошук, якщо під час оцінки все ще спостерігаються проблеми з відтворенням всіх даних.

Ніщо з вищесказаного не може врятувати поганий процес аналізу. Спочатку виправте процес видобутку даних, перш ніж налаштовувати інструменти розділення.

Односторінкова версія

Етап 0 — Аналіз. Підберіть відповідні інструменти аналізу для вхідних даних: структурований Markdown для оригінальних документів; текст із макетом та показниками точності OCR для сканованих документів; чистий Markdown із заголовками для HTML-документів; межі слайдів чи аркушів для презентацій; транскрипції аудіо. Це визначає максимальні можливості процесу аналізу.

Рівень 1 — Синтаксичний. Фіксований розмір лише для прототипів. Наявність перекриття визначається потенціометром (10–15%), що призводить до дублювання результатів типу top-k. Розділення речень враховує граматику, ігноруючи логіку документа. Рекурсивний підхід з коефіцієнтом 500/50 є стандартною відправною точкою, а не кінцевою метою.

Рівень 2 — З урахуванням структури. Розділення заголовків містить шляхи до розділів; ефективність залежить від якості парсера. Розташування за принципом by_title зберігає таблиці цілими; дешеві парсери скасовують цей ефект. Розділення за допомогою AST використовується для кодових репозиторіїв.

Рівень 3 — На основі моделей. Семантичне розділення здійснюється за критерієм схожості; налаштування залежать від корпусу даних. Пропозиції підвищують точність у невеликих корпусах, але можуть відхилятися від початкового формулювання. Агентні методи рідко є ефективними у великих масштабах.

Рівень 4 — Час отримання даних. Пошук у малих одиницях; доставка великих. Батьківський документ є оптимальним варіантом для максимальної ефективності. Для контекстного пошуку необхідна кешування. Розділення на частини пізніше є дешевшим, але обмежує можливості інструменту для вбудовування даних. RAPTOR устаріває під час оновлень. Якщо потрібен другий сховище, це належить до рівня 4.

Проблема, яка спричинила проблеми, насправді не стосувалася вибору між LangChain та LlamaIndex. Йшлося про необхідність дотримання структури документа перед виконанням математичних операцій, оцінку інструментів для розділення на частини за реальними запитаннями клієнтів та відмову від використання фрагментів моделі, які неможливо було б використати для отримання повної числової інформації.

Створюйте «золотий набір» на основі тих даних, які вже спричиняють проблеми у продукті — неправильні суми страхових відшкодувань, відсутні виключення, нечіткі умови — а не на основі синтетичних даних. Оцінюйте правильність відповідей та точність посилань окремо, щоб помилковий результат ніколи не виглядав як перемога. Коли з’являється новий алгоритм, необхідно, щоб він перевершив базові показники за обома критеріями, перш ніж почати працювати з реальним трафіком.

Нарешті, майте можливість негайно призупинити роботу: якщо рівень впевненості OCR щодо числового даних низький або версії батьківського та дочірнього елементів не збігаються, асистент повинен відхилити запитання про страхове відшкодування та передати його на подальше розглядання, замість того щоб вигадувати показники впевненості. Користувачі значно швидше пробачають відповідь типу «не можна підтвердити це за наданою формою», ніж випадкову помилку у розмірі п’ятисот доларів. Такий механізм відхилення має бути частиною вимог до продукту, а не лише мати місце у презентації після аналізу наслідків.