← Назад к кейсу
Пайплайн архитектуры

AI-агент ускорил ответы поддержки на 60%

Индустрия: E-commerce (D2C, ритейлер одежды среднего размера, ~450 тыс. заказов/мес)
Область работ: Диалоговый агент с tool calling, который ведёт чаты поддержки и обработку возвратов от начала до конца
GPT-4oFunction callingPostgresRedis queue
−60% времени первого ответа · 68% автоматически решено · CSAT 4.5/5
Такая глубина анализа стоит за каждым нашим проектом. Мы публикуем её как подтверждение того, как мы на самом деле работаем — реальные варианты, которые мы взвешивали, цифры, которые их отсеяли, единственная архитектура, пережившая столкновение с требованиями — чтобы процесс был для клиентов таким же прозрачным, как и результат. Это инженерный эквивалент того, как дизайн-студия показывает пять эскизов логотипа перед тем, как остановиться на одном, — только здесь следы резца — это цифры бенчмарков, а не карандашные линии.

Бриф

У клиента была растущая D2C-витрина с командой поддержки из 6 агентов, обрабатывающих около 3000 диалогов в неделю через чат и почту. В пиковый сезон (от Чёрной пятницы до Нового года) время первого ответа регулярно уходило за 20 минут, и большая часть этого объёма приходилась на один и тот же небольшой набор вопросов: «Где мой заказ», «Хочу вернуть товар», «Можно ли получить возврат средств».

Запрос звучал не как «сделайте нам чат-бота». Он звучал так: сократить время ответа без расширения штата и не дать агенту ошибиться там, где на кону деньги (возвраты средств — как раз то место, где самоуверенный бот хуже, чем отсутствие бота вообще).

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

Нефункциональные требования, которые реально сформировали стек

ТребованиеПочему это важноКакое решение оно определило
Субсекундная воспринимаемая задержка (стриминг токенов, а не «спиннер, а потом стена текста»)Чат поддержки ощущается сломанным, если первый токен приходит дольше 3 секундБэкенд-фреймворк (§1)
Каждый вызов инструмента (возврат средств, поиск заказа) обязан строго соответствовать схеме, без исключенийНекорректный вызов возврата средств — это тикет в поддержку на нас самих, а не на исходную проблему клиентаLLM / движок tool calling (§2)
База знаний небольшая (~4 тыс. чанков политик/FAQ), но должна оставаться согласованной с транзакционными данными заказовРазделение «фактов о политике» и «фактов о заказах» по двум базам данных провоцирует рассинхронизациюСлой данных и поиска (§3)
Фоновые задачи измеряются сотнями в день, а не миллионами в секундуУведомления, триггеры CSAT-опроса, повтор нестабильного вебхука в OMS клиентаСлой асинхронных задач (§4)
Виджет встраивается прямо в витрину клиента, включая воронку оформления заказаКаждый лишний килобайт JS на конверсионно-критичной странице — это издержка, которую платит клиент, а не мыФронтенд-доставка (§5)
Окно разработки — 6 недель до пикового сезонаОтсекает всё, что требует поднимать новые категории инфраструктуры с нуляВсе решения выше
§1

Бэкенд-фреймворк — FastAPI vs. Django vs. Flask

Эскизы
  • Flask — «скучный, но мы его знаем наизусть» вариант. Минимализм, никакого навязанного мнения, огромная экосистема.
  • Django — «всё из коробки, быстрый запуск MVP» вариант, который мы уже выбирали раньше (ORM, админка и аутентификация достаются бесплатно). См. наш собственный FAQ на странице услуг, где это дефолтный ответ на вопрос «на каком фреймворке вы работаете».
  • FastAPI — async-нативный вариант, построенный на Starlette (ASGI) и Pydantic.

Основной цикл этого агента: держать открытым чат-соединение, стримить токены по мере генерации 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».

§2

LLM и движок tool calling — GPT-4o vs. self-hosted open source

Эскизы
  • Self-hosted (Llama 3, Mistral) за vLLM — полный контроль над стоимостью, без пословной оплаты, данные никогда не покидают нашу инфраструктуру.
  • GPT-4-Turbo — модель предыдущего поколения от OpenAI, уже проверенная в продакшене в других проектах.
  • GPT-4o со Structured Outputs — модель текущего поколения с фичей, нацеленной ровно на наш сценарий отказа.

Почему 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).

§3

Слой данных и поиска — Postgres+pgvector vs. выделенная векторная база

Эскизы
  • Postgres для транзакционных данных + Pinecone/Weaviate/Qdrant для базы знаний по политике.
  • Postgres для всего, включая векторный поиск через расширение pgvector.
  • Документная база (в духе Mongo) для логов диалогов, Postgres для заказов, векторная БД для поиска — три системы, три истории консистентности.

Почему мы не потянулись за выделенной векторной БД: база знаний здесь — это политика возвратов, FAQ по доставке и правила по категориям товаров — реалистично это несколько тысяч чанков, а не миллионы. Опубликованный бенчмарк pgvector от Supabase (индексация HNSW, сентябрь 2023) тестировал это на реальном масштабе: на датасете из 1 миллиона векторов размерностью 1536 индексация HNSW дала более чем в 6 раз больше запросов в секунду по сравнению со старым индексом IVFFlat при точности 98%, а при точности 99% тот же бенчмарк показал, что pgvector обгоняет Qdrant на эквивалентном железе. Наш корпус примерно на три порядка меньше датасета из этого бенчмарка. Поднимать вторую базу данных, второй набор эксплуатационных runbook’ов и второй домен отказа ради поиска по нескольким тысячам чанков FAQ — это не строгость инженерного подхода, а переусложнение задачи, которую Postgres уже решает на этом масштабе.

Почему хранение всего в одном экземпляре Postgres важно не только по производительности: реальный риск в брифе был не в скорости поиска, а в согласованности между «что агент сказал о политике» и «что на самом деле верно для этого конкретного заказа». Сверка правила доступности возврата (из корпуса политик с векторным поиском) с реальным окном возврата конкретного заказа (в транзакционной таблице orders) внутри одной транзакции тривиальна, когда это одна база данных. Между двумя системами это превращается в проблему eventual consistency с деньгами клиента на кону — именно тот сценарий отказа, которого бриф просил избежать.

Финальный выбор

Postgres с расширением pgvector, один инстанс, один пул соединений, одна история бэкапа/восстановления. Мы отслеживаем рост корпуса как триггер для пересмотра решения — то же исследование указывает, что точка перехода, где выделенный векторный движок начинает выигрывать по сырой пропускной способности, находится далеко за отметкой в 10 миллионов векторов. Мы далеко не рядом с ней.

§4

Слой асинхронных задач — Redis-очередь vs. Celery-на-RabbitMQ vs. Kafka

Эскизы
  • Kafka — вариант «на случай если понадобится при масштабировании» по умолчанию.
  • RabbitMQ (через Celery) — традиционный брокер очереди задач.
  • Очередь на Redis (в духе RQ) — минимальный вариант, переиспользующий инфраструктуру, которую мы уже держим под кеш и сессии.

Почему 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 развернётся из «избыточно» в «правильно». Мы туда ещё не пришли, а строить под масштаб, которого ещё нет, — значит добавлять операционную поверхность сегодня без пользы сегодня.

§5

Фронтенд-доставка — виджет и внутренняя консоль

Эскизы
  • Задача А: чат-виджет на витрине клиента. Сторонний embed на страницах, где каждый килобайт JS — издержка, которую платит конверсия клиента, включая, в худшем случае, саму воронку оформления заказа.
  • Задача Б: внутренняя консоль эскалации, которой пользуются агенты клиента. Никаких ограничений на встраивание, авторизованный внутренний инструмент, нужны обновления в реальном времени и богатая компонентная экосистема для плотных по данным экранов.

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

React зарабатывает своё место на внутренней консоли по той же логике, что мы применили бы к любой внутренней операционной панели: зрелые паттерны real-time UI, самый широкий пул найма для того, кому команда клиента в итоге это передаст, и никаких ограничений по размеру бандла, потому что он никогда не находится на чужой витрине. Смысл разделения этих двух ответов явно: «мы используем React» — это не архитектурное решение, это привычка. Реальное решение — «что нужно каждой поверхности», и здесь двум поверхностям было нужно противоположное.

Финальный выбор

Скомпилированный, фреймворк-агностичный Web Component для виджета на витрине (одинаково встраивается в Shopify, кастомную React-витрину или legacy jQuery) + React для авторизованной внутренней консоли эскалации.

Финальная архитектура

Витрина клиента — Shopify / кастом / др.
Чат-виджет (скомпилированный Web Component)
SSE / WebSocket
FastAPI (ASGI)
Обработчик чата/сессии (async)
Исполнитель вызовов инструментов — Pydantic-схема = OpenAI fn spec
GPT-4o + Structured Outputs
Postgres
заказы / возвраты (транзакционно)
pgvector — база знаний по политике и FAQ
эскалация по порогу уверенности
Очередь задач на Redis
уведомление об эскалации · триггер CSAT · повтор вебхука в OMS клиента
Консоль оператора
React · внутренняя · real-time

Как цифры связаны с решениями

Опубликованный заглавный результат не случайное совпадение с этими пятью решениями; каждое из них убирает конкретный способ, которым система иначе бы отказала:

  • Async-ядро FastAPI — это причина, по которой время ответа вообще падает под конкурентной нагрузкой: синхронный фреймворк, держащий потоки заложниками round-trip’ов к GPT-4o, ограничил бы число одновременных диалогов гораздо ниже, и время ответа деградировало бы ровно в пиковый сезон.
  • Гарантированные схемой вызовы инструментов GPT-4o + Structured Outputs — причина, по которой 68% можно уверенно решать автоматически: автоматическое решение работает только если неправильный вызов инструмента не может проскочить.
  • pgvector в том же экземпляре Postgres — причина, по которой ответы агента о политике остаются согласованными с реальными данными заказа: уверенная, но противоречащая заказу цитата политики — это проблема CSAT, а не проблема релевантности поиска.
  • Эскалация по порогу уверенности в консоль оператора через очередь Redis — причина, по которой CSAT держится на 4.5/5, а не падает: опубликованные отраслевые паттерны стабильно показывают разрыв CSAT между решением силами только ИИ и решением с участием человека; эскалация неуверенных случаев не даёт этому разрыву проявиться в агрегированной цифре.

Показатель автоматического решения в районе 68% находится ближе к верхней границе того, что реалистично достижимо для зрелого внедрения — публично сообщаемые отраслевые цифры обычно кластеризуются в диапазоне 40–70%, а более высокие заголовочные цифры от вендоров обычно отражают благоприятные условия, а не типовую базовую линию. Мы не заявляем аномальный результат — мы объясняем, почему попадание в достоверную верхнюю границу этого диапазона потребовало всех пяти решений выше, а не только выбора модели.

Где эта архитектура перестаёт быть правильной

Стоит сказать прямо, потому что ни одна архитектура не вечна:

Объём поддержки вырастает в 10–20 раз, или аналитике нужно устойчивое воспроизведение каждого событияПересмотреть Kafka (§4)
База знаний вырастает за пределы примерно 10 миллионов чанков, или задержка запросов на масштабе становится узким местомПересмотреть выделенную векторную базу данных (§3)
Объём токенов вырастает настолько, что стоимость API за токен превышает self-hosted GPU + операционные расходыПересмотреть открытые, self-hosted модели (§2)
Async ORM в Django дозревает до полной поддержки транзакций, а следующая сборка команды по форме больше admin/CRUD, чем стриминговаяDjango остаётся правильным дефолтом для такой формы задачи (§1)

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

Источники и уровень доверия

Async ORM/транзакции в Django 5.x всё ещё частичны
Официальная документация Django по asyncВысокий — прямо из документации Django
GPT-4o Structured Outputs: 100% соответствие схеме (strict) против <40% у gpt-4-0613
Блог OpenAI, «Introducing Structured Outputs in the API», авг. 2024Высокий — это собственная публикация OpenAI
GPT-4o ≈83% против Llama-3-8B-Instruct ≈59% по точности function calling
Berkeley Function-Calling Leaderboard, снимок 2024 г.Средний — лидерборд живой, расстановка наверняка уже сдвинулась
Kafka избыточен ниже ~10 тыс. устойчивых событий/сек / без потребности в replay-fan-out
Confluent, «When to Use Apache Kafka (And When You Shouldn’t)»Высокий — это слова самого Confluent о собственном продукте
Redis подходит для небольших/быстрых сообщений; RabbitMQ — для крупных сообщений на экстремальном масштабе
Официальная документация CeleryВысокий — прямо из документации Celery
pgvector HNSW: 6x+ QPS над IVFFlat при точности 98% на 1М векторов; обгоняет Qdrant при точности 99%
Инженерный блог Supabase, «pgvector v0.5.0», сент. 2023Высокий — Supabase опубликовали полную методологию, цифры можно перепроверить
Зрелые AI-внедрения поддержки: типичный диапазон автоматического решения ~40–70%; CSAT только-ИИ отстаёт от человеческого на несколько десятых балла
Агрегировано из отраслевых источников (данные Zendesk, кейсы Sierra AI)Средний — диапазон подтверждается разными источниками, но нет одного точного исследования

Мы публикуем эту таблицу доверия намеренно. Клиенту полезнее документ «вот в чём мы уверены, а что мы бы перепроверили», чем документ, который выглядит чисто только потому, что неопределённость из него тихо вычистили.