
LLM-агенты работают через tool calling — модель получает описание доступных функций и возвращает JSON с именем инструмента и аргументами. На практике этот механизм даёт сбои даже у ведущих моделей: пропуск обязательного вызова, неверный тип аргумента, вызов несуществующего инструмента или дублирование одного и того же действия.
Разработчики часто замечают такие проблемы только в продакшене, когда агент уже начал списывать деньги с карты пользователя или отправлять дублирующиеся письма. Этот материал — практический чеклист для проверки стабильности tool calling до развёртывания. Все примеры — на Python, с готовыми тестами, которые можно добавить в CI.
Почему tool calling ломается: три типовых сценария
Tool calling — не генерация текста, а структурированный вывод. Модель обучают предсказывать токены, и JSON — лишь один из возможных форматов. Отсюда три категории ошибок, которые встречаются в бенчмарках (например, в работе Berkeley Function Calling Leaderboard и исследованиях вроде «ToolLLM» arxiv:2307.08702):
Пропуск вызова — модель решает, что инструмент не нужен, хотя по сценарию он обязателен. Например, агент поддержки должен вызывать `get_user_info` перед ответом, но пропускает шаг.
2. Неверные аргументы — модель передаёт строку вместо числа, пустой объект вместо обязательного поля или значение вне допустимого диапазона.
3. Нарушение порядка — модель вызывает инструменты в неправильной последовательности или дублирует вызовы.
Систематическое тестирование на этих трёх осях закрывает большинство проблем. Дальше — конкретные тесты с кодом.
Тест 1: проверка на пропуск обязательного вызова
Самый простой и важный тест: модель должна вызвать инструмент, когда это явно требуется промптом или контекстом.
Сценарий: пользователь просит перевести деньги. Агент обязан вызвать `transfer_money(amount: float, recipient: str)` перед тем, как ответить.
Пример теста на Python
python
import json
from openai import OpenAI
client = OpenAI()
tools = [{
«type»: «function»,
«function»: {
«name»: «transfer_money»,
«description»: «Перевести деньги получателю»,
«parameters»: {
«type»: «object»,
«properties»: {
«amount»: {«type»: «number»},
«recipient»: {«type»: «string»}
},
«required»: [«amount», «recipient»]
}
}
}]
messages = [
{«role»: «user», «content»: «Переведи 500 рублей Ивану»}
]
response = client.chat.completions.create(
model=»gpt-4o»,
messages=messages,
tools=tools,
tool_choice=»required» # принудительно требует вызова
)
has_call = bool(response.choices[0].message.tool_calls)
assert has_call, «Модель не вызвала обязательный инструмент»
Важный нюанс: параметр `tool_choice` не всегда доступен (например, в некоторых open-source моделях). В этом случае проверяйте наличие вызова после обычного запроса. Если модель в 10% случаев не вызывает инструмент — это проблема, которую нужно документировать и решать через few-shot примеры или дообучение.
Тест 2: валидация типов и структуры аргументов
Даже если модель вызывает нужный инструмент, аргументы могут быть некорректными. Самый частый случай — строка вместо числа или отсутствие обязательного поля.
Пример теста с JSON Schema валидацией
python
from jsonschema import validate, ValidationError
tool_schema = {
«type»: «object»,
«properties»: {
«amount»: {«type»: «number», «minimum»: 0.01},
«recipient»: {«type»: «string», «minLength»: 1}
},
«required»: [«amount», «recipient»]
}
def validate_tool_call(tool_call):
try:
args = json.loads(tool_call.function.arguments)
validate(instance=args, schema=tool_schema)
return True
except (json.JSONDecodeError, ValidationError) as e:
print(f»Ошибка валидации: {e}»)
return False
Проверка на серии из 100 запросов
failures = 0
for _ in range(100):
response = client.chat.completions.create(
model=»gpt-4o»,
messages=messages,
tools=tools,
tool_choice=»required»
)
for tool_call in response.choices[0].message.tool_calls:
if not validate_tool_call(tool_call):
failures += 1
print(f»Невалидных вызовов: {failures}/100″)
Дополнительно стоит проверять граничные значения: отрицательные числа, пустые строки, слишком длинные строки. Некоторые модели склонны передавать `»amount»: 0` или `»recipient»: «»`, что может привести к ошибкам на стороне бэкенда.
Тест 3: проверка на дублирование вызовов и порядок
Агенты, которые вызывают несколько инструментов последовательно (multi-step), могут страдать от дублирования: модель вызывает один и тот же инструмент дважды с одинаковыми аргументами или нарушает ожидаемый порядок (например, вызывает `send_email` до `create_draft`).
Пример теста на порядок вызовов
python
EXPECTED_ORDER = [«create_draft», «send_email»]
def check_call_order(tool_calls):
actual_order = [tc.function.name for tc in tool_calls]
# Проверяем, что последовательность подпоследовательность ожидаемой
idx = 0
for call in actual_order:
if idx < len(EXPECTED_ORDER) and call == EXPECTED_ORDER[idx]:
idx += 1
return idx == len(EXPECTED_ORDER)
Проверка на дубликаты
def check_duplicates(tool_calls):
seen = set()
for tc in tool_calls:
key = (tc.function.name, tc.function.arguments)
if key in seen:
return False # дубликат
seen.add(key)
return True
В реальных сценариях дублирование особенно опасно для идемпотентных операций: повторный вызов `charge_payment` может привести к двойному списанию.
Тест 4: стресс-тест на большое количество инструментов
Модели хуже справляются с выбором, когда в описании больше 10–15 инструментов. Полезно провести стресс-тест: добавить 20–30 фиктивных инструментов и проверить, что модель всё ещё вызывает нужный.
Методика
Создайте список из 30 инструментов с разными названиями и описаниями.
2. В одном из запросов явно укажите, какой инструмент нужно использовать.
3. Запустите 50 запросов и замерьте долю правильных вызовов.
Исследование «ToolLLM» arxiv:2307.08702 показывает, что точность падает на 15–20% при увеличении набора инструментов с 5 до 20. Это важно учитывать при проектировании агентов: не складывайте все инструменты в один промпт, группируйте их по сценариям.
Интеграция тестов в CI
Все перечисленные тесты можно упаковать в один скрипт, который запускается при каждом пуше. Пример конфигурации для GitHub Actions:
yaml
name: Tool Calling Tests
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
— uses: actions/checkout@v4
— name: Run tool calling tests
run: python tests/test_tool_calling.py
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
Важно: тесты будут тратить токены. Чтобы не разориться, ограничьте количество запросов (например, 10–20 на прогон) и используйте кэширование ответов для повторяющихся сценариев.
Ограничения подхода и что делать, если тесты падают
Ни один набор тестов не гарантирует стабильность на 100%. Tool calling зависит от версии модели, температуры (при температуре > 0 модель может «креативить» с аргументами) и даже от формулировки описания инструмента.
Что делать при падающих тестах
Добавьте few-shot примеры — покажите модели 2–3 корректных вызова в системном сообщении.
2. Уточните описание параметров — вместо `»amount»: {«type»: «number»}` напишите `»amount»: {«type»: «number», «description»: «Сумма перевода в рублях, минимум 0.01»}`.
3. Используйте строгую валидацию на бэкенде — не доверяйте аргументам от LLM, проверяйте их перед выполнением.
4. Снизьте температуру до 0 для критических вызовов.
Если тесты продолжают падать на конкретной модели — документируйте это как известное ограничение и, возможно, меняйте модель.
Что проверить перед запуском агента в продакшен
Перед развёртыванием прогоните минимальный набор:
- Пропуск вызова — модель вызывает инструмент в 100% обязательных сценариев.
- Валидность аргументов — JSON проходит схему, типы корректны, граничные значения обработаны.
- Отсутствие дубликатов — модель не вызывает один инструмент дважды с одинаковыми аргументами.
- Порядок вызовов — multi-step сценарии выполняются в правильной последовательности.
- Стресс-тест — модель не теряется при 20+ инструментах.
Эти пять проверок не заменяют полноценного QA, но отсекают самые дорогие ошибки. Если вы используете open-source модели — добавляйте тесты на специфичные для них баги (например, Llama 3 иногда возвращает невалидный JSON в tool calling, что требует повторной генерации).