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

Второй способ я исследую в собственной экспериментальной системе ELM — так я называю свой проект. Код предлагает варианты, нейросеть выбирает из них, а код собирает вызов. В проверяемом здесь режиме нейросеть не генерирует текст вызова.

На сборке вызовов локальная Bonsai-2-27B заметно обошла мою систему. На задачах, где нужен отказ от неподходящего инструмента, результат был обратным: 88,3% у ELM против 15,4% у Bonsai. Разберу, что именно я измерил и где проходит граница этого сравнения.

В сборке вызовов проигрываю. В корректном отказе — 88,3% против 15,4%.
В сборке вызовов проигрываю. В корректном отказе — 88,3% против 15,4%.

Разделяю выбор функции и создание значения

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

Это важное ограничение. Выбрать функцию поиска погоды и создать дату в нужном формате — разные задачи. Судья может понять, какой инструмент нужен, но не выбрать аргумент, которого код не предложил. Если нужного значения нет в запросе, одной нарезки подстрок мало. Нормализатор может преобразовать найденную дату, но для составного значения нужен другой способ построения кандидатов.

BFCL — набор задач на вызов инструментов. Сначала я запускал BFCL v3 собственной программой: по 60 отладочных вопросов в каждой категории. Получил 50,0% на simple, 50,0% на multiple и 86,7% на irrelevance, где правильный ответ — отказ. При этом в первых двух категориях функция была выбрана верно во всех 60 случаях. Потери возникали при сборке аргументов.

Потом проверил кандидатов отдельно, без обращений к судье. У части запросов необходимого значения вообще не было в тексте. Такой вызов нельзя собрать одним выбором подстрок.

Добавил нормализаторы дат, времени и единиц. На прежних выборках результат вырос до 51,7% и 53,3%: всего один и два дополнительных вопроса. Убедительного улучшения такая проба не показала. Зато стало понятно, что расширить список кандидатов и научить судью выбирать новые варианты — два отдельных шага.

Переношу проверку в официальный раннер

Мой чекер и данные v3 оставляли вопрос о сопоставимости. Я подключил ELM к официальному bfcl-eval 2026.3.23: данные v4, их проверка синтаксического дерева вызова, их правила оценки. Версии и выборки здесь различаются; прежние 60 вопросов нельзя смешивать с полным официальным прогоном на 1240 записях.

Сначала прогнал по десять записей из пяти категорий. В parallel и parallel_multiple получил нули. Я сперва заподозрил чекер, но ошибка была проще: мой обработчик возвращал один вызов, а эти задачи требовали список вызовов в одном ответе.

Добавил многовызов. Код делит запрос на подзапросы, судья выбирает число вызовов, затем функцию и аргументы для каждого. В пробе parallel поднялся до 4 из 10, parallel_multiple — до 2 из 10. Эти первые десять записей входят и в полный набор, поэтому независимой проверкой правки проба не была.

Я исправил сохранение результатов. Если не задать папку через BFCL_PROJECT_ROOT, пакет сохранял их среди установленных библиотек Python — в site-packages, вне репозитория. Теперь задаю путь до загрузки пакета; результаты лежат рядом с метаданными и контрольными суммами. Команда проверки тоже могла сообщить об успехе после сбоя: конвейер возвращал статус последней команды tail, которая показывала конец вывода. Теперь сначала запускаю проверку и смотрю её результат, затем отдельно делаю коммит.

Сравниваю две системы по категориям

В simple доступна одна функция, в multiple нужно выбрать из нескольких. parallel требует нескольких вызовов одной функции, parallel_multiple — вызовов разных функций. irrelevance проверяет правильный отказ, когда ничего не подходит.

В контрольной ячейке Bonsai-2-27B сама пишет вызов через штатный OpenAI-совместимый обработчик. ELM собирает его из выбранных кандидатов. Машина, официальный раннер, набор и чекер одинаковые. Каждая ячейка прошла полный набор один раз.

Категория

Записей

ELM

Bonsai-2-27B

simple_python

400

53,0%

89,5%

multiple

200

56,0%

89,5%

parallel

200

36,0%

89,0%

parallel_multiple

200

7,0%

80,5%

irrelevance

240

88,3%

15,4%

Все пять категорий

1240

50,2%

73,6%

В сумме это 622 верных ответа у ELM и 913 у Bonsai. На отказах — 212 из 240 против 37 из 240. Средняя точность скрывает два разных результата: отставание при сборке вызова и преимущество при выборе «ничего не подходит».

Проверил и свой скорер на тех же ответах. Для simple_python официальный дал 53,0%, мой — 53,5%; для multiple — 56,0% и 55,5%; для irrelevance оба — 88,3%. Здесь расхождение составляет одну-две записи. Систематически более мягкой оценки мой скорер на этих трёх категориях не дал; это полезная проверка для дальнейшей отладки.

Ошибки ELM в BFCL v4: пять однотуровых Python-категорий, 1240 записей, один прогон. Доли внутри категории считаются среди её ошибок, не среди всех запросов.
Ошибки ELM в BFCL v4: пять однотуровых Python-категорий, 1240 записей, один прогон. Доли внутри категории считаются среди её ошибок, не среди всех запросов.

Из 276 ошибок simple_python и multiple только одна связана с неверным именем функции. Неверное значение или тип дают 77,2%, пропущенный обязательный аргумент — 16,7%, число вызовов — 5,8%. В параллельных категориях из 314 ошибок 58,6% относятся к числу вызовов. Следующий шаг поэтому вполне конкретный: строить значения аргументов и точнее определять, сколько вызовов требуется.

Считаю время и цену с оговорками

На запись ELM затратил 1,15 секунды, Bonsai — 2,98. Расчётная цена на 1000 записей: $0,0716 и $0,5051. Для ELM расчёт основан на 2 114 484 измеренных входных токенах судьи и ставке $0,042 за миллион. Локальная Bonsai не выставляла счёт: её оценку я получил по историческому прайсу 27B-класса, $0,80 за миллион входных и выходных токенов, записанному в метаданных 2026-09-30. Это разные способы расчёта, а не два фактических платежа; текущие тарифы из них не следуют.

Часть полей пришлось восстановить офлайн, без повторного прогона. Я сохранил их отдельно с описанием источника. Для ELM известна только нижняя оценка пика памяти: не меньше 307,9 МБ. Она получена по RSS дочерних процессов, завершения которых дождалась программа. Веса локальной Bonsai занимали 10 695 МБ. Сравнивать эти числа как полный расход памяти двух систем нельзя: одно — нижняя оценка процесса, другое — веса. Судья работает удалённо, его память и вычисления мне неизвестны.

Проверяю границы перед применением

Результаты относятся к пяти однотуровым Python-категориям. Java, JavaScript и multi_turn я здесь не запускал. Последний требует состояния, исполнения инструментов между ходами и отдельного диалогового цикла. Обязательные списки и словари мой механизм подстрок тоже пока не собирает.

Категории live не оказались сплошной границей: проверка 2251 записи показала, что 83–100% в разных категориях укладываются в ограничения механизма. Но покрытия формата недостаточно: точность на live этим прогоном не измерена. Особенно нужен live_relevance, чтобы узнать цену ошибочного отказа там, где инструмент действительно подходит. Успех на irrelevance этого не доказывает.

Один прогон на ячейку не даёт разброса по запускам. Малые различия я не интерпретирую; большие разрывы в таблице задают направление работы. Схему «сначала селектор проверяет применимость, затем генератор собирает вызов» теперь можно проверять как отдельную гипотезу. Её итоговый выигрыш и ошибки отказа я пока не измерил.

Код и метаданные: https://github.com/ADanMan/elm-research, тег paper2-v1. Репозиторий пока приватный; планирую открыть его после публикации статей. Препринт на arXiv ещё не опубликован: для подачи нужен endorsement в cs.CL. Если можете помочь с endorsement, напишите мне в личные сообщения на Хабре.