- BrainTools - https://www.braintools.ru -
Треть нашей базы знаний лежит в выгрузках Excel, и на них ИИ-ассистент отвечает хуже всего. Разбираем, что теряется по дороге от ячейки до индекса.
У нас внутренний помощник по методологии. Он ищет по базе из нескольких сотен документов: регламенты в PDF, презентации, записки и примерно треть корпуса в Excel. Справочники категорий, прайс-листы, списки контрагентов. Всё это мы порезали на куски и сложили в векторный индекс, тридцать пять тысяч кусков.
Сотрудник спрашивает: «Является ли Северянка нашим партнёром?»
Ответ лежит в таблице. Название сети в колонке A, признак партнёрства в колонке B. Помощник эту строку не находит.
Дальше мы разберём, почему так вышло.
Короткий ликбез для тех, кто не собирал такое руками. Кто собирал, листайте до следующей главы.
RAG расшифровывается как retrieval-augmented generation, поиск плюс генерация. Языковая модель не знает ваших внутренних документов и при первой возможности придумает ответ сама. Поэтому перед ответом мы ищем по базе, подкладываем модели найденные куски и просим отвечать по ним.
Всё держится на шаге поиска. Не нашли нужный кусок, и модель ответит складно и мимо.
Ищут двумя способами, мы используем оба.
Лексический поиск считает совпадение слов. Классический BM25 смотрит, сколько слов запроса встретилось в куске, и взвешивает их по редкости: слово «ритейлер» весит больше слова «данные», потому что встречается реже. Он точен и прямолинеен. Спросите «аптеки», а в документе написано «аптека», и без приведения слов к начальной форме он пройдёт мимо.
Семантический поиск сравнивает смысл. Модель-эмбеддер превращает текст в вектор из нескольких сотен чисел, и тексты про одно и то же оказываются рядом в этом пространстве. «Аптечная сеть» и «фармацевтический ритейлер» окажутся соседями, хотя общих слов у них нет. Взамен он теряет точность: близкие по смыслу куски он путает, а артикул, код или цифру держит плохо.
Мы запускаем оба и сливаем две выдачи в одну.
Оба способа стоят на одном допущении: нужный кусок чем-то отличается от соседей. У прозы так и есть. Абзац про методологию аудита написан не теми словами, что абзац про сроки поставки, и поиск разведёт их без труда.
Лист таблицы устроен наоборот. Возьмите прайс на двести категорий товаров. Все строки написаны по одному шаблону: код, название, покрытие, цена. Отличаются они значениями в паре ячеек. Для поиска такие строки выглядят почти одинаково, потому что одинаковыми их сделал аналитик, и в этом весь смысл таблицы.
К этому добавляются ещё две вещи.
Слов в строке мало, и половина из них — это коды. Лексическому поиску не на чем считать совпадения, семантическому не из чего лепить смысл.
Смысл строки лежит не в строке. Что означает колонка, написано в шапке листа. О чём вообще таблица, написано в имени файла. Человек читает лист сверху вниз и держит это в голове, а в индекс приезжает одна строка без всякого окружения.
Отсюда и берётся разрыв, который мы меряли дальше по тексту.
Откройте лист в Excel. Вы увидите примерно вот это:
A B C D E
1 Каналы National (Urban)
2 Название Партнёр Гипер- Супер- Мини-
3 сети маркеты маркеты маркеты
4 СЕВЕРЯНКА Да 91 4 1
5 ГАСТРОНОМ 24 Нет — 12 3
Вы читаете это без труда. Слева кто, справа сколько и где. Надпись «Каналы National (Urban)» стоит над тремя колонками сразу, её растянули объединённой ячейкой.
Парсер ничего этого не видит. Для него лист выглядит как сетка. Он не знает, где кончается шапка и начинаются данные, а также не знает, что надпись в первой строке относится к трём колонкам.
Возьмите за шапку первую строку, отрендерьте строку как есть, и в индекс уедет вот такая запись:
строка 4: A=СЕВЕРЯНКА | B=Да | C=91 | D=4 | E=1
Ни «названия», ни «партнёра», ни «гипермаркетов». По такой записи вы найдёте строку только по имени самой сети.
Мы собираем её иначе:
Лист "Сети", строка 4:
A (Название сети)=СЕВЕРЯНКА;
B (Партнёр)=Да;
C (Каналы National (Urban) / Гипермаркеты)=91;
D (Каналы National (Urban) / Супермаркеты)=4
За этим стоят два приёма. Первый: мы копируем надпись из объединённой ячейки во все колонки, которые она накрывает. Второй: мы склеиваем два этажа шапки в одно имя через слэш.
Букву колонки мы оставляем рядом с именем. В таблице буква однозначна всегда, а имя колонки бывает пустым, повторяется или написано по-английски.
Пустые ячейки выбрасываем. На листе в двести колонок, где сотрудник заполнил восемь, запись иначе на 96% состоит из K=; L=; M=;.
Посмотрите на ту же картинку ещё раз:
A B C D E
1 Каналы National (Urban)
2 Название Партнёр Гипер- Супер- Мини-
3 сети маркеты маркеты маркеты
Объединённая ячейка накрывает C, D и E. Над A и B пусто. Имена этих двух колонок аналитик написал во второй строке шапки, и больше нигде.
Наш парсер брал имена из первой строки. Для правой части листа он их нашёл, для левой нет. В индекс уезжало такое:
Лист "Сети", строка 4: A=СЕВЕРЯНКА; B=Да;
C (Каналы National (Urban))=91; …
Правую половину строки парсер описал, левую бросил. Ответ про партнёрство сидит как раз в левой: A=СЕВЕРЯНКА; B=Да.
Слова «партнёр» в записи нет. Из вопроса сюда попадает одно название сети, и этого мало, чтобы строка обошла тридцать пять тысяч конкурентов.
Правка заняла десяток строк кода. Имена колонок, которых нет в первой строке шапки, мы берём из второй. Вопрос начал отвечаться.
Библиотека openpyxl [1] читает файл двумя способами. Обычный держит лист в памяти [2] целиком и знает про объединённые ячейки. Режим read_only идёт по файлу потоком, памяти ест мало, про объединённые ячейки молчит.
Книгу на сотню мегабайт вы обычным способом не откроете. Значит, у файлов крупнее порога шапка собирается хуже. Мы про эту границу знаем и пока живём с ней.
Допустим, вы собрали запись идеально. Строка попала в индекс со всеми именами колонок. Поиск всё равно достаёт её реже, чем абзац из PDF.
Мы посчитали, какая доля знаменательных слов вопроса встречается в том куске, где лежит ответ.
Слова вопроса, которые встречаются в куске с ответом
|
Откуда кусок |
Доля слов |
|---|---|
|
Проза: PDF, DOCX, презентации |
50% |
|
Строка таблицы |
32% |
У части табличных вопросов пересечение нулевое. Вот два примера из нашего набора.
«Какой новый контрагент добавляется в отчёт?» Размеченная строка целиком:
строка 192: A=ЯБЛОНЬКА;. Общих слов ноль. В строке нет ни «нового», ни «контрагента», ни «отчёта». Чтобы понять, что строка новая, надо сравнить файл с прошлой версией. Такого знания в строке нет, и никакой парсер его туда не добавит.
«Как найти категорию корма для животных?» Строка выглядит как
RSCAT; CAT; CAT-CAT FOOD; NSCATST. Русских слов нет вообще. Тут нужен переводчик, а поиск по словам бессилен.
Проза отвечает на вопрос теми же словами, которыми его задают. Таблица отвечает значением ячейки.
Мы искали и сперва решили, что проблемы как будто не существует. В туториалах по RAG её нет. В статьях про нарезку документов вы найдёте абзац «сохраняйте заголовки таблиц» и дальше про размер окна.
Литературы про это написано много. Она лежит под другими именами и решает соседнюю задачу. Коротко, кто что сделал.
WikiTableQuestions, 2015. Вопросы к одной таблице из Википедии. Ответ необходимо вычислить: сложить, отфильтровать, сравнить. Таблицу выдают вместе с вопросом.
WikiSQL, 2017. Восемьдесят тысяч пар «вопрос — SQL-запрос» к одной таблице. Модель учится переводить фразу в запрос.
Spider, 2018. То же самое, но к базе из нескольких связанных таблиц и на незнакомых доменах. Здесь впервые появляется выбор таблицы: модель видит схему базы и решает, к какой обращаться.
TAPAS, 2020. К BERT добавили эмбеддинги строки, колонки и ранга и обучили на паре миллионов таблиц из Википедии. Модель отвечает выбором ячеек, без всякого SQL.
TAPEX, 2021. Модель предобучили исполнять SQL на синтетических запросах. Она научилась вести себя как маленький движок таблиц.
FeTaQA, 2021. Ответом является не одна ячейка, а связная фраза, собранная из нескольких.
Плюс обзоры по text-to-SQL и по работе языковых моделей с таблицами, где всё это разложено по полочкам.
Таблица уже выбрана. В TableQA её отдают вместе с вопросом. У Spider выбор есть, но по схеме базы: имена таблиц и колонок известны заранее, их десятки. У нас несколько сотен листов, и в какой лезть, никто не сказал.
Таблица чистая. Прямоугольник, одна строка шапки, колонки подписаны. У нас два этажа шапки, объединённые ячейки, преамбула в первой строке и лист на сто восемьдесят колонок.
Вопрос задан словами таблицы. Разметчик формулирует вопрос, глядя на неё. Наш сотрудник таблицы не видел и спрашивает своими словами.
Корпус однородный. Там только таблицы. У нас строка таблицы конкурирует за место в промпте с абзацем из PDF и проигрывает ему по всем признакам.
Меряют другое. Там точность ответа при данной таблице. Нам важно, доехала ли нужная строка до модели вообще.
Ближе всех к нашей боли [3] подошла автор работы про структуру листа (arXiv 2609.20732 [4]). Она собрала 480 вопросов по 80 листам и разобрала жалобы практиков на форумах. Топ причин отказа: потеря контекста при нарезке в 59% обсуждений, сложные шапки в 37%, рваная разметка в 29%. Второй пункт вы прочитали выше целиком. Её метод: не писать строку целиком, а разложить её на отдельные утверждения, где над каждым числом стоит его заголовки.
было: СЕВЕРЯНКА | 91 | 4 | 1
стало: СЕВЕРЯНКА · Гипермаркеты = 91
СЕВЕРЯНКА · Супермаркеты = 4
СЕВЕРЯНКА · Минимаркеты = 1
Модели это помогает: спросят про супермаркеты, и она не схватит 91 из соседней колонки.
Поиску не помогает: слово «Супермаркеты» стоит во всех 192 строках листа и потому не отличает их друг от друга. Различает по-прежнему только название сети.
Если в строке нет слов вопроса, давайте их туда добавим. Выдумывать мы ничего не стали. При заливке файла агент читает лист и пишет про него два предложения: что за таблица, что означает одна строка, что лежит в колонках. Эту шапку мы приписываем каждому куску листа. Сотрудник спрашивает про любую строку, а не только про первую.
Прайс-лист на базы по категориям товаров, где каждая
строка это категория с английским и русским названием.
В строке также код покрытия и стоимость.
Лист "Цены", строка 12: B (Название ENG)=TOTAL AIR
FRESHENERS; C (Покрытие)=NT; D (Название RUS)=…
Первый абзац и есть шапка, дальше данные как были. Кусок вместе с шапкой может перестать влезать в окно эмбеддера. Тогда мы режем его по границам строк на столько частей, сколько нужно, и шапку получает каждая. Саму запись резать нельзя: половина строки хуже, чем строка без шапки.
Мы собрали 155 табличных вопросов, где ответ это конкретное значение ячейки. Поиск во всех плечах наш обычный, отличается только обращение с шапкой.
155 табличных вопросов, ответ сверяется по значению ячейки
|
Что делаем с шапкой |
Ответ верен |
Кусок дошёл |
|---|---|---|
|
не делаем ничего |
60% |
72% |
|
кладём в индекс |
63% |
80% |
Восемь пунктов по доставке нужного куска.
Мы разделили вопросы по тому, есть ли в них слова из шапки нужного листа.
Доля вопросов, где нужный кусок дошёл до модели
|
Какие вопросы |
Без шапок |
С шапками |
|---|---|---|
|
Заданы словами таблицы, 112 вопросов |
68% |
78% |
|
Слов таблицы в вопросе нет, 43 вопроса |
84% |
86% |
Шапка работает, когда сотрудник спрашивает словами самой таблицы: именем колонки, названием категории. На остальных вопросах она ничего не меняет.
Дальше мы захотели понять, что именно меняется внутри файла. Взяли один файл и один вопрос.
Файл — справочник товарных категорий. В нём 265 листов, по листу на категорию, и 21 197 кусков в индексе. Вопрос сотрудника: «из чего состоит категория освежителей воздуха». Ответ лежит на листе AIR, в нём 704 куска.
Мы прошли по всем 21 197 кускам этого файла и посчитали, в скольких из них встречается хотя бы половина знаменательных слов вопроса. Это грубая прикидка того, сколько кусков поиск вообще может счесть подходящими.
Справочник на 21 197 кусков, вопрос про освежители воздуха
|
Вариант |
Кусков совпало |
Как они распределены по листам |
|---|---|---|
|
без шапок |
4 |
2 на листе AIR, 2 на посторонних листах |
|
с шапками |
721 |
704 на листе AIR, то есть все его куски, и 17 ещё на шести листах |
Без шапок нужный лист ничем не выделялся: два совпавших куска на нём и два на чужих листах. Выбрать по такому сигналу правильную таблицу невозможно.
С шапками лист AIR совпал целиком, все 704 куска, а у соседних листов остались единицы. Вопрос «какая таблица отвечает» решён: перепутать лист теперь трудно.
Тут же видно и ограничение. Все 704 куска совпали одинаково, потому что совпала шапка, а она у них одна и та же. Какая из 704 строк содержит ответ, поиск по-прежнему сказать не может. Строки различаются только содержимым ячеек, а слов вопроса там нет.
Ровно про это предупреждал автор работы про роли ячеек: такая разметка помогает модели написать ответ, а поиску не помогает.
Мы меряем два числа, и разрыв между ними оказался содержательным.
Поиск достаёт 60 кандидатов, в промпт уходят 12. На размеченном наборе из 81 вопроса нужный кусок попадает в шестьдесят кандидатов в 65 случаях, а до двенадцати кусков промпта доезжает в 58. Семь вопросов теряются на порядке выдачи. Ответ найден и лежит где-то на тридцатом месте.
Строка таблицы проигрывает прозе по всем признакам разом. Она короче, слов в ней меньше, плотность совпадений ниже. На шкале релевантности она оказывается ниже абзаца, который «вообще про то же самое» и конкретного ответа не содержит. Двенадцать мест в промпте разбирают многословные соседи.
Обогащением строки мы занимаемся до сих пор. Последнее, что пробовали: агент при заливке пишет справку о файле, а мы кладём её в индекс рядом со строками. На корпусе из одних таблиц это подняло долю выполненных проверок с 19% до 23%. На нашем смешанном корпусе таблицы выросли с 42% до 75%, зато проза потеряла столько же, и в сумме вышел ноль.
Зато выбор листа мы закрыли. Раньше нужный лист набирал два совпавших куска, а случайный сосед один, и отличить их было нечем. С шапками нужный лист совпадает целиком, все 704 куска, а у ближайшего соседа их шесть. Разница в сто раз, и лист теперь выбирается однозначно.
Поэтому дальше мы делим задачу надвое. Лист ищем поиском, как сейчас. А значение внутри листа перестаём искать и начинаем брать: находим в колонке «Название сети» строку со Северянкой, читаем колонку «Партнёр». Считаем, фильтруем, берём уникальные значения. Обычный код по таблице, которому неважно, какими словами задан вопрос.
Автор: NoHatus
Источник [5]
Сайт-источник BrainTools: https://www.braintools.ru
Путь до страницы источника: https://www.braintools.ru/article/35975
URLs in this post:
[1] openpyxl: https://openpyxl.readthedocs.io/
[2] памяти: http://www.braintools.ru/article/4140
[3] боли: http://www.braintools.ru/article/9901
[4] arXiv 2609.20732: https://arxiv.org/abs/2609.20732
[5] Источник: https://habr.com/ru/articles/1086194/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1086194
Нажмите здесь для печати.