Такая глубина анализа стоит за каждым нашим проектом. Мы публикуем её как подтверждение того, как мы на самом деле работаем — реальные варианты, которые мы взвешивали, цифры, которые их отсеяли, единственная архитектура, пережившая столкновение с требованиями — чтобы процесс был для клиентов таким же прозрачным, как и результат. Это инженерный эквивалент того, как дизайн-студия показывает пять эскизов логотипа перед тем, как остановиться на одном, — только здесь следы резца — это цифры бенчмарков, а не карандашные линии.
У клиента была растущая D2C-витрина с командой поддержки из 6 агентов, обрабатывающих около 3000 диалогов в неделю через чат и почту. В пиковый сезон (от Чёрной пятницы до Нового года) время первого ответа регулярно уходило за 20 минут, и большая часть этого объёма приходилась на один и тот же небольшой набор вопросов: «Где мой заказ», «Хочу вернуть товар», «Можно ли получить возврат средств».
Запрос звучал не как «сделайте нам чат-бота». Он звучал так: сократить время ответа без расширения штата и не дать агенту ошибиться там, где на кону деньги (возвраты средств — как раз то место, где самоуверенный бот хуже, чем отсутствие бота вообще).
Именно это последнее ограничение — у неправильного вызова инструмента есть финансовые последствия — в итоге определило больше архитектурных решений, чем любое требование к производительности. Держите это в голове — оно всплывает снова и снова ниже.
Основной цикл этого агента: держать открытым чат-соединение, стримить токены по мере генерации GPT-4o и — прямо посреди стрима — приостанавливаться для выполнения вызова инструмента (найти заказ, проверить правило доступности возврата), прежде чем продолжить ответ. Это не CPU-bound нагрузка; это десятки-сотни одновременных соединений, которые в основном ждут — ответа от API OpenAI, от нашего собственного Postgres, от вебхука системы управления заказами клиента.
Flask и классический Django по умолчанию используют синхронную модель «один запрос — один воркер» (WSGI). Запрос, который ждёт 5–10 секунд ответа от GPT-4o, блокирует весь воркер-процесс на всё это время — классическая проблема исчерпания пула потоков при конкурентной I/O-bound нагрузке. Можно навесить gevent/eventlet, чтобы сымитировать конкурентность, но это костыль, а не архитектурное решение.
В Django 5.x уже есть async views, и это закрыло часть разрыва — но, согласно собственной документации Django по async, поддержка async ORM всё ещё явно частичная: async-транзакции пока не поддерживаются, а в собственной документации проекта работа над async ORM описана как продолжающаяся, а не завершённая. Для агента поддержки, который постоянно читает/пишет состояние заказа и диалога внутри того же запроса, который стримит токены, — это реальный разрыв, а не теоретический.
Мы намеренно не опирались на сырые бенчмарки пропускной способности при принятии этого решения — публичные цифры FastAPI-vs-Django-vs-Flask, гуляющие в сети, слишком расходятся между источниками, чтобы им можно было доверять настолько, чтобы показывать клиенту. Несущий факт здесь архитектурный, а не бенчмарочный: ASGI event loop обслуживает сотни ожидающих LLM-сессий на одном процессе; синхронный WSGI-воркер — одну.
Деталь, которая закрепила выбор: модели запроса/ответа в FastAPI — это Pydantic-модели, а Pydantic нативно генерирует JSON Schema. Function calling API от OpenAI и есть JSON Schema. Это значит, что один и тот же Pydantic-класс, который валидирует вызов инструмента create_return от GPT-4o, валидирует и FastAPI-эндпоинт, который его реально исполняет, — одна схема, а не два определения, которые надо вручную держать синхронизированными. Учитывая жёсткое ограничение брифа, устранение целого класса багов рассинхронизации схем стоило больше, чем любая сырая цифра RPS.
FastAPI. Django остаётся ровно там же, где и был в нашем стеке — правильным выбором для контентно-тяжёлых, admin-driven сборок с быстрым выходом на рынок (как сам сайт ISTRALLEN). Это никогда не было «FastAPI лучше Django» — это «форма именно этой нагрузки пока не совпадает с sync-first историей ORM в Django».
Почему self-hosting отсеялся первым, ещё до сравнения качества моделей: окно разработки в 6 недель с командой поддержки из 6 человек как единственной операционной подстраховкой — не то окно, в которое стоит ещё и поднимать GPU-инфраструктуру для инференса, мониторинг обслуживания моделей и пайплайн дообучения под надёжность tool calling. Это не оценка качества открытых моделей — это оценка сроков. Self-hosting снова становится правильным выбором в тот момент, когда объём токенов достаточно велик, чтобы стоимость API за токен перевесила накладные расходы на инфраструктуру и эксплуатацию; мы отслеживаем эту точку перехода — на объёме этого клиента мы до неё не дошли.
Почему именно GPT-4o, а не GPT-4-Turbo: решающая точка данных — собственный анонс OpenAI от августа 2024 о Structured Outputs. При строгом соответствии схеме (strict:true) gpt-4o-2024-08-06 показал 100% соответствие схеме на внутреннем эвале со сложными схемами, против менее 40% у gpt-4-0613 без Structured Outputs. Даже без принудительного constrained decoding «естественная» точность соответствия схеме у GPT-4o уже составляла 93%. Для вызова инструмента refund формулировка «модель в основном пытается вызвать функцию правильно» недостаточно хороша — 100% инженерно гарантированное соответствие схеме напрямую отвечает единственному жёсткому ограничению брифа.
Сравнение с открытыми моделями, с честной оговоркой: на Berkeley Function-Calling Leaderboard (стандартный публичный бенчмарк именно для этой способности) модели уровня GPT-4o заметно опережали Llama-3-8B-Instruct по общей точности tool calling (примерно 83% против 59% на снимке, который мы изучили). Мы намеренно помечаем эту цифру оговоркой по надёжности: BFCL — живой лидерборд с ежемесячным обновлением, конкретные цифры выше — это снимок 2024 года, и к моменту, когда вы это читаете, расстановка почти наверняка изменилась — новые открытые модели быстро сокращают этот разрыв. Честная версия этого решения — не «GPT-4o всегда побеждает», а «перепроверьте это сравнение на актуальном лидерборде, прежде чем считать вывод всё ещё верным».
GPT-4o со Structured Outputs, схемы инструментов определены один раз как Pydantic-модели и используются дословно и в спецификации функций OpenAI, и в FastAPI-эндпоинте, который их исполняет (см. §1).
Почему мы не потянулись за выделенной векторной БД: база знаний здесь — это политика возвратов, FAQ по доставке и правила по категориям товаров — реалистично это несколько тысяч чанков, а не миллионы. Опубликованный бенчмарк pgvector от Supabase (индексация HNSW, сентябрь 2023) тестировал это на реальном масштабе: на датасете из 1 миллиона векторов размерностью 1536 индексация HNSW дала более чем в 6 раз больше запросов в секунду по сравнению со старым индексом IVFFlat при точности 98%, а при точности 99% тот же бенчмарк показал, что pgvector обгоняет Qdrant на эквивалентном железе. Наш корпус примерно на три порядка меньше датасета из этого бенчмарка. Поднимать вторую базу данных, второй набор эксплуатационных runbook’ов и второй домен отказа ради поиска по нескольким тысячам чанков FAQ — это не строгость инженерного подхода, а переусложнение задачи, которую Postgres уже решает на этом масштабе.
Почему хранение всего в одном экземпляре Postgres важно не только по производительности: реальный риск в брифе был не в скорости поиска, а в согласованности между «что агент сказал о политике» и «что на самом деле верно для этого конкретного заказа». Сверка правила доступности возврата (из корпуса политик с векторным поиском) с реальным окном возврата конкретного заказа (в транзакционной таблице orders) внутри одной транзакции тривиальна, когда это одна база данных. Между двумя системами это превращается в проблему eventual consistency с деньгами клиента на кону — именно тот сценарий отказа, которого бриф просил избежать.
Postgres с расширением pgvector, один инстанс, один пул соединений, одна история бэкапа/восстановления. Мы отслеживаем рост корпуса как триггер для пересмотра решения — то же исследование указывает, что точка перехода, где выделенный векторный движок начинает выигрывать по сырой пропускной способности, находится далеко за отметкой в 10 миллионов векторов. Мы далеко не рядом с ней.
Почему Kafka отсеялся примерно за пять минут: Confluent — компания, стоящая за Kafka, — публикует собственную инструкцию «когда не стоит использовать Kafka», и она прямолинейна. Kafka оправдывает свои операционные накладные расходы, когда у вас есть устойчивые упорядоченные потоки событий с несколькими независимыми консьюмерами, воспроизводящими один и тот же журнал событий, на устойчиво высокой пропускной способности (их собственное эмпирическое правило — порядка 10 000+ событий/сек устойчиво). Наша реальная фоновая нагрузка — это уведомления об эскалации, триггеры CSAT-опроса и повтор вебхука в систему заказов клиента при сбое — несколько сотен задач в день, а не тысячи в секунду, без требования воспроизведения или fan-out. Поднимать кластер Kafka ради такой нагрузки — это покупать товарный поезд, чтобы доставить одну посылку.
Redis-очередь vs. RabbitMQ/Celery: собственная документация Celery честно говорит о компромиссе — Redis как брокер хорошо подходит для быстрой доставки небольших сообщений, тогда как RabbitMQ более изящно справляется с крупными сообщениями и очень высокой частотой сообщений на реальном масштабе. Наши задачи небольшие (пейлоад уведомления, повтор вебхука, триггер отчёта), и мы уже держали Redis под кеш и rate-limiting — добавление второго брокера рядом с ним означало бы эксплуатацию двух систем ради нагрузки, которая комфортно помещается в одну.
Очередь на Redis. Это единственное решение из этого списка, которое мы почти наверняка пересмотрим без серьёзного изменения требований — если объём поддержки вырастет в 10–20 раз, или появится потребность в устойчивом воспроизведении каждого клиентского события для аналитики, расчёт Kafka развернётся из «избыточно» в «правильно». Мы туда ещё не пришли, а строить под масштаб, которого ещё нет, — значит добавлять операционную поверхность сегодня без пользы сегодня.
Виджет, построенный на полноценном фреймворк-рантайме, — неверная форма для встраивания независимо от того, какой это фреймворк; вся категория встраиваемых виджетов (в частности, виджеты чата поддержки) поставляет скомпилированные, фреймворк-агностичные рантаймы именно по этой причине — вы не наследуете React хост-страницы (или его отсутствие), и вы не можете предполагать, что хост-страница хочет загрузить рантайм вашего фреймворка рядом со своим.
React зарабатывает своё место на внутренней консоли по той же логике, что мы применили бы к любой внутренней операционной панели: зрелые паттерны real-time UI, самый широкий пул найма для того, кому команда клиента в итоге это передаст, и никаких ограничений по размеру бандла, потому что он никогда не находится на чужой витрине. Смысл разделения этих двух ответов явно: «мы используем React» — это не архитектурное решение, это привычка. Реальное решение — «что нужно каждой поверхности», и здесь двум поверхностям было нужно противоположное.
Скомпилированный, фреймворк-агностичный Web Component для виджета на витрине (одинаково встраивается в Shopify, кастомную React-витрину или legacy jQuery) + React для авторизованной внутренней консоли эскалации.
Опубликованный заглавный результат не случайное совпадение с этими пятью решениями; каждое из них убирает конкретный способ, которым система иначе бы отказала:
Показатель автоматического решения в районе 68% находится ближе к верхней границе того, что реалистично достижимо для зрелого внедрения — публично сообщаемые отраслевые цифры обычно кластеризуются в диапазоне 40–70%, а более высокие заголовочные цифры от вендоров обычно отражают благоприятные условия, а не типовую базовую линию. Мы не заявляем аномальный результат — мы объясняем, почему попадание в достоверную верхнюю границу этого диапазона потребовало всех пяти решений выше, а не только выбора модели.
Стоит сказать прямо, потому что ни одна архитектура не вечна:
Ни один из этих пунктов не является провалом исходного решения — это условия, при которых тот же самый процесс рассуждения, запущенный заново, дал бы другой ответ.
Мы публикуем эту таблицу доверия намеренно. Клиенту полезнее документ «вот в чём мы уверены, а что мы бы перепроверили», чем документ, который выглядит чисто только потому, что неопределённость из него тихо вычистили.