Как настроить контрактные тесты для tool calling в LLM: пошаговый чеклист для разработчиков

Контрактные тесты для tool calling — это барьер между некорректным вызовом инструмента и продакшеном. Разбираем, какие проверки включать, как фиксировать сигнатуры и что делать с регрессиями на примере реального CI/CD контура.

Схема контрактного тестирования для tool calling в LLM: проверка сигнатур, аргументов и ответов инструментов перед релизом
Схема контрактного тестирования для tool calling в LLM: проверка сигнатур, аргументов и ответов инструментов перед релизом
Lavender and Pink Crayons | by Pink Poppy Photography | openverse | by

Любая LLM-интеграция, которая вызывает внешние функции, рано или поздно сталкивается с одной и той же проблемой: модель начинает передавать аргументы, не соответствующие ожидаемой схеме. Поле `temperature` оказывается строкой, идентификатор пользователя содержит лишние пробелы, а обязательный параметр `email` вдруг отсутствует. В продакшене такие вызовы либо падают с ошибкой, либо, что хуже, молча обрабатываются с некорректными данными.

Контрактные тесты для tool calling решают эту проблему на уровне CI/CD. Вместо того чтобы полагаться на ручное тестирование или логи после релиза, вы фиксируете ожидаемую схему каждого инструмента и проверяете, что модель вызывает его строго по контракту. В этой статье — пошаговый чеклист для разработчиков, которые хотят внедрить такую проверку в свой пайплайн.

Что такое контрактный тест для tool calling и зачем он нужен

Контрактный тест — это автоматическая проверка того, что две стороны (в данном случае LLM и ваш инструмент) договорились об интерфейсе взаимодействия. В контексте tool calling это означает:

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

Без контрактных тестов регрессии возникают незаметно. Вы обновляете модель с GPT-4o на GPT-4.5, и она начинает передавать `user_id` как integer вместо string. Или Anthropic выпускает новую версию Claude, которая интерпретирует описание параметра иначе, чем предыдущая. Контрактный тест ловит такие расхождения до того, как они попадут к пользователю.

Практика контрактного тестирования хорошо известна в микросервисной архитектуре (библиотека Pact, подход Мартина Фаулера), но для LLM-инструментов она адаптируется с учётом вероятностной природы модели. Модель может вызвать правильную функцию с неправильными аргументами, или не вызвать её вовсе — контракт должен покрывать и такие сценарии.

Шаг 1: Фиксация схемы инструмента в машинно-читаемом формате

Первый шаг — описать каждый инструмент в формате, который можно автоматически сравнивать. JSON Schema — естественный выбор, потому что большинство LLM-провайдеров (OpenAI, Anthropic, Google) используют её вариации для описания tool calling.

Пример контракта для функции поиска пользователя:

json
{
«name»: «search_user»,
«description»: «Поиск пользователя по email или ID»,
«parameters»: {
«type»: «object»,
«properties»: {
«query»: {
«type»: «string»,
«description»: «Email или ID пользователя»
},
«limit»: {
«type»: «integer»,
«description»: «Максимальное количество результатов»,
«default»: 10,
«minimum»: 1,
«maximum»: 100
}
},
«required»: [«query»]
}
}

Этот контракт хранится в репозитории как отдельный JSON-файл или часть кода инструмента. Главное — он должен быть единственным источником правды. Любое изменение схемы начинается с изменения контракта, а не наоборот.

На практике удобно хранить контракты в директории `contracts/tools/` и генерировать из них код валидации на стороне бэкенда. Так вы гарантируете, что проверка входящих данных совпадает с тем, что ожидает модель.

Шаг 2: Проверка, что модель вызывает инструмент с корректными аргументами

Самый частый сценарий регрессии — модель передаёт аргументы, которые не проходят валидацию по схеме. Контрактный тест должен:

Отправить модели промпт, который триггерит вызов конкретного инструмента.

Перехватить сгенерированный вызов функции.
3. Проверить имя функции и аргументы против сохранённого контракта.

Пример на Python с использованием библиотеки `jsonschema`:

python
import jsonschema
from openai import OpenAI

client = OpenAI()

contract = load_contract(«search_user»)

response = client.chat.completions.create(
model=»gpt-4o»,
messages=[{«role»: «user», «content»: «Найди пользователя с email [email protected]»}],
tools=[contract_to_openai_tool(contract)]
)

tool_call = response.choices[0].message.tool_calls[0]
assert tool_call.function.name == contract[«name»]

arguments = json.loads(tool_call.function.arguments)
jsonschema.validate(instance=arguments, schema=contract[«parameters»])

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

Важный нюанс: модель может не вызвать инструмент вообще. Контрактный тест должен проверять, что нужный tool call присутствует в ответе. Если модель возвращает текстовый ответ вместо вызова функции — это тоже регрессия, которую нужно ловить.

Шаг 3: Тестирование граничных случаев и обработки ошибок

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

Включите в контрактные тесты следующие сценарии:

  • Отсутствие обязательного параметра: модель не передаёт `query` в `search_user`.
  • Неверный тип данных: модель передаёт `limit` как строку «десять».
  • Выход за допустимые пределы: `limit` = 200 при максимуме 100.
  • Неизвестное имя функции: модель вызывает `search_users` (с ошибкой в названии).

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

Пример теста для неверного типа:

python
# Симулируем вызов с некорректными аргументами
invalid_arguments = {«query»: «[email protected]», «limit»: «десять»}
try:
result = search_user(invalid_arguments)
except TypeError as e:
# Проверяем, что ошибка читаема для модели
assert «limit» in str(e)
assert «integer» in str(e)

Шаг 4: Интеграция контрактных тестов в CI/CD пайплайн

Контрактные тесты должны запускаться автоматически при каждом изменении, которое может повлиять на tool calling:

  • Обновление модели (смена провайдера или версии).
  • Изменение схемы инструмента (добавление/удаление параметров).
  • Изменение промпта, который управляет выбором инструмента.

В CI/CD конвейере разместите контрактные тесты как отдельный этап после unit-тестов, но до интеграционных тестов. Если контракт нарушен — пайплайн останавливается, и разработчик получает отчёт о том, какой именно инструмент и какое поле не прошло проверку.

Пример конфигурации для GitHub Actions:

yaml
name: Contract Tests

on: [pull_request]

jobs:
contract-tests:
runs-on: ubuntu-latest
steps:
— uses: actions/checkout@v4
— name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ‘3.12’
— name: Install dependencies
run: pip install openai jsonschema pytest
— name: Run contract tests
run: pytest tests/contracts/
— name: Generate contract diff
run: python scripts/contract_diff.py —base main —head HEAD

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

Шаг 5: Версионирование контрактов и управление изменениями

Контракты на tool calling — такой же артефакт, как API-спецификация. Их нужно версионировать и документировать изменения. Когда вы добавляете новый параметр в инструмент, контракт меняется, и старые версии модели могут его не поддерживать.

Рекомендуется:

  • Хранить контракты в Git вместе с кодом инструментов.
  • Использовать семантическое версионирование для контрактов (major.minor.patch).
  • При изменении контракта запускать тесты на всех поддерживаемых моделях.
  • Вести changelog с описанием каждого изменения и его влияния на совместимость.

Если вы поддерживаете несколько моделей одновременно (GPT-4o, Claude Sonnet, Gemini), имеет смысл хранить отдельные контракты для каждой, потому что разные провайдеры по-разному интерпретируют описания параметров.

Практические ограничения контрактного тестирования для LLM

Контрактные тесты не решают всех проблем. Они проверяют формат вызова, но не семантику. Модель может вызвать правильную функцию с правильными аргументами, но с неправильным намерением — например, передать email одного пользователя вместо другого. Это уже задача для более сложных интеграционных тестов и мониторинга в продакшене.

Кроме того, контрактные тесты чувствительны к изменениям в промптах. Если вы измените формулировку инструкции для модели, она может начать вызывать инструмент иначе, и тест упадёт. Это не обязательно плохо: падение теста — сигнал пересмотреть, действительно ли новое поведение корректно.

Ещё одно ограничение — стоимость. Каждый контрактный тест делает реальный вызов к API LLM, что увеличивает расходы на CI/CD. Для больших проектов имеет смысл кэшировать ответы модели для одинаковых промптов или использовать локальные модели для базовой валидации.

Что дальше: от контрактов к полному релизному шлюзу

Контрактные тесты — это первый слой защиты. На их основе можно построить полноценный релизный шлюз для LLM-инструментов, который включает:

  • Проверку безопасности (нет ли вызова опасных функций с неожиданными аргументами).
  • Метрики точности (сколько процентов вызовов корректны).
  • A/B-тестирование новой модели на исторических данных.

Начните с малого: зафиксируйте контракты для трёх-пяти ключевых инструментов, напишите тесты и добавьте их в CI. Через неделю вы увидите, сколько регрессий они ловят — и, скорее всего, захотите покрыть контрактами все инструменты в проекте.

Перед внедрением проверьте актуальные примеры из документации Anthropic и LangGraph — там есть готовые рецепты контрактного тестирования для tool use. Они сэкономят вам время на написание базовой инфраструктуры.

Источники