Такая глубина анализа стоит за каждым нашим проектом. Мы публикуем её как подтверждение того, как мы на самом деле работаем — реальные варианты, которые мы взвешивали, цифры, которые их отсеяли, архитектура, пережившая столкновение с живым пайплайном авторизации платежей — чтобы процесс был для клиентов таким же прозрачным, как и результат. В этом проекте «высекание из камня» было не про выбор самой точной модели — ни один тип модели сам по себе не укладывался в бюджет задержки и одновременно не ловил поведенчески новый фрод, так что реальная работа заключалась в том, чтобы заставить два несогласных сигнала договориться достаточно быстро, чтобы это имело значение.
У клиента был digital-first платёжный бизнес, обрабатывающий большой объём транзакций без физической карты (card-not-present) — платежи в e-commerce и подписках, где нет ни чипа, ни PIN-кода, ни карты, которую можно было бы просто посмотреть. Антифрод-движок был правило-ориентированным: вручную настроенные пороги по сумме, скорости транзакций и набору блок-листов, годами дорабатываемые тем, кто был на дежурстве во время последнего инцидента. Он работал в узком смысле — блокировал многое, включая многое, что фродом не являлось. Постоянный клиент, покупающий ноутбук в 2 часа ночи с нового устройства в командировке, для статичного правило-ориентированного движка выглядел точь-в-точь как захват аккаунта. Доля ложных срабатываний на легитимных крупных транзакциях стала измеримым источником оттока клиентов, и команда антифрода это понимала — просто не могла это исправить, не ослабив заодно правила, которые ловили реальный фрод.
Запрос звучал не как «добавьте модель поверх правил». Он звучал так: сократить ложные срабатывания на легитимных клиентах, ловить больше фрода, который упускал правило-ориентированный движок, и уложиться в то же окно задержки, которое чекаут уже выделяет на решение по антифроду. Именно это последнее ограничение определило почти всё, что описано ниже — более точная модель, добавляющая полсекунды к чекауту, не пойдёт в прод, потому что медленная авторизация — это отдельный вид отказа.
Второе ограничение, которое имело не меньшее значение, чем задержка: это регулируемый финансовый процесс. Решение отклонить чью-то карту должно быть объяснимым и восстанавливаемым — для комплаенс-ревьюера, команды по спорным транзакциям, потенциально регулятора — месяцы спустя, а не только быстрым в моменте.
Почему глубокое обучение не было очевидным апгрейдом, которым его часто считают: бенчмарк-статья Гринштайна, Ойаллона и Варокё на NeurIPS 2022, «Why do tree-based models still outperform deep learning on typical tabular data» (arXiv:2207.08815), провела контролируемое сравнение на 45 табличных датасетах и пришла к выводу — дословно от авторов — что «tree-based модели остаются state-of-the-art на данных среднего размера (~10 тыс. сэмплов) даже без учёта их превосходства в скорости». Мы намеренно цитируем именно качественный вывод статьи, а не конкретную цифру разрыва — таблицы по каждому датасету нужно было бы открывать напрямую, чтобы процитировать точную дельту точности, а мы предпочитаем цитировать то, за что можем поручиться, а не округлять цифру, которую сами не проверили. Табличные данные о фроде — структурированные, с инженерными признаками, умеренной размерности — это ровно тот режим, в котором статья документирует преимущество деревьев, а не режим (изображения, текст, очень большие датасеты), где вместо этого проявляется преимущество глубокого обучения.
Почему бюджет задержки независимо подкрепил тот же вывод: опубликованный NVIDIA рассказ о модели антифрода American Express «Gen X» (блог вендора вычислительной инфраструктуры, то есть вендорски окрашенная подача, а не нейтральная третья сторона) описывает продакшен-модель, сочетающую градиентный бустинг (более 1000 деревьев) с компонентом LSTM, построенную под заявленный бюджет инференса в 2 миллисекунды, при этом GPU-ускоренная версия давала примерно 50-кратное ускорение по сравнению с предыдущим CPU-подходом. Мы не утверждаем, что наша задержка совпадает с показателем Amex — их цифра покрывает один компонент гораздо большего пайплайна на масштабе, на котором мы не работаем, — но сам паттерн и есть релевантная точка данных: даже одна из крупнейших карточных сетей в мире полагается на деревья с градиентным бустингом, а не на сквозную глубокую сеть, когда ограничение измеряется в миллисекундах.
Почему не поручить основной скоринг LLM: вызов LLM API — это сетевой round-trip к размещённой в облаке модели: реалистично десятки-сотни миллисекунд даже для быстрой модели, ещё до того, как отработает всё остальное в пайплайне. Публичное описание Radar от Stripe (через разбор от третьей стороны — то есть это пересказ из вторых рук, а не собственная документация Stripe) описывает их основное решение как укладывающееся в менее чем 100 миллисекунд при оценке 1000+ сигналов — и даже эта цифра описывает плотно оптимизированный, специально построенный путь скоринга, а не вызов LLM общего назначения. Сделать языковую модель основным, синхронным скором на каждой транзакции внутри общего бюджета в 200мс — это не вопрос качества модели, это арифметика. В бюджете просто нет места для неё как для единственного сигнала.
XGBoost как базовый скор, обученный на инженерных табличных и поведенческих признаках, остающийся основным и самым быстрым сигналом в пайплайне. Где реально применяется LLM — отдельное решение (§4): она не заменяет базовую модель на деревьях, а работает параллельно с ней.
Почему join’ы во время запроса отсеялись первыми: признаки, которые реально ловят фрод — «сколько транзакций эта карта попыталась провести за последние 10 минут», «видели ли этот отпечаток устройства с тремя разными держателями карт на этой неделе» — это скользящие оконные агрегаты по недавней истории. Вычислять их как ad-hoc join’ы в момент скоринга, внутри бюджета в суб-200мс, который также должен покрыть инференс XGBoost, вызов LLM и сетевые round-trip’ы, означает выполнять нетривиальные агрегирующие запросы под давлением времени на каждой транзакции. Это не проблема оптимизации задержки, которую можно решить позже, — это просто неправильное место в пайплайне для этой работы вообще.
Почему ночные batch-таблицы отсеялись следующими, и по причине, специфичной для фрода, а не общей проблеме устаревания данных: фрод специально нацелен на этот разрыв. Атакующий, прогоняющий скрипт card-testing по списку украденных карт, генерирует всплеск транзакций за минуты; признак скорости, обновляемый раз в ночь, к моменту обновления уже был нерелевантен на всё окно атаки. То, что признаки отражают живое поведение, а не снепшот, — это близко к всему смыслу добавления поведенческих сигналов вообще.
Почему именно real-time feature store: собственная документация Feast по производительности онлайн-обслуживания описывает Java gRPC-сервер поверх Redis как наиболее производительную конфигурацию, дающую в 4–10 раз лучшую пропускную способность по сравнению с альтернативными онлайн-хранилищами в их собственном бенчмаркинге — это самостоятельно заявленная Feast цифра, не проверенная независимо, но согласующаяся с тем, что можно ожидать от профиля чтения Redis в памяти. Однако свойство, которое важнее конкретного множителя, — это согласованность обучения и обслуживания (training-serving consistency): Feast (и сопоставимые платформы вроде Tecton) определяют трансформацию признака один раз и используют то же определение и для офлайн-хранилища (обучение), и для онлайн-хранилища (живые предсказания). Для регулируемой модели, где на вопрос «почему модель оценила эту транзакцию как рискованную» нужно уметь ответить, гарантия, что признак на этапе обучения и на этапе обслуживания вычисляется одинаково, — это не удобство, а закрытие целого класса багов «модель ведёт себя в проде иначе, чем на валидации».
Real-time feature store в духе Feast, с потоковой записью из журнала событий Kafka (§3), наполняющей онлайн-хранилище на Redis, — даёт XGBoost чтение свежевычисленных поведенческих и скоростных признаков за единицы миллисекунд в момент скоринга.
Почему это по-настоящему другой расчёт, чем типичное решение «нужна ли нам Kafka»: стандартный аргумент против Kafka в том, что большинство событийных нагрузок на самом деле — низкообъёмные, эфемерные уведомления, и для них лёгкая очередь — правильный выбор; собственная рекомендация Confluent о том, когда не использовать Kafka, говорит именно об этом. Это не та нагрузка. Каждая транзакция без физической карты, которую обрабатывает клиент, генерирует событие непрерывно, на значимом объёме — это не форма «несколько сотен фоновых задач в день», которая в других контекстах исключает Kafka.
Требование, которое реально решило вопрос, было не пропускной способностью — это было устойчивое воспроизведение: очереди в духе SQS и RabbitMQ построены вокруг доставки — сообщение потребляется, подтверждается и, без дополнительной надстроенной оснастки, исчезает. Базовое проектное свойство Kafka, задокументированное в собственной архитектурной документации Apache, противоположно: append-only журнал, который потребители читают, не удаляя, хранится заданный период, воспроизводим любым числом независимых consumer-групп. Для регулируемого антифрод-пайплайна запрос комплаенс-ревьюера «восстановите точно, что система знала об этой транзакции шесть недель назад» — рутинный запрос, а не крайний случай, и транзитная очередь архитектурно не может на него ответить без второй системы, надстроенной для имитации хранения.
Почему запас по пропускной способности стоило упомянуть, даже если он не был решающим фактором: собственные опубликованные бенчмарки Confluent сообщают о кластерах Kafka, устойчиво выдерживающих порядка 2 миллионов записей в секунду на кластере из 3 брокеров, и десятки миллионов сообщений в секунду в сценариях с большим числом консьюмеров при медианной end-to-end задержке менее 5мс. На дальнем конце спектра масштаба инженерный блог LinkedIn описывает работу Kafka на уровне примерно 7 триллионов сообщений в день на более чем 100 кластерах — мы цитируем это явно как референс экстремального масштаба, а не базовое ожидание; платёжный процессор среднего размера не работает на масштабе LinkedIn. Честная версия аргумента такая: у Kafka есть запас пропускной способности на порядки больше, чем нужно этой нагрузке, что приятно иметь, но именно свойство устойчивого воспроизведения, а не потолок пропускной способности, реально определило решение.
Kafka, принимающая живой поток транзакций один раз и питающая и потоковую агрегацию feature store (§2), и долгосрочное хранение для комплаенс-воспроизведения и будущего переобучения модели — один и тот же журнал событий обслуживает несколько независимых нижестоящих потребителей вместо того, чтобы каждому нужен был свой механизм доставки.
Почему последовательное вентилирование (сначала скор, потом рассуждение о пограничных случаях) выглядело естественным дизайном, который не укладывался в бюджет: интуитивный дизайн запускает XGBoost первым и тратит вызов LLM только на транзакции, попадающие в неоднозначную полосу скора — дёшево, потому что большинство транзакций вообще не доходят до LLM. Проблема в том, что это делает вызов LLM аддитивным поверх задержки древовидной модели именно на тех транзакциях, в правильности которых клиент заинтересован больше всего, и он вообще не касается уверенно оценённых транзакций — включая ложноотрицательные случаи, фрод, который выглядит статистически ничем не примечательным в инженерных признаках, но читался бы как подозрительный по самому описанию транзакции. Дизайн, применяющий языковое рассуждение только к случаям, в которых древовидная модель уже была не уверена, не может исправить слепые зоны самой этой модели.
Почему именно параллельный, а не последовательный запуск XGBoost и вызова LLM делает бюджет задержки вообще рабочим: если вызов LLM выполняется параллельно с инференсом дерева, а не после него, суммарная добавленная задержка близка к длительности самого вызова LLM, а не к сумме обоих. Само по себе древовидное инференс достаточно быстрое, чтобы не иметь значения здесь — скоринг за единицы миллисекунд является стандартной индустриальной практикой для моделей на градиентном бустинге с сотнями-низкими тысячами деревьев на CPU. Это значит, что весь бюджет задержки для этого решения сводится к одному числу: насколько быстр сам вызов LLM.
Почему GPT-4o-mini, а не GPT-4o, раз вызов теперь идёт на каждой транзакции, а не на выборочном подмножестве: собственный анонс OpenAI от 18 июля 2024 года, представляющий GPT-4o-mini, позиционирует её явно для «быстрых, real-time текстовых ответов» и «приложений, требующих цепочек или параллельных вызовов нескольких моделей» — именно этот кейс. GPT-4o-mini стоит $0.15 за 1M входных токенов и $0.60 за 1M выходных, против стартовой цены GPT-4o в $5.00 и $15.00 соответственно — разрыв более чем в 30 раз, который имеет большее значение здесь, потому что вызов теперь касается каждой транзакции. По качеству OpenAI сообщает результат GPT-4o-mini в 82.0% на MMLU (официальная цифра); собственный показатель GPT-4o по MMLU обычно упоминается на уровне около 88.7%, но мы помечаем эту вторую цифру как менее надёжную — мы проверили её через вторичные источники, а не напрямую со страницы OpenAI. Для задачи более узкой, чем общее рассуждение, реал-тайм позиционирование и цена за вызов у младшей модели значили больше, чем закрытие разрыва в бенчмарке, который этот вызов, по сути, и не решает.
Почему не self-hosted открытая модель: это регулируемый финансовый процесс, и практический вопрос — не только стоимость инференса, а то, кто несёт бремя доказательства, что модель ведёт себя безопасно и последовательно во времени. Управляемый API frontier-модели идёт с собственной задокументированной оценкой и тестированием безопасности вендора, версионированием и бумажным следом, на который можно опереться в разговоре с комплаенсом. Поднять и непрерывно валидировать дообученную открытую модель для задачи рассуждения, смежной с фродом, за окно разработки в 10 недель без выделенной MLOps-функции на этом направлении, перекладывает это бремя управления на клиента без соответствующей выгоды, достаточной, чтобы оправдать это на этом объёме.
GPT-4o-mini, вызываемая параллельно со скором XGBoost на каждой транзакции — без привязки к порогу пограничного скора, — рассуждающая над описанием транзакции и контекстом устройства/поведения, с результатом, объединяемым со скором на деревьях в лёгком ансамблевом шаге перед решением о маршрутизации (§5).
Почему бинарный порог — это именно то, от чего нас явно наняли уйти: исходный правило-ориентированный движок и был по сути бинарной системой блок/пропуск, и его сценарий отказа — легитимный крупный клиент, получающий отказ без пути к быстрой коррекции, — был конкретной проблемой в рамках задачи. Более точная модель за той же бинарной структурой решения всё равно не оставляет места, чтобы «модель на 55% уверена, что это фрод» разрешилось как-то иначе, чем угадыванием. Более точный скор не чинит архитектуру решения, у которой всего два выхода.
Почему полностью автоматизированное решение с постфактум-ревью не работает именно для фрода, хотя это легитимный паттерн в других местах: постфактум-ревью — правильный выбор для многих задач качества и модерации — пусть действие произойдёт, ошибки ловим потом. Здесь это не работает, потому что «действие» — это движение денег. К моменту, когда аналитик рассматривает уже прошедшую транзакцию, средства обычно уже расчитаны или товар уже отгружен; поимка фрода постфактум превращает проблему предотвращения в проблему восстановления — категорически худшую позицию для клиента и обычно для держателя карты.
Почему именно средний уровень реально снижает ложные срабатывания, а не модель сама по себе: объединённый скор из §4 даёт три применимые зоны вместо одного порога: уверенно легитимно, уверенно мошеннически, реально неоднозначно. Направление в человека только неоднозначной средней полосы означает, что автоматизированный путь по-прежнему обслуживает подавляющее большинство транзакций на полной скорости — большинство не являются пограничными случаями, — в то время как транзакции, где неправильный автоматический вызов стоит дороже всего, получают человеческое решение вместо подбрасывания монеты, замаскированного под порог.
Почему консоли ревью нужно было показывать оба сигнала, а не только риск-скор: аналитику, принимающему быстрое, обоснованное решение по задержанной транзакции, нужно видеть почему система не уверена — какие признаки подняли скор XGBoost и что реально сказало рассуждение LLM над описанием транзакции — рядом друг с другом, а не одно непрозрачное число. Это требование комплаенса не в меньшей степени, чем удобства: задокументированное обоснование решения об отмене — это то, что реально запрашивает процесс спорных транзакций или аудита позже. Мы построили это на React по той же причине, по которой мы по умолчанию выбираем React для любого внутреннего, насыщенного данными, авторизованного операционного инструмента — никаких ограничений по размеру бандла и самый широкий доступный пул найма. Более важным решением здесь был не фреймворк — а то, что решения аналитиков записываются обратно в тот же журнал Kafka (§3) как размеченные исходы, так что каждая ручная отмена становится обучающим сигналом, а не разовым суждением, испаряющимся после закрытия тикета.
Трёхуровневое решение о маршрутизации — авто-пропуск, авто-блок и мягкая задержка для неоднозначной средней полосы, направляемая в real-time консоль аналитика на React, показывающую и атрибуцию признаков XGBoost, и трассировку рассуждений GPT-4o-mini, с решениями аналитиков, поступающими обратно как размеченные данные для будущего переобучения модели.
Ни одна из трёх опубликованных цифр не случайна — каждая является следствием конкретной пары решений выше:
Опубликованные вендорами кейсы по фроду дают полезную проверку на правдоподобность, с честной оговоркой, что это лучшие вендорские случаи, а не типичные средние: опубликованные кейс-стади Feedzai сообщают о снижении ложных срабатываний в диапазоне 50–73% с ростом выявления фрода до 114% для отдельных клиентов, в то время как публичные заявления Stripe описывают более консервативный исход (примерно на 15% больше выявленного фрода без роста ложных срабатываний для ранних адоптеров). −40%/+22% нашего клиента комфортно укладываются в этот диапазон — ближе к середине, чем к самой агрессивной заголовочной цифре любого из вендоров.
Стоит сказать прямо, потому что ни одна архитектура не вечна:
Ни один из этих пунктов не является провалом исходного решения — это условия, при которых тот же самый процесс рассуждения, запущенный заново, дал бы другой ответ.
Мы публикуем эту таблицу доверия намеренно. Клиенту полезнее знать, за какие цифры мы поручимся без колебаний, а какие перепроверили бы по первоисточнику перед тем, как повторить их на совете директоров, чем получить документ, который выглядит чисто только потому, что мы тихо сгладили эту разницу.