Ассистенту, подключённому к BugDump, задали разумный вопрос:
Отчёт по моим задачам за эту неделю, с разбивкой на «В работе» и «Готово».
Ответ был правильным. Но на него ушло около сорока секунд и больше тридцати вызовов инструментов: ассистент получил список задач, а потом по одной открывал историю каждой, чтобы понять, когда задачу перенесли и кто это сделал.
Это проблема N+1. Каждый бэкенд-разработчик встречал её в ORM: загружаешь список, а потом делаешь по запросу на каждую строку, потому что нужного поля в списке не было. Здесь то же самое, только на месте цикла сидит более медленный и дорогой движок запросов — языковая модель.
Агент не глупил. API BugDump просто не оставлял ему другого способа ответить.
Агент может спросить только то, что вы открыли
Модель с инструментами делает что-то близкое к планированию запросов. Она читает описания ваших инструментов, прикидывает, какие вызовы могут дать ответ, и выполняет их. Если нужной ей оси нет среди фильтров, она восстановит эту ось вручную — запрашивая строки и разглядывая их.
list_tasks в BugDump умел фильтровать по исполнителю, статусу, колонке, метке, любому пользовательскому свойству и диапазону дат создания. Не умел он одного — ответить, когда задачу завершили. Время завершения существовало: колонка resolvedAt, с индексом, готовая к работе. Она просто не была фильтром и не попадала в ответ.
Поэтому «что я закончил на этой неделе» с точки зрения модели раскладывалось так: получить список закрытых задач, затем открыть историю каждой и найти перенос. Один вызов на задачу. А информация была на расстоянии одного WHERE.
Три вопроса, которые стоит задать о каждом инструменте
С тех пор я проверил все инструменты по трём вопросам. Их стоит позаимствовать.
1. Можно ли фильтровать по каждой оси данных?
Не по «каждому полю», а по каждой оси, о которой кто-то спросит. Время почти всегда одна из них — и почти всегда их несколько: создано, обновлено, завершено, в последний раз затронуто. Это разные вопросы. Инструмент, который открывает только createdAt, заставляет всех, кто спрашивает о трёх остальных, листать строки.
Исправление — два параметра, resolvedFrom и resolvedTo. Четыре строки серверного кода. Они убрали целый класс N+1 из каждого отчёта, который пишет агент.
2. Знает ли инструмент, кто его вызывает?
«Мои задачи» — самая частая формулировка в таких запросах, и долгое время агент не мог ответить на неё, не угадывая. get_current_project возвращал участников проекта — имена, роли, ID, — но не говорил, от имени кого из них действует этот токен.
Посмотрите, что агент с этим делает. Он смотрит на список участников, видит «Alex Rivera», вспоминает, что пользователь сказал «мои», и выбирает того, чьё имя больше всего похоже на что-то увиденное раньше. Это работает, пока в команде не окажутся два человека по имени Alex.
Теперь сервер возвращает объект viewer: ID пользователя, имя и роль участника, которому принадлежит токен. Личность не должна быть догадкой.
3. Возвращает ли список то же, что и детальный запрос?
Если get_task отдаёт поле, а list_tasks его опускает, вы построили цикл запросов. Агент получает двадцать строк, ему нужно это поле для всех двадцати, и он двадцать раз вызывает детальный эндпоинт.
У меня в элементах списка не было resolvedAt и updatedAt, хотя в детальном ответе были оба. Добавить их — правка в две строки, которая убирает двадцать запросов туда и обратно.
Ловушка: временная метка, которая врёт
Добавляя фильтры, я чуть не выпустил четвёртый — updatedFrom / updatedTo по updatedAt, — потому что это очевидный способ спросить «что изменилось на этой неделе».
Он принёс бы прямой вред, и причина шире этого случая.
updatedAt в таблице задач обновляет ORM: любой UPDATE строки его сдвигает. Но статус, исполнителя, приоритет, метки и срок задачи BugDump хранит в отдельной таблице значений. Поэтому:
назначение исполнителя не сдвигает
updatedAt;смена приоритета не сдвигает;
перенос между двумя открытыми колонками не сдвигает;
голос незнакомца за задачу на вашем публичном портале — сдвигает.
Фильтр по этой колонке ответил бы на «над чем я работал на этой неделе» списком, в который не попала работа, зато попал чужой клик. Хуже того, ответ выглядел бы правдоподобно: плохой инструмент ломается не ошибкой, а уверенным неправильным ответом.
Я не стал его добавлять и написал об этом в описании инструмента: updatedAt — отметка о том, что строку тронули, а не об активности. Описание инструмента — это документация, которую модель действительно читает. Используйте его, чтобы исключать варианты, а не только чтобы объяснять.
Во что обходится ошибка
Медленный эндпоинт в мире агентов — это не медленная страница. Это:
Задержка, которую чувствует пользователь. Тридцать последовательных запросов, и перед каждым модель решает, каким будет следующий.
Сожжённое контекстное окно. Каждый ответ — это токены. Тридцать историй задач могут вытеснить то, о чём пользователь спрашивал.
Деньги. Это оплачиваемые токены — каждый раз, когда кто-то спрашивает.
Молчаливое обрезание. Агент, которому не хватило места, останавливается раньше и пересказывает то, что успел собрать, а в ответе не пишет «я прочитал только одиннадцать задач из сорока».
Из-за последнего пункта это проблема правильности, а не производительности.
Тот же отчёт сейчас
С тремя исправлениями недельный отчёт выглядит так:
get_current_project— кто я и какие есть колонки;list_tasksсassigneeId+ колонка «В работе»;list_tasksсassigneeId+status: "closed"+resolvedFrom.
Три вызова, и их число не зависит от того, сколько задач вернётся. В каждой строке уже есть resolvedAt, так что для группировки по дням больше ничего не нужно.
Одну оговорку я документирую, а не прячу: resolvedAt записывает, когда задача в последний раз попала в закрытую колонку. Переведите колонку из открытых в закрытые — и каждая задача в ней получит отметку этого момента, так что недельный отчёт может подхватить работу, которую закончили ещё несколько месяцев назад. Такие оговорки агент сам не выведет. Записывайте их там, где их прочитает модель.
Если вы делаете MCP-сервер
Задайте настоящему агенту настоящий вопрос и посмотрите журнал вызовов. Не тот удачный сценарий, который вы спроектировали, а вопрос, который пользователь напечатал бы своими словами в пятницу после обеда.
Каждый цикл, который вы увидите, — это ось, которую ваш API не открыл. Исправление обычно — условие WHERE, поле в ответе списка или предложение в описании. Это гораздо дешевле, чем обходной путь, который найдёт модель.
MCP-сервер BugDump бесплатен на всех тарифах, включая бесплатный: подключите Claude Code к своему проекту по статье Как подключить BugDump к Claude Code (MCP) и попросите его рассказать, что вы сделали за неделю.
Читайте также
Что я понял, пока делал MCP-сервер для инструмента разработчика — почему инструменты для агента проектируют иначе, чем REST API, и как сделать так, чтобы агент пользовался ими правильно с первой попытки.
OAuth на MCP-сервере: Claude и ChatGPT подключаются без токена — как MCP-сервер авторизует приложения, в которые некуда вставить токен, и что он проверяет при каждом запросе.
Пусть ваш ИИ-агент читает баг-репорты — как Claude Code через MCP-сервер сам получает отчёты, логи консоли и сетевые запросы, и дашборд открывать не приходится.