Всем привет! Решил написать AI-агента, который отвечает на вопросы по рабочему проекту. Думал: пара вечеров - и готово. В итоге несколько недель, куча граблей и странных открытий - ответы по 25 минут, бюджет токенов тает как снег, агент уходит в бесконечный цикл и тупо спамит одними и теми же запросами, а семантический поиск, который казался серебряной пулей - не работает. В статье - как я с этим боролся: планировщик + синтезатор, давление как дедлайн, роли “Новичок/Исследователь/Эксперт” и защита от зацикливания. Боль и страдания а так же конкретные решения.

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

Выбор фреймворка

Вначале мне все казалось максимально просто и понятно: берем готовый фреймворк, описываем логику системы, реализуем ее в коде - собственно и все, профит! Почти сходу я выбрал Python, LangGraph/LangChain, SQLite для локального хранилища, так как планировал быстро запустить прототип, а потом докрутить все как надо (перейти на Postgres и вот это вот все). Почему LangGraph? Потому что это довольно популярный и мощный фреймворк на данный момент.

Стек выбран. С чего начать? Решил не бросаться сразу в самое сложное, а начать с простых и относительно понятных задач - набить руку на фреймворке и быстро получить что-то рабочее.

Сервисы

Та часть, которая радует меня больше всего (дальше поймете почему…). Здесь поведение достаточно строго детерминировано. Агент имеет четко определенный набор действий, он ожидает получить четко определенные запросы от пользователей, и его действия и сам ответ тоже жестко определены. В результате странное или непредвиденное поведение, различные галлюцинации и тому подобное просто не наблюдаются. Система работает как часы.

Итак, какие сервисы были добавлены?

  • Review Tracker - помощник в ревью (процесс, не код), а именно пинг забывчивых, соблюдение времени на ревью, саммари, сложно, важность и т.п. полезные индикаторы

  • Duty Notifier - график дежурств на каждый день

  • Deploy analyzer - сводка, проверка, саммари, пинги и т.п. для деплоя

  • Vacation notifier - удобный ежедневный мини-отчет про команду: кто в отпусках (начало и окончание), у кого праздники, кто на больничном, что будет завтра и так далее.

Небольшая схемка как устроены сервисы:

Все вышеперечисленные штуки были реализованы максимально быстро и вообще без каких-либо проблем, и это вселило в меня надежду замахнуться на мегапроект.

Мега-задача

В чем суть вопроса? А все просто: вот уже много лет у меня жила идея реализовать агента, который будет отвечать на вопросы приходящих людей по проекту. Это всегда было актуально, так как проект сложный и в течение недели возникает множество вопросов от разных людей (от продукта, саппорта, клиентов и тому подобных) о том, как работает та или иная фича, какие настройки у конкретного клиента, что задано в конфигах, какой протокол взаимодействия между определенными клиентами, какие поля мы получаем или отправляем - в общем, тысячи их.

В последнее время с этим уже неплохо помогают разнообразные клоды, но их использование все равно замкнуто на разработчике, а хотелось бы инструмент, который могли бы использовать обычные люди. Кроме того, у сторонних агентов в базе нет полного набора инструментов, их надо либо задавать, либо делать какой-то шаренный список. А также это все-таки “закрытое” стороннее решение, а хочется всегда чего-то своего, теплого и лампового, пусть даже и не всегда такого мощного.

Агенты/Инструменты

Итак, поехали. Вначале все просто - создаем тулы, получаем доступы, настраиваем MCP, где и куда надо. Создаем первых агентов. Все быстро, ноль проблем. Я создал следующих агентов и дал им базовый набор инструментов для чтения/записи (в основном, конечно, чтения, так как использоваться будет кем-то и модификация данных это не то, чего бы нам хотелось):

  • supervisor - начальная точка входа и вызов других агентов

  • поиск по-документам

  • поиск по-коду

  • поиск по-задачам

  • работа с метриками и дашбордами

  • поиск в мессенджерах

В теории и на практике это примерно 90% необходимой информации, которая должна закрывать ответы на входящие вопросы.

Момент истины.

Пишу вопрос боту и… Он отвечает! Победа? Нет. Полное поражение - качество ответа такое, от которого глаза лезут на лоб, и показать такое кому-бы то ни было просто нельзя.

Как оказалось, создать агента и дать ему набор инструментов - это еще даже не полдела, так как качество ответов, которое генерировало это решение, было даже не близко к современным агентам для разработчиков типа Codex, Claude и тому подобных. При том что использовались идентичные модели. Вот здесь уже стало наглядно видно, что хороший агент - это далеко не только лишь ллм.

Но сдаваться пока рано, попробуем подфиксить.

Поиск по документам, или проблема номер раз

Казалось бы, что тут сложного - ищем, находим, отдаем результат. Документы можно доставать по HTTP или через API. Дергаем поиск, получаем результат, читаем, находим релевантную информацию - формируем ответ. На практике оказалось, что формулировка поискового запроса очень важна - их может быть несколько, в запросе может быть ошибка в слове, и тогда поиск ничего не найдет. Надо отсекать и выбирать только релевантные страницы. Некоторые страницы могут быть огромными и не вмещаться в контекст или раздувать его. В результате легкая прогулка начинает превращаться в попытку залезть на гору.

Что было реализовано, чтобы стабилизировать решение и возвращать релевантный ответ - лимит на количество релевантных страниц в поиске, 2-3 как правило мне оказалось что хватает. Лимит на чтение документа в 4000 байт и далее, если надо последующее чтение документа, частями до конца. Определение релевантных спейсов для поиска, так как полезные страницы могут находиться в нужных нам разделах и перебирать все подряд может быть неэффективно. Ну и сейчас я думаю, что это только верхушка айсберга, так как для быстрого и эффективного поиска, вероятно, нужна некая схема/индекс или содержание, где бы агент мог быстро определиться с тем, что и где находится. В целом любая структурированная информация может существенно сокращать время и увеличивать качество поиска (внезапно немного напоминает людей, не правда ли?).

Поиск по задачам, метрикам и мессенджеру пропустим, поскольку там хоть и не было сразу все гладко, но это все более-менее заработало. И перейдем к самому интересному - поиску по коду.

Поиск по коду

Исторически так получилось, что проект разбит на несколько репозиториев, и для качественного ответа может понадобиться провести поиск либо в одном, либо в нескольких из них.

Наивный подход

Простой grep + read_file работал у меня из рук вон плохо - большое количество вызовов инструментов, как результат - огромный расход токенов, и неудовлетворительное качество ответа как следствие.

Далее я начал его докручивать:

grep_code - как оказалось, grep лучше использовать не простой, а сразу с возвратом найденных строк, а также строк до и после вхождения (-A / -B - опять внезапно полезные ключи, которые я и сам люблю использовать). Так агент сразу может увидеть больше контекста: названия функций, логику работы и т. п.

read_file - ограничиваем чтение. Весь файл читаем только в том случае, если с первого раза не нашли нужной информации.

Семантический поиск

Ок, раз обычный подход работает не очень, попробуем подключить тяжелую артиллерию. Сказано - сделано. Добавляем семантический поиск. Вначале мне казалось, что это какая-то чуть ли не серебряная пуля, когда агент не грепает по коду, а формулирует некий текстовый запрос и в ответ получает нужный, “почти” готовый результат. Но на практике оказалось, что этот инструмент работает существенно хуже, чем поиск стандартными инструментами (кстати, тот же клод использует тоже простые инструменты).

Семантический поиск, если кто не в курсе - это поиск не по словам, а по смыслу. Звучит многообещающе. На практике ллм-ка превращает ваш код в списки векторов, и при вашем запросе вектор вашего предложения сравнивается с кодом. Какая-то магия, и хорошо было бы, если бы она работала, но нет.

Для создания индекса пришлось сделать отдельный скрипт для токенизации репозиториев. Он проходит по проекту и файл за файлом генерирует токены и описания для них, записывая полученную информацию в Chroma DB. Проиндексированные файлы могут устаревать, их надо переиндексировать, новые файлы могут добавляться и так далее - в общем, масса головняка.

В итоге ответы опять нерелевантные, грусть-печаль, рефлексия и попытка снова как-то улучшить решение. Оставляю оба инструмента поиска, при этом простой поиск в приоритете, поскольку он абсолютно прозрачен (в целом при поиске чего-то я действую похожим образом: простые grep + Ctrl+F - инструмент максимально понятен и проверен веками). Параллельно добавляю полный трекинг и логирование всей системы агентов, вызовов ими тулов и тому подобного, чтобы понимать верхнеуровнево, что происходит.

Проблемы с 503 ошибкой. Реализация фоллбеков

Еще одна беда пришла откуда не ждали. Внезапно оказывается, что недостаточно просто закинуть денег на счет и подключить сторонние платные API для использования ллм-моделей. У них есть квоты на частоту запросов, а также различные лимиты, которые они сами устанавливают, когда сервис перегружен. В результате, например, что Anthropic, что Google регулярно динамили меня ошибкой 503 на платных тарифах. Уточню, что я тестировал эти модели исключительно с целью оценить качество, проверить, на что способен прототип, и т.п. То есть это был далеко не прод, без параллельных вызовов от разных пользователей. В целом я не планировал их использовать в дальнейшем, так как у нас есть свой набор моделей, но сделал это чисто ради научного интереса. Но даже на своих моделях этот вопрос важен, так как обращения к ллм дороги, и чем мощнее у вас модель и чем больше вызовов - тем больше будут расходы.

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

Эволюция агента

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

Разные модели для разных агентов

Для экономии токенов в некоторых случаях для простого класса задач (например, классификация текста на 3 типа) я решил использовать локальные модели. Например, ollama/qwen2.5:14b вполне себе неплохо справляется, потребляет мало ресурсов, и качество ответов вполне приемлемое. Но как только я начал использовать разные модели, то оказалось, что они и работают не одинаково - может сильно отличаться размер и формат ответа, по-разному работает кэширование, для некоторых моделей формат вызова тулов может отличаться, или слабые модели могут вообще на них глючить и не использовать как надо. Все это мелочи и детали, но они пожирают уйму времени, когда начинаешь в них углубляться.

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

Добавление планировщика и synthesizer

Следующим шагом монолитный супер-агент был разделен на несколько частей. В частности, вначале был добавлен планировщик. На самой мощной модели он составлял план из 3-5 шагов, что надо сделать, и постил его в лог. В качестве еще одной части был добавлен synthesizer - по сути, это агент для создания ответа пользователю на основе всей собранной информации.

Где-то на этом этапе уже произошел заметный прорыв, бот начал отвечать что-то похожее на правду. Но успокаиваться рано, движемся дальше.

Добавление thinking в логи и консоль

Как еще можно понять, что происходит в нашей системе? Правильно, надо сделать ее максимально прозрачной, шаг за шагом, чтобы видеть всю последовательность: что на входе, что агент хочет сделать, каких агентов и инструменты он будет вызывать, какой план, что он думает. Это все надо, чтобы проверить ответ вручную и шаг за шагом докрутить до нужного состояния. Понять, где он ошибается, какие лишние шаги делает. Подсказать, поправить, отредактировать промпты для агентов и тому подобное. Итог - качество продолжает расти.

Добавление лимитов

Но теперь другая беда. Иногда ответы занимают 20-25 минут - это много, очень-очень много. Особенно когда кто-то ждет и даже не понимает, что происходит. В нашем человеческом мире мы работаем не так: нас спросили - быстрый ответ, начало инвестигации, какой-то результат за разумное время, либо, если вопрос или задача сложные, то можно, предварительно сообщив, и вовсе завернуть это в билет и начать полноценное расследование на несколько дней. Пусть бот делает так же. Добавим ему ограничений и внутренние тайминги.

Добавляем лимиты на число обращений к ллм, глобальное и локальные по агентам. Зачем? Чтобы иметь защиту от перерасхода и ухода бота в бесконечность. Добавляем лимит по времени - это больше для людей, чтобы бот отвечал за какое-то разумное время, поскольку вопрос пользователя может быть внезапным, он может чего только не попросить, на что не хватит никакого времени (привет людям со списком из 10и вопросов в одном посте).

Но что если вопрос реально сложный, бот где-то на середине расследования, а время заканчивается? Должен ли бот как-то понимать это? Думаю, да. Добавляем сразу две вещи: во-первых, формируем ответ по уже найденным данным и указываем то, что не было найдено или на что не успели ответить в исходном вопросе, а также предлагаем, если это еще актуально, продолжить ответ с этого места. Вторая идея - добавляем в бота так называемый бюджет, который начинает постепенно снижаться в зависимости от прошедшего времени и израсходованных токенов. Это так называемое pressure, или давление на бота, с целью мотивировать его заканчивать исследование и стремиться к формированию ответа. В зависимости от уровня pressure меняется промпт к ллм, становясь более жестким.

Здесь у меня возникает некая аналогия с дедлайнами: никто их не любит, но что поделать - придется и боту немного пострадать. Кнут и пряник, как говорится.

При расходе около 90% бюджета роутер принудительно направляет собранный граф в генератор ответа.

Примеры:

Легкое давление:

“Skip optional deep-dives. Prefer a good answer now over a perfect answer late.”

Сильное давление:

“Give the best answer you can RIGHT NOW with available information.”

“Do NOT fetch extra sources. Do NOT replan. Answer directly.”

  # Example
  pressure = max(time_frac, call_frac)
  if pressure < 0.5:
      note = "Budget OK"
  elif pressure < 0.75:
      note = "Skip optional steps..."
  else:
      note = "STOP. Answer NOW."

Где-то на этом этапе уже начинаю проверять работу бота на реальных вопросах, так как мои синтетические тесты проходят. В целом ответы уже вполне приемлемые.

Добавление ролей

Ради интереса начинаю сравнивать некоторые ответы боевого бота и клод-подобных систем. Делаю интересное открытие: зачастую клод делает гораздо меньше шагов, дает ответ гораздо быстрее и сопоставимого качества. Начинаю изучать, и оказывается, что далеко не для всех вопросов нужен детальный ответ, какой-то подробный план и большие лимиты (а агент, если ему дать большой бюджет, часто любит израсходовать его весь, почти как мы). В результате решаю сделать разделение на роли и добавить роль “новичка”, а текущую обозначить как “исследователь”.

Разделяю их на уровне конфигурации: для “новичка” подрезаю лимиты, сокращаю план до 2-3 шагов, уменьшаю время. Упрощаю промпты, кое-где подрезаю тулы и тому подобное. В результате для относительно простых вопросов удается существенно сократить время без заметного снижения качества, а если надо, то всегда можно переключиться на более мощную роль или провести полноценное изучение в несколько шагов. В итоге новичок обычно укладывается в 5 мин., чего с учетом его качества хватает примерно на половине запросов.

Роль эксперт

Ну и на закуску, чтобы использовать уж наверняка все бест-практис на данный момент, была добавлена роль “эксперт”. В ней лимиты на вызовы ллм были подняты до небес, подняты лимиты по времени на генерацию ответов. Использованы самые мощные модели. А самое главное - добавлен динамический репланнинг, то есть теперь планировщик мог оценить полученную информацию на любом шаге, сразу проверить, достаточно ли полученных данных, и сразу дать ответ, если надо, либо поменять план на более оптимальный, добавить или убрать шаги при необходимости. Был добавлен quality gate, который проверяет исходный вопрос пользователя и полученную информацию. На финальном шаге осуществляется контроль качества и проверка доказательств при ответе на вопрос.

сравнение ролей

Новичок

Исследователь

Эксперт

Thinking

Выкл

Вкл

Вкл

Planning steps

2

5

6

LLM calls

20

45

70

Timeout

0.5*base

base

1.5*base

Max answer size

2800

8000

12000

Replan

0

0

3

Quality gate

No

No

Yes

Prompts

novice

researcher

researcher+self-extended

Финальная схема

Оптимизация, оптимизация, оптимизация

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

  • кэширование (кстати провайдер-специфичная штука, так что для разных придется докручивать)

  • лимиты где только можно на размер возвращаемых и отправляемых данных

  • изоляцию контекста между агентами

  • лимит вызова инструментов

  • борьбу с зацикливанием

Изоляция контекста - достаточно важная часть, так как она сужает окно поиска для агента, экономит токены и помогает его мыслям не растекаться а исследованию идти поступательно в заданном направлении. Агент исполнитель видит только нужную ему на данном шаге часть.

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

Про борьбу с зацикливанием особенно интересно - как оказалось, в некоторых ситуациях агент может входить в цикл (не знаю, бесконечный или нет, жаль было токенов) и начинает запрашивать одни и те же похожие запросы через одни и те же тулы, получать похожие ответы и так по новой. Спасло от этого добавление специального механизма защиты на похожесть ответов: если она близка - то завершение этого цикла.

Пример сравнения похожести:

  # difflib.SequenceMatcher comapre similarity
  def _query_similarity(a: str, b: str) -> float:
      return SequenceMatcher(None, a.lower(), b.lower()).ratio()

  # stop on huge similarity
  if any(_query_similarity(query, prev) > 0.85
         for prev in state["step_search_queries"]):
      state["search_stuck"] = True

Небольшой под-итог по оптимизациям, без них исходное решение получалось в 3-5 раз дороже. Чистый эффект от кеширования примерно 40-50% снижения.

Сохранение истории

Ну и напоследок. Для сохранения результатов исследований по пришедшим вопросам было добавлено сохранение саммари в базу (так называемая сборка фактов). Релевантность накопленных ответов и их использование зависит от времени, кроме того, старые через какое-то время совсем удаляются. Бот может использовать и использует эту информацию для подтверждения, как дополнительную.

При возникновении похожего вопроса planner может вообще использовать целиком данные из накопленной базы фактов, если они не устарели и не проводить полный цикл исследования.

Что еще можно сделать?

Eval loop - подготовка большого пула вопросов и прогон бота по нему при модификациях, оценка качества ответа неким score и таргет на рост этого показателя. Есть еще, наверное, 10к разных идей, но думаю, что уже не буду перегружать статью.

В итоге то, что вначале казалось мне относительно простым, вылилось в довольно большое приключение с разнообразными проблемами и внезапными открытиями. Для доведения прототипа до чего-то похожего на рабочее решение ушло несколько недель, хотя вначале я думал, что его можно запилить за парочку вечеров на расслабоне).

Сухой остаток

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

Короткий чек-лист если соберетесь делать что-то подобное:

  • Контролируйте время и вводите “дедлайны”. Без жестких рамок агент может отвечать по 20-25 минут. Вводите тайминги и глобальные/локальные лимиты на вызовы ллм

  • Не бросайте пользователя в неведении на долго. Если время вышло, а расследование завершено наполовину - лучше явно сообщить об этом

  • Разделяйте роли (Новичок / Исследователь / Эксперт). Далеко не каждому вопросу нужен глубокий анализ.

  • Защищайте решение от зацикливания

  • Кэшируйте - запросы и ответы к ллм и собранные факты

Спасибо за внимание.

Комментарии (0)