Перейти к содержимому
← Назад к блогу

Разработка ИИ-агентов: от прототипа до продакшена

В ноябре 2024 года владелец грузовой брокерской компании в Гданьске попросил меня перевести его диспетчерского помощника из демо в ежедневную работу. Демо отвечало на двадцать образцовых вопросов о грузах, тарифах и доках. Оно работало на сорока чистых документах. Оно впечатлило комнату. Я подключил того же помощника к живому хранилищу документов на тысячу сто файлов, дал доступ сорока диспетчерам и увидел четыре сбоя до обеда. Он процитировал тариф из договора, истекшего в марте. Он выдумал номер дока для склада с шестью доками. Он отвечал двадцать две секунды на один вопрос о задержанном грузе. Он сжигал девять центов за вызов при его объёме. Владелец поставил запуск на паузу в тот же день. Каждый агент, которого я выпускаю в продакшен, теперь проходит дорогу ниже. Шесть этапов ведут его от того провального утра к ровной ежедневной работе. Сузьте задачу и выйдете в прод быстрее: бот, который за два дня разгребает одну очередь лидов, я разбираю в посте Соберите AI-агента, который обрабатывает лиды за 48 часов. Я веду их по порядку. Каждый этап приводит цифры, которые я замерил на клиентских сборках.

Почему демо умирают в продакшене

Демо живут в теплице. Двадцать образцовых вопросов, сорок свежих документов, один пользователь, ноль счётчика расходов. Продакшен открывает дверь. Две тысячи живых вопросов приходят с опечатками, половинами фраз и фотографиями бумаг. Тысяча сто документов включает истекшие договоры, дубли тарифов и три версии одного прайса. Сорок человек спрашивают разом в понедельник утром. Каждый вызов ложится в счёт.

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

Порядок починки важен. Сначала я чиню поиск: ни одна правка формулировок не переживёт неверные исходные страницы. Затем добавляю набор оценки: мне нужен счётчик очков раньше любых других изменений. Третьим ограничиваю стоимость и задержки: бюджеты определяют выбор модели. Четвёртым закрываю комплаенс-слой: имена клиентов и номера телефонов заслуживают отдельной проверки. Журнал вызовов, конвейер очистки и ночную задачу удаления за этой проверкой разбираю в посте Почему вашему ИИ-агенту нужен комплаенс-слой. Затем выпускаю. Пропустите порядок, и вы будете настраивать фразы поверх сломанного поиска.

RAG-конвейер, который держит нагрузку

Документы я храню в Postgres с pgvector на Neon. Одна таблица держит фрагменты: chunk_id uuid, tenant_id text, source_url text, updated_at timestamptz, content text, embedding vector(1536). HNSW-индекс по колонке эмбеддингов отдаёт поиск ближайших соседей с m 16 и ef_construction 64. Btree-индекс по tenant_id с updated_at режет каждый запрос под нужного клиента и сначала под свежие файлы.

Текст я режу на фрагменты по четыреста-шестьсот токенов с перехлёстом восемьдесят токенов. Каждый фрагмент хранит исходную страницу и дату. Короткие фрагменты теряли контекст в моих замерах: полнота на фиксированном наборе вопросов встала на шестидесяти одном проценте. Длинные фрагменты размывали совпадение: модель цитировала целые страницы вместо отдельных строк. Окно четыреста-шестьсот подняло полноту до восьмидесяти восьми процентов на том же наборе.

Каждый запрос фильтруется по tenant_id, ищет пять ближайших фрагментов и отбрасывает всё, что владелец пометил истекшим. Ответ печатает источники внизу: имя документа, страницу, дату. Диспетчеры открывают их и верят тому, что проверили. Экран с напечатанными источниками и потоковым статусом я показываю в посте Проектирую интерфейсы, которые действуют: гид по агентному UI. Проверку полноты я прогоняю еженедельно. У каждого документа один автор, и протухшие копии покидают хранилище в день замены.

Оценки: привычка двухсот примеров

Я держу двести фиксированных вопросов с записанными ответами и исходными страницами. Восемьдесят закрывают рядовую работу: номера грузов, тарифы, часы доков. Шестьдесят несут опечатки, пропущенные улицы и полузабытые имена. Сорок требуют отказа: бот сообщает, что не нашёл документ, и передаёт чат диспетчеру. Двадцать ставят ловушки: истекшие тарифы, конфликтующие прайсы, два склада на одной улице.

Полный набор я прогоняю перед каждым релизом и записываю дату, версию модели и счёт. Просадка сильнее трёх пунктов останавливает релиз, и это правило держится. Гданьская сборка показала зачем: один мартовский файл тарифов истёк, набор поймал одиннадцать неверных цитат за один прогон, и релиз подождал день, пока я заменил файл. Без набора эти одиннадцать цитат дошли бы до сорока перевозчиков.

Каждый промах из продакшена входит в набор в день появления. Водитель звонит о неверном окне, я добавляю точный вопрос с верным ответом и источником. Со временем набор растёт за двести. Имя остаётся. Двести задаёт минимум, и минимум держит планку.

Бюджеты стоимости и задержек

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

По тарифам малой модели пятнадцать центов за миллион входных токенов и шестьдесят центов за миллион выходных такой вызов стоит около пяти десятитысячных доллара. Две тысячи сто входных токенов по $0.15 за миллион дают $0.00032. Триста пятьдесят выходных токенов по $0.60 за миллион дают $0.00021. Эмбеддинги добавляют $0.00002. Итого около $0.0005. Десять тысяч вызовов в месяц стоят пять долларов. Та же форма на флагманской модели по $5 за миллион входных и $15 за миллион выходных стоит около $0.016 за вызов, или сто шестьдесят долларов в месяц за те же десять тысяч вызовов.

Поэтому рядовые чтения я направляю на малую модель, а флагман держу для писем по претензиям об ущербе, где один неверный пункт стоит дороже месяца вызовов. Я ставлю две планки задержек: первый видимый ответ внутри двух секунд через потоковый статус, полный ответ внутри девяти секунд. Каждую неделю я читаю p50 и p95 из журнала вызовов. Когда p95 переходит планку две недели подряд, я урезаю фрагменты раньше, чем трогаю модель.

Комплаенс-слой

Агенты, читающие имена клиентов, номера телефонов и договоры, нуждаются в малом слое данных с первого дня: я пишу каждый вызов инструмента со ссылкой на одобрение, чищу идентификаторы до попадания в журналы и ставлю на каждую строку дату удаления, чтобы старые переписки покидали хранилище по расписанию. Я добавляю этот слой в каждую сборку для продакшена до подачи живого трафика.

Чек-лист перед релизом

Этот список я прогоняю утром перед любым запуском.

  1. Поиск фильтрует по tenant_id и отбрасывает истекшие файлы.
  2. Каждый ответ печатает имя источника, страницу и дату.
  3. Набор из двухсот вопросов прогоняется в зелёной зоне без просадки сильнее трёх пунктов.
  4. Цена вызова лежит на бумаге с приложенной математикой месячного объёма.
  5. p95 отвечает внутри девяти секунд две недели подряд.
  6. Исходящие сообщения ждут ссылки на одобрение.
  7. Каждая хранимая строка несёт дату удаления с ночной задачей удаления за спиной.
  8. Один флаг откатывает релиз за минуты.

Если ваш помощник пока живёт в форме демо, закажите ИИ-аудит: я оценю вашу сборку по каждому этапу выше.

Что я померил

Демо-в-прод измеряется только числами. Что я читаю из биллинга провайдера:

МетрикаДоПослеИсточник
Стоимость одного закрытого запроса$0.031$0.012provider billing
Проект: RAG-бойлерплейт для продакшена — pgvector в Postgres

Прочитали? К делу

Эти методы я применяю на платных проектах. Посмотрите, во сколько это обходится.