Разработка ИИ-агентов: от прототипа до продакшена
В ноябре 2024 года владелец грузовой брокерской компании в Гданьске попросил меня перевести его диспетчерского помощника из демо в ежедневную работу. Демо отвечало на двадцать образцовых вопросов о грузах, тарифах и доках. Оно работало на сорока чистых документах. Оно впечатлило комнату. Я подключил того же помощника к живому хранилищу документов на тысячу сто файлов, дал доступ сорока диспетчерам и увидел четыре сбоя до обеда. Он процитировал тариф из договора, истекшего в марте. Он выдумал номер дока для склада с шестью доками. Он отвечал двадцать две секунды на один вопрос о задержанном грузе. Он сжигал девять центов за вызов при его объёме. Владелец поставил запуск на паузу в тот же день. Каждый агент, которого я выпускаю в продакшен, теперь проходит дорогу ниже. Шесть этапов ведут его от того провального утра к ровной ежедневной работе. Я веду их по порядку. Каждый этап приводит цифры, которые я замерил на клиентских сборках.
Почему демо умирают в продакшене
Демо живут в теплице. Двадцать образцовых вопросов, сорок свежих документов, один пользователь, ноль счётчика расходов. Продакшен открывает дверь. Две тысячи живых вопросов приходят с опечатками, половинами фраз и фотографиями бумаг. Тысяча сто документов включает истекшие договоры, дубли тарифов и три версии одного прайса. Сорок человек спрашивают разом в понедельник утром. Каждый вызов ложится в счёт.
Три убийцы заканчивают большинство демо в первую неделю. Протухший поиск кормит модель старыми страницами, и модель цитирует их с полной уверенностью. Отсутствие оценок прячет урон: никто не прогоняет фиксированный набор вопросов после каждого изменения, поэтому дрейф расползается, и его никто не видит. Незамеренные стоимость и задержки бьют владельца в конце месяца, когда десять тысяч вызовов по девять центов каждый дают девятьсот долларов за ответы, которым никто не верит.
Порядок починки важен. Сначала я чиню поиск: ни одна правка формулировок не переживёт неверные исходные страницы. Затем добавляю набор оценки: мне нужен счётчик очков раньше любых других изменений. Третьим ограничиваю стоимость и задержки: бюджеты определяют выбор модели. Четвёртым закрываю комплаенс-слой: имена клиентов и номера телефонов заслуживают отдельной проверки. Затем выпускаю. Пропустите порядок, и вы будете настраивать фразы поверх сломанного поиска.
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, ищет пять ближайших фрагментов и отбрасывает всё, что владелец пометил истекшим. Ответ печатает источники внизу: имя документа, страницу, дату. Диспетчеры открывают их и верят тому, что проверили. Проверку полноты я прогоняю еженедельно. У каждого документа один автор, и протухшие копии покидают хранилище в день замены.
Оценки: привычка двухсот примеров
Я держу двести фиксированных вопросов с записанными ответами и исходными страницами. Восемьдесят закрывают рядовую работу: номера грузов, тарифы, часы доков. Шестьдесят несут опечатки, пропущенные улицы и полузабытые имена. Сорок требуют отказа: бот сообщает, что не нашёл документ, и передаёт чат диспетчеру. Двадцать ставят ловушки: истекшие тарифы, конфликтующие прайсы, два склада на одной улице.
Полный набор я прогоняю перед каждым релизом и записываю дату, версию модели и счёт. Просадка сильнее трёх пунктов останавливает релиз, и это правило держится. Гданьская сборка показала зачем: один мартовский файл тарифов истёк, набор поймал одиннадцать неверных цитат за один прогон, и релиз подождал день, пока я заменил файл. Без набора эти одиннадцать цитат дошли бы до сорока перевозчиков.
Каждый промах из продакшена входит в набор в день появления. Водитель звонит о неверном окне, я добавляю точный вопрос с верным ответом и источником. Со временем набор растёт за двести. Имя остаётся. Двести задаёт минимум, и минимум держит планку.
Бюджеты стоимости и задержек
Цену каждого вызова я считаю до выбора модели. Один гданьский ответ тянул пять фрагментов примерно по триста пятьдесят токенов каждый, добавлял триста токенов инструкций и истории и писал триста пятьдесят токенов в ответ. На вход уходило около двух тысяч ста токенов. На выход уходило триста пятьдесят.
По тарифам малой модели пятнадцать центов за миллион входных токенов и шестьдесят центов за миллион выходных такой вызов стоит около пяти десятитысячных доллара. Две тысячи сто входных токенов по $0.15 за миллион дают $0.00032. Триста пятьдесят выходных токенов по $0.60 за миллион дают $0.00021. Эмбеддинги добавляют $0.00002. Итого около $0.0005. Десять тысяч вызовов в месяц стоят пять долларов. Та же форма на флагманской модели по $5 за миллион входных и $15 за миллион выходных стоит около $0.016 за вызов, или сто шестьдесят долларов в месяц за те же десять тысяч вызовов.
Поэтому рядовые чтения я направляю на малую модель, а флагман держу для писем по претензиям об ущербе, где один неверный пункт стоит дороже месяца вызовов. Я ставлю две планки задержек: первый видимый ответ внутри двух секунд через потоковый статус, полный ответ внутри девяти секунд. Каждую неделю я читаю p50 и p95 из журнала вызовов. Когда p95 переходит планку две недели подряд, я урезаю фрагменты раньше, чем трогаю модель.
Комплаенс-слой
Агенты, читающие имена клиентов, номера телефонов и договоры, нуждаются в малом слое данных с первого дня: я пишу каждый вызов инструмента со ссылкой на одобрение, чищу идентификаторы до попадания в журналы и ставлю на каждую строку дату удаления, чтобы старые переписки покидали хранилище по расписанию. Точные таблицы, настройку очистки и ночную задачу удаления я описал в посте Почему вашему ИИ-агенту нужен комплаенс-слой и добавляю этот слой в каждую сборку для продакшена до подачи живого трафика.
Чек-лист перед релизом
Этот список я прогоняю утром перед любым запуском.
- Поиск фильтрует по tenant_id и отбрасывает истекшие файлы.
- Каждый ответ печатает имя источника, страницу и дату.
- Набор из двухсот вопросов прогоняется в зелёной зоне без просадки сильнее трёх пунктов.
- Цена вызова лежит на бумаге с приложенной математикой месячного объёма.
- p95 отвечает внутри девяти секунд две недели подряд.
- Исходящие сообщения ждут ссылки на одобрение.
- Каждая хранимая строка несёт дату удаления с ночной задачей удаления за спиной.
- Один флаг откатывает релиз за минуты.
Если ваш помощник пока живёт в форме демо, закажите ИИ-аудит: я оценю вашу сборку по каждому этапу выше.