Можно ли за 72 часа(личный вызов/ограничение) в одиночку спроектировать, собрать и отладить RAG-систему, архитектура которой основана на пяти state-of-the-art научных статьях, опубликованных буквально в последние месяцы? В рамках хакатона AI for Finance Hack 2025 от Changellenge » я решил проверить это на практике. Спойлер: было больно, интересно, а результат превзошел все ожидания.

Задача хакатона была классической, но с подвохом: создать финансового AI-ассистента, который дает точные ответы на вопросы пользователей, основываясь на предоставленной базе знаний. "С подвохом" — потому что среди простых вопросов вроде "что такое ОСАГО" скрывались и сложные, многоэтапные ("multi-hop") кейсы: "сравни условия по продукту А и продукту Б", "какой был лимит по вычету в прошлом году и как он изменился сейчас?".

Стандартный RAG-подход "найди похожие документы -> передай в LLM" на таких задачах быстро ломается. Он либо не находит все части информации, разбросанные по разным документам, либо находит слишком много "шума", в котором "тонет" языковая модель.

Поэтому, вместо того чтобы строить очередной простой RAG, мы решили пойти по пути "тяжелой артиллерии" — спроектировать полноценного агентского помощника, который умеет планировать, анализировать и итеративно уточнять поиск.

Эта статья — история штурма: от амбициозного плана, собранного из передовых исследований, через жестокую реальность отладки API и библиотек, до финальной, оптимизированной и высокопроизводительной архитектуры. Я покажу, какие именно идеи из научных статей я взял, как адаптировал их под жесткие рамки хакатона и, самое главное, как решал проблемы, которые возникали на каждом шагу.


Дисклеймер: Этот проект был настоящим спринтом. Из команды в четыре человека активным участником оказался только я. Учитывая грандиозность планов и временные рамки в 72 часа(ведь надо оставить время на тестирование и непредвиденные обстоятельства), я активно использовал AI-ассистентов в роли "второго пилота" и "сеньора-ревьюера" на всех этапах — от проектирования архитектуры до написания и отладки кода. Эта статья — история нашего совместного с AI штурма.


Оглавление

  1. Правила и Ограничения

  2. Часть 1: "Наполеоновские Планы"

  3. Часть 2: Жестокая Реальность Отладки

  4. Часть 3: Производительность превыше всего

  5. Часть 4: Заглянем "Под Капот" — Трассировка и Визуализация

  6. Часть 5: Финальный Вердикт — В Поисках "Золотой Середины"

  7. Часть 6: Заключение — Главные Уроки и Будущее Проекта

  8. Заключение: История Одного Архитектурного Спринта

Правила и ограничения хакатона

Прежде чем мы погрузимся в архитектурные дебри, важно очертить "поле боя". Хакатон AI for Finance Hack 2025, организованный Changellenge  при поддержке экспертов Райффайзен Банка, ставил участников в довольно жесткие, но интересные рамки.

Стек и модели

Участникм был предоставлен ограниченный, но очень качественный набор инструментов:

  • Генеративные модели: Доступ через прокси-API к четырем моделям — Mistral Small, Llama-3-70B, Gemma-27B и Grok-3 Mini. Это сразу подтолкнуло к идее "Multi-Model" архитектуры, где для каждой задачи используется свой, наиболее подходящий инструмент.

  • Эмбеддинг-модели: Доступ к text-embedding-3-small и text-embedding-ada-002.

Бюджетные и временные лимиты

Это были главные вызовы, определившие всю мою стратегию оптимизации:

  • Денежный Лимит: 

    3$ в день на генеративные модели и 1$ на эмбеддинги. Этот бюджет был общим на команду для всех экспериментов и локальной отладки. Финальный прогон на сервере, к счастью, имел свой, отдельный бюджет.

  • Временной Лимит: 4 часа на полную обработку 500 вопросов на сервере проверки.

Запреты и "Сюрпризы"

  • HuggingFace под запретом: Организаторы ограничили использование HuggingFace для предотвращения запуска тяжелых локальных моделей, что сразу вычеркнуло из моего плана SOTA-реранкеры на базе CrossEncoder.

  • "Монолитный" main.py: Все решение должно было быть собрано в один Python-файл.

  • Реранкер "в догонку": Уже в середине хакатона, после многочисленных просьб участников, организаторы открыли доступ к API-реранкеру (Qwen3-Reranker-4B). Это было приятным дополнением, но к тому моменту моя архитектура уже была спроектирована так, чтобы показывать высокое качество и без него, полагаясь на синергию гибридного поиска и агентского анализа.

Именно эти ограничения — бюджетные, временные и технологические — стали главным драйвером для всех архитектурных решений, о которых я расскажу дальше.

Часть 1: "Наполеоновские планы"

Изначальная идея была простой: не пытаться изобрести велосипед, а встать на плечи гигантов. Я решил изучить последние (буквально за последние несколько месяцев) научные статьи, посвященные решению ключевых проблем RAG, и собрать из их идей единую, ультимативную архитектуру.

Научный фундамент: собираем "Франкенштейна" из SOTA-Идей

Мой финальный "чертеж" был основан на пяти ключевых концепциях, взятых из пяти блестящих работ:

  • Итеративный Цикл и Анализ Пробелов из FAIR-RAG

    • Проблема: Простой RAG не умеет "дособирать" информацию. Если он с первого раза не нашел все нужные факты, он выдаст неполный ответ.

    • Идея: Превратить RAG в итеративный, агентский процесс. После первого поиска специальный "Агент-Аналитик" (в статье он называется SEA) проверяет, достаточно ли найденной информации для ответа на все части вопроса. Если нет, он определяет "пробелы" в знаниях, и второй "Агент-Уточняющий" генерирует новые, точечные запросы, чтобы закрыть именно эти пробелы. Цикл повторяется.

    • Моё Решение: Это стало сердцем моей архитектуры. Я решил реализовать этот агентский цикл с помощью промпт-инжиниринга, создав двух специализированных агентов.

Схема из статьи
Схема из статьи
  • Глобальное Планирование из GlobalRAG

    • Проблема: Для сложных вопросов ("сравни А и Б") простой поиск по всему вопросу неэффективен.

    • Идея: Прежде чем что-либо искать, LLM должна составить "глобальный план" — разбить сложный вопрос на простые, независимые под-вопросы.

    • Моё Решение: Я решил встроить это как альтернативную стратегию (DECOMPOSE). Для явно сравнительных вопросов система сначала вызывает "Агента-Декомпозитора", который создает план, а уже потом запускает параллельный поиск по всем пунктам этого плана.

Схема из статьи
Схема из статьи
  • Граф Концептов из Graph-Guided Concept Selection (G2ConS)

    • Проблема: Семантический поиск хорош, но он не видит неявных связей между документами.

    • Идея: Вместо дорогого графа знаний, можно построить дешевый, но эффективный граф "концептов" (ключевых слов). Если "ОСАГО" и "ДТП" часто встречаются в одних и тех же документах, между ними в графе будет сильная связь.

    • Моё Решение: Это стало основой для моей "Головы D" в поисковом движке. Я реализовал создание такого графа с помощью TF-IDF и networkx.

Схема из статьи
Схема из статьи
  • Иерархический Поиск из LATTICE

    • Проблема: "Плоский" граф из тысяч концептов все еще может быть слишком "шумным".

    • Идея: Сгруппировать концепты в иерархию (например, "темы" -> "концепты").

    • Мое Решение: Я не стал строить полное дерево, а адаптировал идею, добавив еще один слой в "Голову D". Сначала я кластеризую все концепты на "темы" с помощью KMeans, а затем поиск по графу происходит в два этапа: сначала ищем релевантные темы, а потом уже связи внутри них.

Схема из статьи
Схема из статьи
  • Динамический Выбор Контекста из Dynamic Passage Selector (DPS)

    • Проблема: Жесткий лимит top_k (например, брать всегда 10 документов) неоптимален. Для простого вопроса нужен 1 документ, для сложного — 15.

    • Идея: После грубого поиска заставить LLM выступить в роли "Агента-Селектора", который сам выберет минимально необходимый набор документов из найденных кандидатов.

    • Моё Решение: Я заложил это в архитектуру как возможное улучшение — еще один агентский шаг после поиска.

Схема из статьи
Схема из статьи
  • Структурированное Мышление из Chain-of-Conceptual-Thought (CoCT)

    • Проблема: Стандартный "Chain-of-Thought" ("думай шаг за шагом") слишком неструктурирован. LLM может "уйти в сторону" в своих рассуждениях.

    • Идея: Заставить модель думать "концепциями". Перед тем как писать текст, она должна сначала сгенерировать тег-план, например: <концепт:ТЕЗИС>, <концепт:ДЕТАЛИЗАЦИЯ>, <концепт:ВЫВОД>.

    • Моё Решение: Это стало основой промпт-инжиниринга для всех моих агентов. Я решил, что каждый "умный" компонент системы — будь то SeaAgent или финальный Generator — должен сначала формировать концептуальный план своих действий, а уже потом выполнять их. Это должно было повысить предсказуемость и качество их работы.

"Чертеж на салфетке": финальная архитектура

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

Финальная схема 'Агентского RAG' со всеми компонентами: 4-головым гибридным ретривером и тремя переключаемыми стратегиями (ITERATIVE и DECOMPOSE, и простой RAG)
Финальная схема 'Агентского RAG' со всеми компонентами: 4-головым гибридным ретривером и тремя переключаемыми стратегиями (ITERATIVE и DECOMPOSE, и простой RAG)

Вооружившись этим планом, я приступил к реализации, еще не зная, что почти каждая красивая стрелочка на этой схеме превратится в часы отладки, а API и организаторы хакатона приготовили свои "сюрпризы".

Часть 2: жестокая реальность отладки

Любой красивый архитектурный план остается лишь теорией до первого запуска. Мой случай не стал исключением. Этап реализации превратился в настоящее детективное расследование, где на каждом шагу меня ждали новые "сюрпризы" от API, библиотек и правил хакатона и собственной невнимательности.

Война с API или как я невнимательно прочёл документацию

Первой и самой болезненной проблемой стали API-вызовы. Архитектурно я решил использовать библиотеку litellm — это блестящий "швейцарский нож", который предоставляет единый интерфейс для работы с сотнями LLM от разных провайдеров. План был прост: написать один LLMClient и затем легко переключать модели (Llama, Gemma, Mistral) в конфиге, проводя A/B-тесты.

Но моя ошибка была классической: я невнимательно прочитал документацию к этому мощному инструменту, понадеявшись на его "интуитивно понятный" интерфейс. Это стоило мне пары часов отладки.

Симптомы:
Мой LLMClient на litellm начал выдавать загадочные ошибки 401 AuthenticationError.

  • Логи показывали, что запросы к эмбеддинг-модели уходят не на прокси-сервер хакатона, а напрямую на api.openai.com.

  • Запросы к генеративным моделям падали с ошибкой key not allowed, хотя в списке разрешенных эта модель была.

Я попал в "парадокс конфигурации":

  1. Отправляю model = "meta-llama/..." -> litellm ругается, что не указан провайдер.

  2. Отправляю model = "openrouter/meta-llama/..." -> Прокси-сервер ругается на "неверный ключ".

Я перепробовал все: добавлял префикс openai/, менял клиентов, но каждый раз натыкался на новую стену. Казалось, что litellm и прокси-сервер говорят на разных языках.

Решение нашлось, когда я отбросил предположения и вернулся к первоисточнику — документации litellm. Там черным по белому было написано, что для работы с кастомными прокси, имитирующими OpenAI (а именно такой был у организаторов), нужно активировать специальный "главный рубильник":

# Заставить litellm работать в режиме "прокси"
litellm.use_litellm_proxy = True

Эта одна строка заставила litellm принудительно отправлять любой запрос на указанный api_base, решая проблему с генеративными моделями. А для эмбеддингов, чтобы гарантировать 100% стабильность, я принял "силовое" решение — использовать нативный openai SDK, который корректно работал с base_url из коробки.

Мораль: RTFM (Read The F... Manual). Даже самые мощные и гибкие инструменты, как litellm, имеют свои особенности. 90% "загадочных" проблем с API — это не баги, а невнимательно прочитанная документация. Не доверяйте интуиции, доверяйте первоисточнику. Это сэкономит вам часы (или даже дни) отладки.

"Война с Ограничениями"

Едва я победил API, как столкнулся с последствиями моей собственной неверной интерпретации правил хакатона.

Проблема №1: Запрет на HuggingFace.

В первый день хакатона, организаторы сообщили:

"Принято решение ограничить использование HuggingFace с целью недопущения применения высоконагруженных моделей."

Это был удар под дых. Мой план включал нейросетевой реранкер (CrossEncoder из sentence-transformers) — "тонкую оптику" для очистки результатов поиска. Этот компонент, загружаемый с HuggingFace, оказался вне закона.
Адаптация: Пришлось отказаться от этой идеи и сделать ставку на повышение качества "грубого" поиска. Я решил компенсировать отсутствие реранкера двумя вещами: внедрением MMR (Maximal Marginal Relevance) для повышения разнообразия результатов и усилением "мозгов" — моих агентских компонентов.

Проблема №2: Неверная Интерпретация "Точки Входа".
Правила гласили, что решение должно быть представлено в виде zip-архива с main.py, который "включает всю логику". Я, как и любой инженер, привыкший к модульной разработке, воспринял это как стандартное требование к "точке входа". Мой план был прост: main.py будет импортировать компоненты из аккуратной структуры папок (rag_components/, utils/), а в zip-архив я положу все эти папки.

Но затем в чате хакатона появилось уточнение:

"предполагается, что весь код для запуска будет находиться в файле main.py"
"...допустимы только 2 файла"

Это полностью разрушило мою стратегию. Сервер проверки ожидал один монолитный файл, а не проект с кучей импортов.

Адаптация: Отказаться от чистой архитектуры в разработке было бы самоубийством. Поэтому я принял единственно верное инженерное решение: "разрабатывай модульно, сдавай монолитно". Я написал скрипт сборки (build.py), который автоматически "склеивает" все мои 10+ Python-файлов в один гигантский submission_main.py, удаляя локальные импорты и собирая внешние в единый блок. Это позволило мне сохранить чистоту и управляемость в разработке, но при этом сдать решение в том странном формате, который требовали организаторы.

Мораль: Всегда перепроверяй даже самые, казалось бы, очевидные правила. Фраза "точка входа" может означать не то, к чему ты привык. И если требования окружения противоречат здравому смыслу, не ломай свой рабочий процесс — автоматизируй адаптацию к этим требованиям.

Часть 3: Производительность превыше всего

После всех сражений с API, Pylance и правилами, я наконец-то получил работающий MVP. Но первый же полный прогон на 15-20 вопросах показал неутешительную картину: среднее время ответа на один вопрос составляло ~35-40 секунд.

Простая экстраполяция на 500 вопросов давала более 5 часов работы. При лимите хакатона в 4 часа это был гарантированный провал. Стало очевидно, что моя "идеальная" архитектура с 2-3 итерациями, хоть и была умной, оказалась непозволительно медленной.

Настало время безжалостной, но основанной на данных оптимизации. Я использовал собственный ResourceManager как "приборную панель" и начал последовательно "дергать за рычаги".

Рычаг №1: Радикальное Сокращение Итераций

Логи показали, что 90% времени "съедают" API-вызовы. Самый очевидный способ их сократить — уменьшить количество итераций. Я изменил в config.py всего одну строку:

MAX_ITERATIONS = 1

Это превратило мой сложный FAIR-RAG цикл в более простой, но быстрый пайплайн: Поиск -> Анализ (SEA) -> Генерация. RefinementAgent больше не вызывался. Это сразу же сократило количество LLM-вызовов на один вопрос с 3-4 до двух.

Рычаг №2: Правильный Инструмент для Каждой Задачи (Multi-Model)

Я запустил ab_test_models.py, чтобы проверить, какая комбинация моделей самая эффективная. Результаты были однозначными:

  • Llama-3-70B — идеальный "аналитик". Она блестяще справлялась с задачами SeaAgent, требующими логики и следования формату JSON.

  • Но в роли "писателя" (Generator) она была медленной и генерировала слишком краткие ответы.

  • Mistral-Small и Gemma оказались гораздо более быстрыми и "разговорчивыми" генераторами.

Это подтвердило мою "Multi-Model" стратегию. Финальный выбор пал на Llama-70B для агентов и Mistral-Small для генерации — лучший баланс "ума", скорости и качества.

Рычаг №3: Распараллеливание Всего, или Вспоминая про GIL

Даже после всех оптимизаций main.py все еще работал последовательно. Прогноз ~2.5 часа хоть и вписывался в лимит, но оставлял мало "запаса прочности". Я лег спать с мыслью, что нужно что-то еще.

И, как это часто бывает, решение пришло на следующий день, когда в голове всплыл классический вопрос с собеседований на Python-разработчика: "Расскажите про GIL". И тут меня осенило.

Global Interpreter Lock (GIL) в Python мешает по-настоящему распараллеливать вычисления на CPU с помощью потоков (threading). Но мои ~20 секунд на вопрос — это не вычисления. Это почти целиком ожидание ответа от сети (I/O-bound задача). А для таких задач threading — идеальный инструмент, потому что пока один поток "спит" в ожидании ответа от API, GIL позволяет другому потоку отправить свой запрос.

Вооружившись этой базовой истиной, я написал еще один тест, test_api_concurrency.py, чтобы измерить, сколько параллельных запросов выдержит API. Результаты показали, что он легко справляется с 8-16 одновременными потоками.

Это был финальный недостающий элемент. Я переписал основной цикл в main.py с использованием concurrent.futures.ThreadPoolExecutor с 8 воркерами. Вместо обработки 500 вопросов один за другим, моя система теперь обрабатывала по 8 за раз.

Мораль: Иногда самые сложные проблемы производительности решаются возвращением к самым базовым принципам Computer Science. И да, ходить на собеседования (или хотя бы вспоминать вопросы с них) — полезно.

Финальный Результат

Комбинация этих оптимизаций дала феноменальный результат. Я запустил финальный полный прогон main.py на всех 500 вопросах. Для максимального качества я использовал конфигурацию с тремя итерациями агентского цикла, рискуя временем выполнения.

Вот финальный отчет ResourceManager после завершения работы:

Финальный отчет о ресурсах после обработки 500 вопросов в режиме ITERATIVE с MAX_ITERATIONS = 3
Финальный отчет о ресурсах после обработки 500 вопросов в режиме ITERATIVE с MAX_ITERATIONS = 3

Давайте проанализируем эти цифры:

  • Время (time_elapsed_sec: 5745.46 s):

    • Общее время составило ~1.6 часа (5745 / 3600).

    • Это означает, что среднее "календарное" время на вопрос, благодаря параллелизму, было всего ~11.5 секунд (5745 / 500).

    • Вывод: Даже самая "тяжелая" и "задумчивая" конфигурация с 3 итерациями уложилась в 4-часовой лимит с более чем двукратным запасом.

  • Стоимость (api_spent_usd: $0.98):

    • Мы потратили чуть меньше одного доллара на весь датасет.

    • Вывод: Наш бюджет в $3 был использован всего на ~33%. Это доказывает высочайшую экономическую эффективность "Multi-Model" подхода, где дорогие LLM используются только для коротких, но важных задач.

В итоге, благодаря системному подходу к архитектуре и оптимизации, я смог реализовать очень сложный итеративный RAG-пайплайн, который оказался не только качественным, но и поразительно быстрым и дешевым в работе.

Это доказывает главный принцип: сложная архитектура — это не всегда медленная архитектура, если правильно определить и оптимизировать "бутылочные горлышки".

Часть 4: Заглянем "под капот" — трассировка и визуализация

Итак, я получили быструю, экономичную и отказоустойчивую систему. Но как именно она "думает"? Чтобы ответить на этот вопрос, я использовал два инструмента для визуализации "внутренностей" моего RAG-агента: Phoenix Tracer для отслеживания логики и Matplotlib для визуализации структуры знаний.

Phoenix Tracer: Разбираем "Мыслительный" Процесс

Чтобы понять, как взаимодействуют все компоненты моего пайплайна, я воспользовался open-source библиотекой arize-phoenix для трассировки. Результат — это наглядная "водопадная" диаграмма, где виден каждый шаг.

Вот как выглядит трейс для стратегии DECOMPOSE. Сначала DecompositionAgent разбивает сложный вопрос на несколько простых под-запросов. Затем наш EnsembleRetriever ищет информацию по каждому из них.

Трейс выполнения в режиме DECOMPOSE. Видно несколько последовательных вызовов ParallelEnsembleRetriever — по одному на каждый под-запрос, сгенерированный на этапе декомпозиции
Трейс выполнения в режиме DECOMPOSE. Видно несколько последовательных вызовов ParallelEnsembleRetriever — по одному на каждый под-запрос, сгенерированный на этапе декомпозиции

Самое интересное — заглянуть внутрь одного из этих вызовов ParallelEnsembleRetriever. Phoenix позволяет нам "провалиться" и посмотреть, что именно нашла каждая из наших четырех "голов".

Детализация одного вызова ParallelEnsembleRetriever. Видно, как параллельно отработали четыре "головы". Самым медленным оказался семантический поиск (~945ms), а самым быстрым — поиск по графу (~0ms, и нет это не ошибка. Просто он не понадобился, вопрос был простой)
Детализация одного вызова ParallelEnsembleRetriever. Видно, как параллельно отработали четыре "головы". Самым медленным оказался семантический поиск (~945ms), а самым быстрым — поиск по графу (~0ms, и нет это не ошибка. Просто он не понадобился, вопрос был простой)

Что мы видим на этом скриншоте:

  1. Параллелизм в Действии: Общее время работы ансамбля (1.4с) определяется временем самой медленной "головы" (~945ms), а не их суммой. Это доказывает эффективность нашего ThreadPoolExecutor.

  2. Синергия "Голов": Каждая "голова" находит разные документы. VectorStoreRetriever ищет семантически похожие тексты, TFIDFRetriever — тексты с точными ключевыми словами. Вместе они обеспечивают максимальную полноту поиска.

  3. Прозрачность: Мы можем буквально "зайти" в каждый спан и посмотреть, какие документы (output) он вернул. Это превращает наш "черный ящик" в полностью прозрачную систему.

"Мозг" Ретривера: Визуализация Графа Концептов

Одной из самых интересных частей нашего поискового движка является "Голова D" — Иерархический Граф Концептов. Я визуализировал его, чтобы показать семантическую структуру нашей базы знаний.

Визуализация Графа Концептов. Размер и цвет узла соответствуют его "центральности". Видно плотное "ядро" из общих терминов (желтый) и тематические "планеты" на периферии.
Визуализация Графа Концептов. Размер и цвет узла соответствуют его "центральности". Видно плотное "ядро" из общих терминов (желтый) и тематические "планеты" на периферии.

Что показывает эта "мешанина" графа:

  • Структуру Знаний: Граф имеет четкую структуру "ядро-периферия". В центре находится плотное ядро из самых общих терминов (деньги, кредит, банк). На периферии — более узкие тематические кластеры ("наследство", "ОСАГО").

  • Ограничения TF-IDF: Простота нашего подхода (TF-IDF) приводит к тому, что в ядре много "мусорных" слов. Нейросетевой экстрактор концептов, как в статье LATTICE, сделал бы граф гораздо чище.

  • Почему это Работает: Несмотря на "грязноту", граф все равно полезен. Он позволяет находить неявные связи между документами, что критически важно для multi-hop вопросов.

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

Часть 5: Финальный вердикт — в поисках "золотой середины"

Визуализация и скорость — это хорошо, но главный критерий успеха — качество финальных ответов. Финальное решение будет оцениваться на закрытом датасете организаторами, но чтобы объективно сравнить мои архитектурные подходы на этапе разработки, я применил state-of-the-art методику — LLM-as-a-Judge.

Процесс Оценки: Создаем Своего "Экзаменатора"

Я написал скрипт llm_as_judge.py, который для публичного датасета из 500 вопросов (questions.csv) провел автоматическую оценку. В роли "судьи" выступила модель Llama-3-70B-Instruct, которая оценивала каждый ответ по двум критериям: Релевантность и Полнота и Полезность и Структура.

Важное примечание: Оценки, представленные ниже, сгенерированы моим собственным "судьей" и служили для внутреннего сравнительного анализа архитектур. Они могут (и, скорее всего, будут) отличаться от официальных результатов на закрытом тестовом датасете хакатона.

Результаты A/B-тестирования: Битва Стратегий

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

Гипотеза №1: "Больше итераций — лучше качество?"

Я протестировал итеративный (ITERATIVE) режим с разным количеством циклов.

Итераций

Общая Оценка (LLM-as-a-Judge)

Вывод

3

8.48 / 10

Избыточность. Ответы становятся "водянистыми" и содержат повторения.

2

8.85 / 10

Хорошо, но все еще страдает от избыточности.

Вывод: Оказалось, что "больше" — не значит "лучше". Дополнительные итерации успешно находили больше информации, но модель-генератор, получив слишком обильный и пересекающийся контекст, начинала повторяться. Ответы оставались правильными, но их структура и лаконичность страдали, за что строгий LLM-as-a-Judge и снижал оценки.

Гипотеза №2: "Декомпозиция — ключ к сложным вопросам?"

Я протестировал стратегию DECOMPOSE, которая разбивает вопрос на под-вопросы перед поиском.

Стратегия

Общая Оценка (LLM-as-a-Judge)

Вывод

DECOMPOSE

9.42 / 10

Отлично. Прекрасный баланс качества и скорости.

Вывод: Этот подход показал себя блестяще, доказывая, что для сложных вопросов лучше сначала "распланировать" поиск.

Гипотеза №3: "Простота — залог успеха?"

Наконец, я протестировал самую простую "умную" стратегию — ITERATIVE всего с одной итерацией. По сути, это пайплайн: Поиск -> Анализ (SEA) -> Генерация.

Стратегия

Общая Оценка (LLM-as-a-Judge)

Вывод

ITERATIVE (1 итер.)

9.66 / 10

Лучший результат из всех.

Финальный Анализ

Результаты автоматической оценки для лучшей конфигурации (ITERATIVE, 1 итерация) на публичном датасете questions.csv.
Результаты автоматической оценки для лучшей конфигурации (ITERATIVE, 1 итерация) на публичном датасете questions.csv.

Результаты тестов привели к важному выводу: для доступных нам моделей (Mistral-Small, Gemma) самая простая из "умных" стратегий оказалась самой эффективной.

Моя гипотеза заключается в том, что наш 4-головый EnsembleRetriever настолько хорош, что одного раунда поиска достаточно для сбора "достаточно хорошего" контекста. Дальнейшие итерации, хоть и добавляют больше деталей, создают для моделей среднего размера (7B, 27B) проблему "утопания в контексте", что приводит к избыточности в ответах.

Возможно, для более мощных и "умных" моделей (таких как GPT-4o или будущих поколений), способных лучше синтезировать большой и противоречивый контекст, 2-3 итерации действительно дали бы прирост качества. Но в рамках нашего хакатона, простота оказалась залогом надежности и качества.

Таким образом, для финального решения была выбрана именно эта конфигурация: ITERATIVE с MAX_ITERATIONS = 2 — идеальный баланс скорости, стоимости и, как показали внутренние тесты, максимального качества для данного стека технологий.


Примеры работы на одиночных вопросах:

Вопрос: Как безопасно оплатить покупку онлайн?

Decompose:

ОТВЕТ

Как безопасно оплатить покупку онлайн?

1. Используйте проверенные платежные системы

  • Мобильные платежные системы (Apple Pay, Google Pay, Mir Pay, Samsung Pay) — данные карты не сохраняются на сайте, а шифруются и передаются напрямую платежной системе.

  • Платежные шлюзы (Visa Secure, MasterCard SecureCode, Mir Accept) — после ввода реквизитов карты вас должны перенаправить на защищенную страницу банка для ввода кода из СМС.

2. Проверяйте страницу оплаты

  • Убедитесь, что сайт перенаправил вас на страницу платежной системы (не на сам сайт магазина).

  • Логотипы платежных систем должны быть активными ссылками, ведущими на их официальные сайты (а не просто картинками).

3. Не вводите данные карты на подозрительных сайтах

  • Если сайт просит ввести код с обратной стороны карты или код из СМС — это признак мошенничества.

  • Никогда не сохраняйте данные карты на сайте, если не уверены в его надежности.

4. Используйте виртуальные карты или отдельные карты для онлайн-покупок

  • Виртуальные карты (одноразовые или с ограниченным лимитом) — защищают от несанкционированных списаний.

  • Отдельная карта для онлайн-покупок — переводите на нее только нужную сумму.

5. Оплата через Систему быстрых платежей (СБП)

  • QR-коды — безопасный способ оплаты без ввода реквизитов карты.

  • Переводы по номеру телефона — удобно и защищено (лимиты до 100 000 ₽ в месяц без комиссии).

6. Дополнительные меры безопасности

  • Не переходите по ссылкам из мессенджеров — мошенники могут создать фишинговые страницы.

  • Используйте «безопасную сделку» на сервисах объявлений (деньги резервируются до получения товара).

  • Оплачивайте наложенным платежом, если нет возможности передать товар лично.

Вывод: Для безопасных онлайн-покупок выбирайте проверенные платежные системы, избегайте ввода данных карты на подозрительных сайтах и используйте дополнительные инструменты защиты (виртуальные карты, СБП).

КОНЕЦ ОТВЕТА


FAIR с 2 Итерациями:

ОТВЕТ:

Как безопасно оплатить покупку онлайн?

1. Проверка надежности сайта

  • Убедитесь в подлинности сайта:

    • Проверьте, что адрес сайта совпадает с официальным (указанным в реестре регулятора).

    • Убедитесь, что сайт начинается с https и есть значок закрытого замка в адресной строке.

    • Ищите логотипы платежных систем (Visa Secure, MasterCard SecureCode, Mir Accept) — они должны быть активными ссылками.

  • Избегайте поддельных сайтов:

    • Мошенники копируют популярные магазины, предлагая заманчивые цены.

    • Не вводите данные карты, если вас не перенаправили на страницу платежной системы.

2. Выбор безопасного способа оплаты

  • Используйте системы мобильных платежей:

    • Apple Pay, Google Pay, Mir Pay, Samsung Pay — данные карты не сохраняются на сайте, а шифруются.

    • Продавец не видит реквизиты карты, что снижает риск мошенничества.

  • Оплата по QR-коду:

    • Удобна и безопасна, так как не требует ввода данных карты.

    • Убедитесь, что магазин поддерживает платежи через ваш банк или СБП.

  • Электронные кошельки:

    • Заведите отдельный кошелек для онлайн-покупок, пополняйте его перед оплатой.

    • Не привязывайте зарплатную или кредитную карту, если не уверены в надежности сайта.

3. Дополнительные меры безопасности

  • Не сохраняйте данные карты на сайтах:

    • Если сайт предлагает сохранить реквизиты, откажитесь, если не уверены в его безопасности.

  • Используйте отдельную карту для онлайн-покупок:

    • Установите лимит трат и настройте оповещения о списаниях.

  • Будьте осторожны с BNPL-сервисами:

    • Проверяйте условия договора, сроки и штрафы за просрочку.

    • Учтите, что такие сервисы не всегда отражаются в кредитной истории.

  • Не передавайте коды из СМС:

    • Вводите их только на странице платежной системы, а не на сайте магазина.

4. Защита от мошенничества

  • Не переводите деньги до получения товара:

    • Дождитесь подтверждения зачисления средств (уведомление от банка).

    • Используйте услугу «безопасная сделка» на сайтах объявлений.

  • Следите за подозрительными операциями:

    • Если перевод не пришел, проверьте, не использовали ли мошенники демоверсию банковского приложения.

  • Обновляйте антивирусы и используйте сложные пароли:

    • Включите двухфакторную аутентификацию для защиты аккаунтов.

Соблюдая эти правила, вы минимизируете риски при онлайн-покупках.

КОНЕЦ ОТВЕТА

Демонстрация того как работает FAIR
Демонстрация того как работает FAIR

Вопрос: Как меняется роль Банка России в защите интересов клиентов при реорганизации НПФ по сравнению с его мандатом по контролю инфляции?

Decompose:

ОТВЕТ

На основе предоставленного контекста можно сделать следующие выводы о роли Банка России в защите интересов клиентов при реорганизации НПФ по сравнению с его мандатом по контролю инфляции:

1. Роль Банка России в защите клиентов при реорганизации НПФ

  • Регулирование и контроль: Банк России выступает регулятором финансового рынка, включая негосударственные пенсионные фонды (НПФ). В случае проблем у НПФ (например, отзыва лицензии) регулятор может вмешаться, чтобы защитить интересы клиентов.

  • Жалобы и претензии: Клиенты могут обращаться в Банк России с жалобами на НПФ, если их права нарушены. Регулятор рассматривает такие обращения и принимает меры.

  • Информационная поддержка: Банк России предоставляет клиентам информацию о том, как действовать в случае проблем с НПФ, включая возможность обращения в АСВ (Агентство по страхованию вкладов) для разъяснений.

2. Роль Банка России в контроле инфляции

  • Монетарная политика: Основная задача Банка России в этом аспекте — поддержание стабильности национальной валюты и контроль инфляции через регулирование денежной массы, процентных ставок и других инструментов.

  • Финансовая стабильность: Контроль инфляции косвенно влияет на защиту интересов клиентов, так как стабильная экономика снижает риски для сбережений и инвестиций.

3. Различия в мандатах

  • Защита клиентов (НПФ): Банк России действует как надзорный орган, обеспечивая соблюдение законов и защиту прав вкладчиков и инвесторов.

  • Контроль инфляции: Это макроэкономическая задача, направленная на стабилизацию экономики в целом, а не на прямую защиту отдельных клиентов.

Вывод

Банк России выполняет две ключевые функции:

  1. Регулирование и защита клиентов (включая НПФ) через надзор, контроль и разъяснение прав.

  2. Контроль инфляции через монетарную политику, что косвенно способствует финансовой стабильности.

В случае реорганизации НПФ регулятор действует в рамках защиты прав клиентов, тогда как контроль инфляции — это более общая задача, направленная на устойчивость экономики.

КОНЕЦ ОТВЕТА


FAIR с 2 Итерациями:

ОТВЕТ

Роль Банка России в защите интересов клиентов при реорганизации НПФ

  1. Контроль и согласование реорганизации

    • Банк России обязан проверить, сможет ли преобразованный НПФ выполнить обязательства перед клиентами.

    • Если проверка пройдена, регулятор согласовывает реорганизацию; в противном случае она отменяется.

  2. Защита прав клиентов

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

    • Банк России следит за соблюдением этих требований, обеспечивая прозрачность процесса.

  3. Финансовая стабильность

    • Регулятор оценивает риски для клиентов, чтобы избежать потерь их пенсионных накоплений.

Роль Банка России в контроле инфляции

  1. Регулирование денежно-кредитной политики

    • Банк России использует ключевую ставку для поддержания инфляции на уровне 4%, балансируя между сдерживанием роста цен и стимулированием экономики.

  2. Реакция на экономические кризисы

    • В чрезвычайных ситуациях (например, санкции, падение курса рубля) регулятор корректирует ставку для стабилизации финансового рынка.

  3. Долгосрочные цели

    • Поддержание низкой инфляции создает условия для устойчивого экономического роста и долгосрочного планирования.

Сравнение ролей

  • При реорганизации НПФ Банк России фокусируется на индивидуальной защите клиентов (прозрачность, сохранность накоплений).

  • При контроле инфляции регулятор действует на макроэкономическом уровне, влияя на общие условия экономики через ставки и денежную политику.

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

КОНЕЦ ОТВЕТА


Снова обе стратегии справились блестяще. Ответ ITERATIVE режима получился немного более подробным и структурированным, что говорит о пользе второй итерации для таких сложных вопросов. Это идеально демонстрирует компромисс "скорость vs. глубина" между разными архитектурными подходами.

Часть 6: Заключение — Главные Уроки и Будущее Проекта

Заключение: Главные Уроки и Будущее Проекта

Этот 72-часовой спринт стал концентрированным курсом по созданию современных RAG-систем. Вот главные уроки, которые я вынес:

  1. Архитектура — это главное. Хорошо продуманная, модульная архитектура (спасибо SOLID!) позволила мне быстро адаптироваться к меняющимся правилам, отлаживать компоненты по отдельности и в итоге собрать сложную систему в одиночку.

  2. Телеметрия — это не роскошь, а необходимость. Встроенный ResourceManager для отслеживания времени, стоимости и RAM был моим главным "компасом" при оптимизации. Без объективных данных все решения по производительности были бы просто догадками.

  3. "Больше" не всегда значит "Лучше". Мои эксперименты показали, что для доступных нам моделей одна итерация агентского цикла оказалась качественнее и надежнее, чем три. Это доказывает, что каждая RAG-система — это сложный баланс, и "слепое" наращивание сложности не всегда приводит к лучшим результатам.

  4. Научные статьи — это не теория, а руководство к действию. Все ключевые "фишки" моей системы (агентский цикл, граф концептов, CoCT) были взяты из свежих исследований. Умение находить, адаптировать и комбинировать SOTA-идеи — это, возможно, главный навык современного ML-инженера.

Будущие Улучшения (Roadmap)

Этот проект — прочный, высокопроизводительный фундамент. Я вижу несколько ключевых направлений для его дальнейшего развития, превращающих его в полноценную SOTA-платформу для агентского RAG.

Уровень 1: Повышение "Интеллекта" Агентов

  1. Адаптивный Маршрутизатор Стратегий:

    • Идея: Вместо ручного переключения STRATEGY в конфиге, создать "Агента-Маршрутизатора". Этот LLM-агент будет на первом шаге анализировать вопрос и автоматически выбирать, какую из наших стратегий (SIMPLE, DECOMPOSE или ITERATIVE) использовать.

    • Влияние: Превратит фреймворк в полностью автономную, адаптивную систему, которая сама выбирает оптимальный баланс "скорость/качество" для каждого вопроса.

  2. Гибридный Агентский Пайплайн (Decompose + Iterative):

    • Идея: Создать "супер-стратегию", которая сначала декомпозирует сложный вопрос на под-запросы (Decomposition), а затем для каждого под-запроса запускает наш итеративный FAIR-RAG цикл (Iterative Refinement).

    • Влияние: Потенциально самый высокий уровень качества и полноты ответов за счет комбинации двух мощных подходов. Основной вызов — оптимизация производительности.

  3. Динамический Выбор Контекста (Агент-Селектор):

    • Идея: Заменить фиксированный MAX_CONTEXT_DOCS на "Агента-Селектора". Этот LLM будет анализировать топ-50 найденных документов и генерировать список номеров тех, которые являются минимально необходимыми для ответа (идея из статьи DPS).

    • Влияние: Сделает размер контекста адаптивным и еще более "чистым".

Уровень 2: Улучшение "Оптики" и "Движка"

  1. Внедрение Нейросетевого Реранкера:

    • Идея: Добавить дополнительный шаг "тонкой очистки" после EnsembleRetriever. Использовать либо разрешенные API-реранкеры, либо реализовать свой легковесный реранкер на torch (вдохновляясь статьей EBCAR).

    • Влияние: Значительно повысит точность (precision) поиска и качество финального контекста, подаваемого в агенты.

  2. Обучаемый Граф Концептов:

    • Идея: Вместо TF-IDF использовать более продвинутые методы для построения графа, например, эмбеддинги графов (Graph Embeddings) или обучать веса ребер на основе обратной связи от SeaAgent.

    • Влияние: Сделает "Голову D" еще более мощным инструментом для multi-hop рассуждений.

Уровень 3: Инженерное Совершенство

  1. Полный Переход на asyncio:

    • Идея: Переписать весь I/O-bound код (API-вызовы, файловые операции) с ThreadPoolExecutor на нативный async/await.

    • Влияние: Потенциальное ускорение параллельной обработки, особенно при масштабировании до десятков и сотен одновременных запросов. Снижение потребления ресурсов по сравнению с потоками.

  2. Расширение Системы Тестирования:

    • Идея: Реализовать полноценный фреймворк для юнит-тестирования с unittest.mock. Создать "золотой" датасет для регрессионного тестирования качества ответов. Настроить CI/CD для автоматического прогона тестов.

    • Влияние: Повысит стабильность и надежность проекта при дальнейших доработках.


Весь код, включая отладочные скрипты, тесты и скрипт сборки, доступен в репозитории на GitHub - https://github.com/Runoi/ai-for-finance-hack-2025.

Заключение: История Одного Архитектурного Спринта

Хакатон — это всегда спринт. Но иногда, в процессе этого забега, ты успеваешь пробежать настоящий марафон идей, гипотез и открытий. Именно таким марафоном и стал для меня AI for Finance Hack 2025.

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

Поэтому эта статья — не отчет о финальном решении и не заявка на победу. Это, скорее, "бортовой журнал" архитектора в реальном времени. Я сознательно сфокусировался на концепциях, идеях и процессе принятия решений, а не на вылизанном до блеска коде. Финальный репозиторий, безусловно, будет опубликован после завершения хакатона, но главную ценность, как мне кажется, представляет именно путь, а не только результат.

Я делюсь этим путем открыто и заранее. Даже если кто-то вдохновится моими идеями, возьмет эту архитектуру и реализует ее лучше — я буду только рад. Я пришел на этот хакатон в первую очередь за опытом, за возможностью отточить навыки в условиях жестких ограничений и, конечно, чтобы узнать новое. Если мой опыт окажется полезен еще кому-то — значит, главная цель достигнута.

Отдельно хочу поблагодарить моего сокомандника Никиту. Хоть он и JS-фронтендер, его свежий взгляд "со стороны" на проблемы и архитектуру был невероятно ценен. Но что еще важнее — просто его присутствие и участие, в отличие от двух других, оказало огромную моральную поддержку, без которой этот "одиночный" штурм был бы гораздо тяжелее.

Наконец, о репозитории. Его история коммитов может показаться немного "грязной" и не всегда последовательной. Причина в том, что одной из моих главных личных целей на этот хакатон было отточить именно командное взаимодействие в Git, с Pull Request'ами, ревью и атомарными коммитами. Когда стало понятно, что большая часть работы ляжет на мои плечи, фокус сместился с идеального Git workflow на скорость итераций и экспериментов. Это тоже ценный урок хакатона: будь готов менять не только архитектуру, но и процессы.

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

Небольшое послесловие: я принял участие в хакатоне в исследовательских целях и для практики, так что результаты не на первом месте. И да, в данной задаче самый лучший - самый простой

Комментарии (0)