RAG без эмбеддингов для энтерпрайза (опыт ИИ-чемпионата)

 Публичный пост
10 марта 2025  1229

Как я отказался от оверинжиниринга и переместился с 30 места на 7 в Enterprise RAG Challenge. И чего не хватило до 1 места.

Это челлендж, организованный llm_under_hood (одно из главных сообществ в РФ инфопространстве по построению LLM-систем) и TimeToAct Austria.

Там были какие-то призы, но думаю, все участвовали чтобы протестить свои подходы на индустриальном датасете. Ну и конечно, чтобы потом выпендриваться местами в лидерборде – сейчас в AI не так много способов отстроиться от инфоцыган, которые прошли недельные курсы по промтингу.

Задача была сложная:

  1. Есть 100 корпоративных отчетов, в каждом 100-1000 страниц душного корпоративного текста.
  2. Есть 100 вопросов по этим отчетам ("сколько отелей у компании Х?", "Были ли изменения в дивидендах компании Y?", "На каких позициях в топ-менеджменте были изменения?", "у какой из компаний X, Y, Z самая высокая выручка?")
  3. Нужно дать ответы на эти вопросы + ссылки на страницы в отчетах, которые это подтверждают (жестокая жизнь, где бизнесу нужны пруфы)

Все бы ничего, но предполагалось, что это будет сделано за несколько часов. Сначала выдаются отчеты, есть пару часов на их предобработку/индексацию, потом выдаются вопросы и несколько часов, чтобы прогнать их через систему.

Как все успеть? Попробовать подготовить систему заранее. Где-то за неделю Ринат нагенерил 40 тестовых pdf и 30 вопросов. Получается, было время протестить на них всю систему, а когда придут новые данные, просто запустить ее на них.

Хаха, кто же знал, что все не так просто.

Вот что я делаю:

Анализирую победившее решение предыдущего челленджа

  • Daniel делает извлечение метрик из отчетов на основе structured output
  • Все извлеченные данные сохраняет в одну базу знаний, которая просто передается в промпт вместе с каждым вопросом
  • Нам это не подходит, потому что у нас
    • больше разных типов вопросов (не только про метрики)
    • больше документов
    • сами документы больше

Думаю, как поменять решение. Понятно, что нужно извлекать данные из отчетов на основе structured output, но какие данные? Смотрю на код генерации вопросов и понимаю, что там 9 типов вопросов.

Делаю похожую систему похожую на то, что у Daniel, только теперь "типов" данных стало больше – целых 9 вместо 1. Соответственно в классе Report делаю 9 списков, а не 1:

class AnnualReport(BaseModel):
    financial_metrics: list[FinancialMetric]
    leadership_changes: list[LeadershipChange]

    # Еще 7 списков

class FinancialMetric(BaseModel):
    metric_name: Literal["Total revenue", "Operating income", "Net income", ...]
    value: float
    currency: str
    period: str
    details: str

class LeadershipChange(BaseModel):
    position: str
    old_holder: str
    new_holder: str
    effective_date: str
    details: str

# Еще 7 разных типов информации

Я специально удалил из кода Field(description=...), чтобы не перегружать пост. Но вообще description важен, потому что уточняет важные детали извлечения для LLM.

Все полученные данные не влезут во входной контекст для ответа на вопрос. А даже если бы и влезли, то нерелевантная инфа настолько бы захламляла контекст, что качество было бы ужасным. Понятно, что нужно как-то определить, данные какого отчета нужны для ответа на вопрос. По сути, нужно вычленить название компании из вопроса. Как? Опять structured output и Literal 🤷‍♂️

class Response(BaseModel):
    company_name_from_question: Literal[tuple(name_to_id.keys())]

...

company_id = name_to_id[parsed_response.company_name_from_question]

Ок, нашел отчет. Но у меня по нему 9 огромных списков с данными. Передавать в промпт все 9 списков?

Нет, лучше определю, какие именно данные нам нужны для ответа на вопрос и буду использовать только эти данные, чтобы еще меньше захламлять промпт.

data_point_types = [
    "financial_metrics",
    "leadership_changes",
    # ...
]

class Response(BaseModel):
    company_name_from_question: Literal[tuple(name_to_id.keys())]
    data_type_question_asks_about: Literal[tuple(data_point_types)]

и передаю это в промпт к o3-mini.

company_id = name_to_id[parsed_response.company_name_from_question]
report = extracted_reports[company_id]

data_type = parsed_response.data_type_question_asks_about
only_relevant_data = report.model_dump(include={data_type})

prompt = f"""
<knowledge_base>
{only_relevant_data}
</knowledge_base>

<goal>Answer the question based on the found knowledges</goal>

<question>
{question}
</question>
""".strip()

Ну все, радуюсь. Прогоняю на тестовых данных, отправляю сабмишшн на тестовое API. Ошибка. Ответ не совпадает со схемой. Нет референсов. Черт! Я забыл, что нужны еще и номера страниц с пруфами.

Добавляю в первый шаг с извлечением инфы номера страниц на вход (в виде xml тегов <page_number_{i}>{page_content_i}</page_number_{i}>). И на выход для каждого data_point прошу указать откуда он. Аналогично делаю и на последнем этапе с ответом на вопрос.

Все работает. У меня еще пара часов до начала соревнования. Я уже переборщил с кофе, так что иду потренить в зал, чтобы немного переключиться.

Вообще-то, одна из самых сложных частей челленджа – доставать текст без потерь из pdf (особенно таблицы). Кто-то использовал кастомный OCR, кто-то использовал Vision модели (я чекнул, показалось дорого). Я сначала вообще использовал базовый PyPDF2, потом прогнал все через marker-pdf, он рисует красивые md таблички, правда на одну пдф могло по пол часа уходить, так что экспериментировал я с PyPDF2, а потом постепенно подсасывал те отчеты, которые выдавал Маркер. Кстати, вот уже обработанные отчеты

Сравнение:

Оригинал

Marker-pdf

PyPDF2

Вообще-то, и с pypdf работает неплохо – это на наш человеческий взгляд разница колоссальная, а для LLM что такой разделитель, что другой, разница не такая большая. Но сложные таблицы он шакалит.


Финишная кривая.

Появляются финальные отчеты. Прогоняю их через свой пайплайн и занимаюсь микроитерациями – смотрю вручную, какие знания пропускает и пытаюсь поправить, калибруя промпты и число страниц, которые подаю за раз.

Вот так выглядит финальная система:

Через несколько часов появляются и вопросы. Уже через пару минут я отправляю первый сабмишшн. Весь вечер провожу в минорных изменениях. Где-то на 5 сабмишшене до меня доходит, что вопросов столько же сколько отчетов, они генерировались рандомно, а значит, не все отчеты вообще нужны. Выбрасываю лишние из пайплайна и из очереди в маркер.

На следующее утро до меня доходит еще одна вещь. Раз вопросов примерно столько же сколько отчетов, значит, по одному отчету не бывает много вопросов. Значит извлеченные знания почти не переиспользуются. Тогда зачем они?

И вместо того, чтобы пытаться заранее предугадать, какие данные могут понадобиться из отчета и доставать их все, я могу тупо доставать данные сразу под конкретный вопрос.

Система упрощается на порядок.

  1. Асинхронно делаю запрос к gpt-4o-mini под каждую страницу отчета "есть тут ответ на вопрос или нет"?

  1. Дамплю все страницы с положительным ответом в промпт и прошу o3-mini ответить на вопрос.

Первое решение получило 89 баллов из 133. Последнее 110. А самое главное, что аналогичное решение поделило 1-е место получив 121.6 баллами.

Чем оно отличалось? Я слишком поздно увидел 8 вопросов, которые требуют данных сразу из нескольких отчетов (в тестовых данных такого не было). К дедлайну на призовые места я успел сделать только нерабочий костыль.

А победитель поступил очень просто – распилил такие вопросы на несколько, каждый из которых требует данных из одного отчета, а это мы умеем. А потом просто одним доп.запросом собрал ответ из всех ответов на эти вопросы.

template = r"Which of the companies had the lowest (?P<metric>.*?) in EUR at the end of the period listed in annual report: (?P<companies>.*?) If data for the company is not available, exclude it from the comparison. If only one company is left, return this company"

...

subquestions = [{
    "text": f"For {company}, what was the value of {metric} at the end of the period listed in annual report? If data is not available, return 'N/A'.",
    "kind": "number"
} for company in companies]

⬆️ Это чисто моя спекуляция о том, как оно примерно выглядело.

Главный вывод

  1. Всякие хитрые архитектуры поиска, ембеддинги и прочее – просто способ снизить стоимость запросов к LLM, заранее отбросив побольше лишней информации.
  2. Запросы становятся все дешевле, и уже сейчас можно использовать gpt-4o-mini как search engine. А учитывая асинхронность, это еще и быстро – пройтись по всем страницам отчета можно за 10-20 секунд. Стоимость ответа на один вопрос - 10 центов. Вручную его искать можно несколько часов. Выгода для бизнеса очевидна.
  3. Уверен, в будущем таких "тупых" архитектур будет все больше. Уже сейчас они делят 1 место со сложными комбайнами (где парень даже переписал часть либы под себя 🫨).

А еще, я пишу про такие штуки в AI и грабли. Например, у меня есть серия о structured_output, который постоянно встречается в посте

33 комментария 👇
Denis Sobolev Питонист, дата скрапер, создатель и внедрятель ЛЛМ агентов под задачи 10 марта в 09:51

А потом прилетает тот самый пдф на 1000 страниц и доки с поломанными шрифтами. XD

Не знал, что ты есть и в Вастрике.) Поздравляю с крутым результатом.)

  Развернуть 1 комментарий

@Creol, Дааа, поломанные шрифты это подстава. Зато полный реализм. Ну и дополнительный плюсик в пользу использования vision моделей.

  Развернуть 1 комментарий

@nikolay_sheyko, Ага, или OCR при обнаружении таких страниц.)

  Развернуть 1 комментарий
Иван Бурнатов градостроительное проектирование 10 марта в 10:59

Спасибо за пост, было познавательно!

Асинхронно делаю запрос к gpt-4o-mini под каждую страницу отчета "есть тут ответ на вопрос или нет"?

Слушай, я когда подобное делал, меня мучали сомнения, а если данные будут так разбиты между страницами, что на стр. 1 выдаст НЕТ, на стр. 2 тоже выдаст НЕТ, а если обе послать одновременно, то выдаст ДА.

И поэтому я сам делил все на чанки и подавал внахлёст, и это было довольно геморрно, а ты получается просто забил на этот момент и вышло все равно нормально?

  Развернуть 1 комментарий

@orbit, В PDFках этого соревнования было не очень много кейсов с разорванным текстом. Мб это какая-то специфика состаления финансовых годовых отчётов. Там прям видно, что составители старались не дробить одну мысль на страницы.
Ну а если где дробление и было, то дробился в основном пространный описательный plain text, который ценности не представляет.
Разорванных таблиц не было, слава богу)

  Развернуть 1 комментарий

@IlyaRice, потому что это здравый смысл, ага: если данные в критически важном документе разорваны, то от этого может пострадать слишком многое при любом повреждении, перепутанном порядке, автоматической или ручной обработке - это банально неудобно и самим людям, которым потом с этим работать.)

  Развернуть 1 комментарий

@Creol, согласен. Но я столько pdf перевидал, что уже меньше верю в здравый смысл)
Я каких извращений только не видел. Разорванные таблицы - это ещё не бОльшее из зол

  Развернуть 1 комментарий

@IlyaRice, не, таблицы как раз норм. А вот рвать плейнтекстовые факты - это дааа. Кстати, парсеры очень орно палят, какие отчеты и даже месоами в каком порядке добавления элементов делались дизайнерами и как они дообились или забивали болт на то, как реализовывать визуал и оформление отчёта.

  Развернуть 1 комментарий

@Creol,

здравый смысл

Эхехе, не то, на что стоит рассчитывать

  Развернуть 1 комментарий

@orbit, да, по формулировкам вопросов и структуре отчетов мне показалось, что вероятность таких кейсов минимальна, и я не стал извращаться, хотя желание было.

  Развернуть 1 комментарий
Boris Chernetsov Оркестрирую AI агентов 1 день, 3 часа назад

Классная статья и хороший результат. Применяем очень похожий подход для решения задач патентного анализа, где приходится анализировать сотни (а иногда и тысячи PDF), так же этот метод отлично работает для создания отчетов типа Deep Research по локальной базе документов.

Лично для меня до сих пор остается загадкой почему в open source до сих пор нет (или я просто не могу найти?) инструментов для обработки больших массивов данных подобным образом. Вместо этого все помешаны на RAG, который просто архитектурно не способен решать такие задачи.

  Развернуть 1 комментарий

@b557, А что имеется в виду под таким инструментом? Так и так, текущее решение - это всё равно RAG, ведь есть и retrieval, и augmentation, и generation, просто без упрт вещей типа эмбеддингов и векторизации всего, что только можно.

  Развернуть 1 комментарий

@Creol, ну формально согласен, но все-таки когда говорят про классический RAG - это больше про поиск и ранжировку результатов, хорошо подходящий под задачи "поиск иголки в стоге сена". Эмбеддинги, индексы, векторы - вот это вот все.
Я вот не видел нигде RAG в разрезе реализации своего Deep Research для генерации сводных отчетов по множеству источников, может что-то упустил?

  Развернуть 1 комментарий

@b557, обычно такое в AI+BPA комбайнах в качестве доп. инструмента, вроде бы, как в том вчерашнем посте на Хабре про В2В для зумеров...

  Развернуть 1 комментарий

Поздравляю ) подписался почитать

  Развернуть 1 комментарий

Спасибо за пост

Немного неясно про самый первый шаг: на основе чего делался выбор конкретной pdf-ки для последующего постраничного парсинга? Или имена pdf файлов уже содержали в себе необходимую инфу, чтобы заматчить вопрос к pdf ?


Вообще да, ощущение что эра файнтюнинга и кастомных rag-ов (года 22-23) уходит, тк проще тупо раздробить огромный контекст на подмножества и сделать N запросов в модели , а огромное окно контекста позволяет добрать недостающее знание если таковое необходимо модели

  Развернуть 1 комментарий

@AlexanderSh, вопросы включали полные и корректные названия компаний. А инфа о таком полном названии компании, соответствующее ей имя документа и её основная индустрия тоже предоставлялись в жсон-файлике метаданных набора документов.

  Развернуть 1 комментарий

@Creol, а, понял, тогда маппинг простой получается

Интересно , стоило ли вообще брать openai модели для всего? возможно, для чтения 1-страничных pdf хватило бы gemini, а потом уже после идентификации закинуть все в более умную модель для ответа ?

  Развернуть 1 комментарий

@AlexanderSh, там и дипсика хватило бы. Просто все доки по 100+ страниц. В конечном итоге, самым простым способом оказалось вообще не парсить ручками, а сразу пихать страницу пдф в виде файла в ллм и задавать промптом вопросы. И так постранично. А потом собирать базу ответов. И да, гемини, дипсик, гпт, клод, экспериментальный ИИ от IBM - все спокойно это хавают.

  Развернуть 1 комментарий

@AlexanderSh, да, хватило бы любой тупой модели, вопрос тут был скорее в rate limits. У меня в openai большие лимиты, поэтому я мог асинхронно тысячи запросов слать и не париться. Быстрее скорость итераций

Ну и бонусом у OpenAI есть встроенное сохранение запросов, удобно смотреть, если нужно найти как на каком-то отчете отработало

  Развернуть 1 комментарий

@nikolay_sheyko, Ого! Я только сейчас узнал, что можно нативными возможностями OpenAI запросы сохранять. Это каким-то флагом включается, полагаю?

  Развернуть 1 комментарий

@IlyaRice, ага

store=True,
metadata={
    "function": "check_if_page_contains_relevant_data",
    "report_id": report_id,
    "page_number": str(page_number),
    "question": question,
},
  Развернуть 1 комментарий

@IlyaRice, там еще из крутого, можно потом все эти же данные бесшовно использовать для умного eval или даже для дистилляции.

  Развернуть 1 комментарий

@nikolay_sheyko, ага, уже глянул в доке, круто!
Хотя я всё ещё мечтаю разобраться и настроить какой-нибудь удобный LLM Observability. Чтобы прям структурно отслеживать взаимосвязь промптов.

  Развернуть 1 комментарий

@IlyaRice, Жизненно, я пока не нашёл хорошего сетапа. Хотя observability у Pydantic AI выглядит интересно, но не хочется на фреймворк целиком завязываться

  Развернуть 1 комментарий
  Развернуть 1 комментарий

@nikolay_sheyko
у меня вопрос одновременно по теме и не по теме :)
Есть у нас инициативная группа, которая только начинает прокачиваться именно вот в этой теме, условно говоря "Как проектировать, тестировать и строить архитектуру приложений вокруг этих ваших нейронок". То есть, это не тренинг моделек, не тюнинг, а именно построение своего комплекса приложений, которые бы решали сугубо наши, внутренние, специфичные для нас задачи.

Собственно, вопрос в том, где и как искать обучающие материлы по этой теме, как прокачиваться именно в этом направлении?

  Развернуть 1 комментарий

@Spaider, Очень крутой запрос. Мне кажется, это как раз самый важный скилл (по сути именно он и отличает решения с 110+ баллами от решений, которые набрали условные 90).

Но единственный подходящий курс, который я знаю – это курс Рината (организатора челленджа):
https://t.me/llm_under_hood/499

Я его не проходил, так что ручаться не могу, но, кажется, что сложно будет найти что-то лучше.

Ну, либо я могу вам личный мини-интенсив сделать, заточенный под ваши задачи, я такое люблю и умею

  Развернуть 1 комментарий

@nikolay_sheyko, а куда писать-то?

  Развернуть 1 комментарий
  Развернуть 1 комментарий

@nikolay_sheyko, я проходил курс Рината, хороший обзор архитектур решений

  Развернуть 1 комментарий

@Spaider, там особо нечего учить. Подаёшь запрос на натуральном языке, как результат получаешь структирированный вывод.

Накручиваешь контроль ошибок, подачу нужных кусков текста, контроль качества.

  Развернуть 1 комментарий

@orbit, Сейчас буду звучать, как инфоцыган, который пытается убедить, что курс очень-очень нужен 😅 Но у меня реально почти нет кейсов, когда одним запросом вот так всё решается.

Тут, если я верно понял задачу, хочется как раз научиться понимать, как сложную задачу разбивать на последовательность простых шагов и как накручивать на них контроль качества.

Если такого никогда не делалось, там достаточно граблей, чтобы их насобирать. Иначе все решения в челленже получили бы 120+

  Развернуть 1 комментарий

😎

Автор поста открыл его для большого интернета, но комментирование и движухи доступны только участникам Клуба

Что вообще здесь происходит?


Войти  или  Вступить в Клуб