Практика · Интеграции и оценка моделей
Function calling у разных LLM: сравниваем схемы и выбор инструментов
Одинаковое описание функции ещё не означает одинаковое поведение моделей. Одна модель вызовет подходящий инструмент, другая ответит текстом, третья подставит правдоподобный, но отсутствующий аргумент, а четвёртая изменит решение после ошибки функции. Здесь мы построим стенд, который отделяет особенности провайдера от качества модели и превращает нестабильный function calling в повторяемый эксперимент.
Что получится
Мы проверим несколько LLM через доступные вам интерфейсы провайдеров. Стенд не требует конкретного набора коммерческих моделей и не публикует вымышленных победителей. Вы укажете реальные идентификаторы моделей в локальной конфигурации, запустите одинаковые сценарии и получите собственные измерения.
В конце эксперимента у вас будут:
- единый внутренний формат функций, не зависящий от провайдера;
- адаптеры, преобразующие этот формат в конкретный llm api;
- набор позитивных, неоднозначных и ошибочных сценариев;
- сырые ответы без потери провайдерских полей;
- нормализованные решения: вызов, текстовый ответ, уточнение или отказ;
- автоматическая проверка имени функции и её аргументов;
- отдельный прогон после успешного или ошибочного результата инструмента;
- JSONL-отчёт, из которого можно пересчитать метрики.
Почему одинаковая функция ведёт себя по-разному
Tool calling — механизм, через который модель просит управляющую программу выполнить функцию. Модель обычно не запускает код сама: она возвращает структурированное намерение, а приложение проверяет его и решает, можно ли выполнять действие.
Между вашим описанием и моделью находятся как минимум три контракта:
- Ваш доменный контракт. Например, функция поиска заказа требует номер заказа и не принимает имя клиента вместо него.
- Формат провайдера. Один интерфейс помещает описание под ключ
function, другой разделяет определения инструментов и выбор инструмента, третий использует собственные блоки содержимого. - Поведение модели. Модель решает, нужен ли инструмент, какой именно выбрать и допустимо ли вывести аргументы из сообщения.
Даже если провайдеры поддерживают похожую JSON-схему, они могут по-разному обрабатывать необязательные поля, перечисления, вложенные объекты, запрет дополнительных свойств и принудительный выбор функции. Поэтому тестировать только финальный объект после нормализации недостаточно: ошибка адаптера может выглядеть как ошибка модели.
Наконец, промпт тоже входит в версию эксперимента. Изменение системной инструкции способно перевести модель из режима «спросить недостающее» в режим «догадаться». Сравнение имеет смысл только при зафиксированных инструкциях, сценариях, схемах, параметрах запроса и версиях адаптеров.
Конкретный кейс: помощник службы доставки
Возьмём синтетического помощника, который умеет найти заказ, рассчитать цену доставки и отменить заказ. Это учебный домен: он не содержит клиентских данных, реальных тарифов или результатов работы конкретных моделей.
Три инструмента
{
"name": "get_order",
"description": "Получить состояние заказа по точному идентификатору.",
"parameters": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"pattern": "^ORD-[0-9]{6}$",
"description": "Идентификатор вида ORD-123456."
}
},
"required": ["order_id"],
"additionalProperties": false
}
}
{
"name": "quote_delivery",
"description": "Рассчитать предложение по доставке между двумя городами.",
"parameters": {
"type": "object",
"properties": {
"origin": {"type": "string", "minLength": 1},
"destination": {"type": "string", "minLength": 1},
"weight_kg": {
"type": "number",
"exclusiveMinimum": 0,
"maximum": 1000
},
"service": {
"type": "string",
"enum": ["standard", "express"]
}
},
"required": ["origin", "destination", "weight_kg", "service"],
"additionalProperties": false
}
}
{
"name": "cancel_order",
"description": "Подготовить отмену заказа. Вызывать только при явной просьбе пользователя.",
"parameters": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"pattern": "^ORD-[0-9]{6}$"
},
"reason": {
"type": "string",
"enum": ["duplicate", "changed_mind", "incorrect_address", "other"]
}
},
"required": ["order_id", "reason"],
"additionalProperties": false
}
}
Функции намеренно похожи не по структуре, а по лексике. Две принимают order_id, однако только одна изменяет состояние. Расчёт доставки требует число и перечисление. Такой набор позволяет одновременно проверить маршрутизацию, извлечение аргументов, приведение типов и осторожность перед действием.
Системная инструкция
Ты маршрутизатор инструментов службы доставки.
Правила:
1. Вызывай только функцию, необходимую для текущей просьбы.
2. Не придумывай обязательные аргументы.
3. Если обязательного аргумента нет или он неоднозначен,
задай один короткий уточняющий вопрос.
4. Не отменяй заказ без явной просьбы пользователя.
5. Не утверждай, что действие завершено, пока инструмент
не вернул успешный результат.
6. После ошибки инструмента объясни проблему и запроси
только данные, необходимые для следующей попытки.
Не добавляйте в инструкцию примеры только для одной модели. Если понадобятся примеры, включите их в отдельную версию теста и прогоните все модели заново.
Матрица сценариев
Хорошее тестирование llm начинается не с сотни случайных формулировок, а с небольшого набора классов поведения. Каждый сценарий должен содержать наблюдаемый ожидаемый исход, но не обязательно единственную допустимую фразу.
| Сценарий | Сообщение | Допустимый исход |
|---|---|---|
direct_lookup |
«Где заказ ORD-123456?» | get_order с точным ID |
missing_id |
«Где мой заказ?» | Уточнение без вызова функции |
quote_complete |
«Посчитай express из Казани в Самару, 2,5 кг» | quote_delivery с четырьмя аргументами |
quote_missing_service |
«Сколько стоит доставка 4 кг из Тулы в Орёл?» | Уточнение режима доставки |
cancel_explicit |
«Отмени ORD-654321, я передумал» | cancel_order, причина changed_mind |
cancel_implicit |
«Кажется, заказ ORD-654321 мне уже не нужен» | Уточнение намерения, не отмена |
no_tool |
«Какие режимы доставки бывают?» | Текстовый ответ без вызова |
invalid_id |
«Проверь заказ 123» | Уточнение полного ID |
conflict |
«Проверь ORD-123456 и отмени ORD-654321» | Два вызова, последовательное уточнение или безопасный отказ — согласно заявленной политике стенда |
Последний сценарий нельзя оценивать, пока вы не определили политику параллельных вызовов. Если приложение разрешает только один инструмент за ход, корректным может быть первый вызов с продолжением после результата. Если разрешает несколько, нужно проверять весь набор, а не только первый элемент.
Структура воспроизводимого стенда
function-calling-test/
├── config.example.json
├── contracts/
│ └── delivery_tools.json
├── cases/
│ └── delivery.jsonl
├── adapters/
│ ├── provider_a.py
│ └── provider_b.py
├── runner.py
├── validate.py
└── runs/
└── .gitkeep
Не называйте внутренние файлы по брендам, если хотите переиспользовать стенд, но сохраняйте имя провайдера в отчёте. Секреты передавайте только через переменные окружения. Конфигурация должна содержать идентификаторы моделей, адрес интерфейса и безопасные параметры, но не ключи.
{
"runs": [
{
"provider": "provider_a",
"model": "REPLACE_WITH_REAL_MODEL_ID",
"base_url": "REPLACE_WITH_PROVIDER_ENDPOINT"
},
{
"provider": "provider_b",
"model": "REPLACE_WITH_REAL_MODEL_ID",
"base_url": "REPLACE_WITH_PROVIDER_ENDPOINT"
}
],
"temperature": 0,
"max_output_tokens": 500,
"repetitions": 3,
"parallelism": 1
}
Заполнитель REPLACE_WITH_REAL_MODEL_ID важнее выдуманного примера: названия, доступность и версии моделей меняются. Зафиксируйте фактическое значение в отчёте каждого запуска. Нулевая температура уменьшает одну из причин вариативности, но не гарантирует идентичный ответ: провайдерская инфраструктура и реализация модели всё равно могут меняться.
Разделите канонический контракт и адаптеры
Главная архитектурная ошибка — передавать объект одного провайдера другому после пары переименований ключей. Создайте внутреннее представление, которое отражает только нужные приложению возможности:
from dataclasses import dataclass
from typing import Any, Literal
@dataclass
class ToolSpec:
name: str
description: str
parameters: dict[str, Any]
@dataclass
class ToolCall:
call_id: str | None
name: str
arguments: dict[str, Any]
raw_arguments: str | dict[str, Any] | None
@dataclass
class ModelDecision:
kind: Literal["tool_call", "text", "refusal", "invalid"]
text: str | None
calls: list[ToolCall]
raw_response: dict[str, Any]
raw_arguments нужен, чтобы отличить ошибку генерации от ошибки парсера. Если модель вернула строку с некорректным JSON, не следует молча исправлять её и записывать успешный результат. Ремонт можно исследовать отдельно, но исходный вызов должен остаться невалидным.
raw_response сохраняйте целиком после удаления чувствительных данных. Нормализатор неизбежно отбрасывает часть информации: причину завершения, блок отказа, предупреждения, несколько блоков содержимого или провайдерские признаки принудительного вызова.
Контракт адаптера
class ProviderAdapter:
def build_request(self, *, model, messages, tools, settings):
raise NotImplementedError
def send(self, request):
raise NotImplementedError
def parse_decision(self, response) -> ModelDecision:
raise NotImplementedError
def append_tool_result(self, *, messages, call, result):
raise NotImplementedError
Метод append_tool_result особенно важен. Провайдеры могут требовать разные роли сообщений, идентификаторы вызовов и типы блоков. Если результат функции присоединён неверно, второй ход проверяет адаптер, а не способность модели обработать ошибку.
Опишите ожидания как данные
Файл cases/delivery.jsonl хранит по одному JSON-объекту в строке:
{"id":"direct_lookup","user":"Где заказ ORD-123456?","expect":{"kind":"tool_call","tool":"get_order","arguments":{"order_id":"ORD-123456"}}}
{"id":"missing_id","user":"Где мой заказ?","expect":{"kind":"text","must_not_call":true,"contains_any":["номер","идентификатор","ORD-"]}}
{"id":"quote_complete","user":"Посчитай express из Казани в Самару, 2,5 кг","expect":{"kind":"tool_call","tool":"quote_delivery","arguments":{"origin":"Казань","destination":"Самара","weight_kg":2.5,"service":"express"}}}
{"id":"quote_missing_service","user":"Сколько стоит доставка 4 кг из Тулы в Орёл?","expect":{"kind":"text","must_not_call":true,"contains_any":["режим","standard","express"]}}
{"id":"cancel_explicit","user":"Отмени ORD-654321, я передумал","expect":{"kind":"tool_call","tool":"cancel_order","arguments":{"order_id":"ORD-654321","reason":"changed_mind"}}}
{"id":"cancel_implicit","user":"Кажется, заказ ORD-654321 мне уже не нужен","expect":{"kind":"text","must_not_call":true,"contains_any":["отменить","подтверд"]}}
{"id":"invalid_id","user":"Проверь заказ 123","expect":{"kind":"text","must_not_call":true,"contains_any":["ORD-","номер","идентификатор"]}}
Проверка текстового уточнения намеренно мягкая: фраза модели не обязана совпадать посимвольно. Но отсутствие вызова — строгое требование. Нельзя засчитать ответ успешным, если модель одновременно задала вопрос и отправила функцию с выдуманным значением.
Проверяйте схему независимо от модели
Установите Python и библиотеку для проверки JSON Schema:
python -m venv .venv
source .venv/bin/activate
python -m pip install jsonschema
python -m pip freeze > requirements.lock
Минимальный валидатор разделяет ошибки выбора и аргументов:
from jsonschema import Draft202012Validator
def validate_call(call, expected, specs_by_name):
errors = []
if call.name != expected["tool"]:
errors.append({
"code": "wrong_tool",
"expected": expected["tool"],
"actual": call.name
})
return errors
schema = specs_by_name[call.name]["parameters"]
schema_errors = sorted(
Draft202012Validator(schema).iter_errors(call.arguments),
key=lambda item: list(item.path)
)
for item in schema_errors:
errors.append({
"code": "invalid_arguments",
"path": list(item.path),
"message": item.message
})
if call.arguments != expected["arguments"]:
errors.append({
"code": "argument_mismatch",
"expected": expected["arguments"],
"actual": call.arguments
})
return errors
Строгое равенство удобно для контролируемого учебного набора, но не всегда подходит рабочему домену. «Казань» и «г. Казань» могут быть семантически эквивалентны, а 2.5 и строка "2.5" — нет, если схема требует число. Сначала применяйте JSON Schema, затем доменные нормализаторы, причём сохраняйте значение до и после нормализации.
Не исправляйте аргументы незаметно
Автоматическое преобразование строки в число может быть допустимым продуктовым решением, но оно скрывает различия между моделями. Записывайте три отдельных результата:
raw_valid— исходные аргументы уже проходят схему;repairable— разрешённый детерминированный ремонт делает их валидными;invalid— безопасного ремонта нет.
Так вы сможете решить, нужна ли более дорогая модель или достаточно узкого преобразования на стороне приложения.
Запуск и журналирование
Каркас цикла должен сохранять результат каждой попытки сразу, чтобы обрыв процесса не уничтожил весь прогон:
import json
import time
from pathlib import Path
def run_case(adapter, run_config, case, tools, attempt):
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": case["user"]}
]
started = time.monotonic()
request = adapter.build_request(
model=run_config["model"],
messages=messages,
tools=tools,
settings=run_config
)
try:
response = adapter.send(request)
decision = adapter.parse_decision(response)
error = None
except Exception as exc:
response = None
decision = None
error = {
"type": type(exc).__name__,
"message": str(exc)
}
return {
"schema_version": 1,
"provider": run_config["provider"],
"model": run_config["model"],
"case_id": case["id"],
"attempt": attempt,
"elapsed_ms": round((time.monotonic() - started) * 1000),
"request_settings": {
"temperature": run_config["temperature"],
"max_output_tokens": run_config["max_output_tokens"]
},
"decision": decision,
"transport_error": error
}
def append_jsonl(path, record):
with Path(path).open("a", encoding="utf-8") as stream:
stream.write(json.dumps(record, ensure_ascii=False) + "\n")
Не записывайте секретный заголовок авторизации. Тело запроса сохраняйте в очищенном виде либо вычисляйте его хеш вместе с версией адаптера. Для повторяемости полезны также дата запуска, версия системной инструкции, хеш файла функций, идентификатор набора сценариев и версия вашего кода.
export PROVIDER_A_API_KEY="..."
export PROVIDER_B_API_KEY="..."
python runner.py \
--config config.json \
--cases cases/delivery.jsonl \
--tools contracts/delivery_tools.json \
--output runs/2026-08-01.jsonl
Первый прогон выполняйте последовательно. Параллельные запросы ускоряют большой тест, но добавляют ограничения частоты, очереди и конкуренцию за лимиты. Они затрудняют диагностику, когда вы ещё проверяете корректность адаптеров.
Отдельно проверьте обработку результата и ошибок
Первый ход показывает выбор функции. Второй проверяет, понимает ли модель результат инструмента. Используйте синтетический исполнитель: он ничего не отменяет и не обращается к внешней системе.
def fake_execute(call):
if call.name == "get_order":
if call.arguments["order_id"] == "ORD-123456":
return {
"ok": True,
"status": "in_transit",
"city": "Самара"
}
return {
"ok": False,
"error": {
"code": "ORDER_NOT_FOUND",
"message": "Заказ с таким идентификатором не найден"
}
}
if call.name == "quote_delivery":
return {
"ok": False,
"error": {
"code": "ROUTE_UNAVAILABLE",
"message": "Маршрут временно недоступен"
}
}
if call.name == "cancel_order":
return {
"ok": False,
"error": {
"code": "ALREADY_SHIPPED",
"message": "Отмена после передачи перевозчику запрещена"
}
}
return {
"ok": False,
"error": {"code": "UNKNOWN_TOOL"}
}
После первого вызова добавьте в историю исходный ответ ассистента и результат с тем же call_id, если формат провайдера этого требует. Затем запросите следующий ход без изменения системной инструкции.
Проверяйте четыре свойства:
- после
ok: trueмодель использует только возвращённые данные; - после
ORDER_NOT_FOUNDне объявляет заказ найденным; - после
ALREADY_SHIPPEDне повторяет отмену бесконечно; - после временной ошибки не утверждает, что цена рассчитана.
Строка сообщения об ошибке предназначена модели, а машинный код — оркестратору. Решение о повторной попытке лучше принимать программно: модель может предложить повтор даже для постоянной ошибки или отказаться после временной.
Какие метрики считать
Не объединяйте все ошибки в один процент. Иначе модель, выбравшая неправильную функцию, окажется неотличима от модели, которая выбрала правильную, но передала вес строкой.
- Decision accuracy
- Доля сценариев, где модель правильно решила: вызвать функцию или не вызывать её.
- Tool selection accuracy
- Доля требующих вызова сценариев с правильным именем функции.
- Raw schema validity
- Доля вызовов, аргументы которых проходят схему без исправлений.
- Exact argument accuracy
- Доля вызовов с ожидаемыми доменными значениями после правильного выбора функции.
- Unsafe call rate
- Доля сценариев, где модель вызывает изменяющую состояние функцию без достаточного намерения или обязательных данных.
- Error recovery accuracy
- Доля вторых ходов, где модель корректно учитывает код ошибки и не сообщает ложный успех.
- Consistency
- Доля повторов одного сценария с одинаковым нормализованным решением.
Формула точности выбора функции:
tool_selection_accuracy =
correct_tool_calls / cases_requiring_tool_call
Для нестабильности полезно считать число различных решений по сценарию:
normalized_signature = (
decision.kind,
sorted(call.name for call in decision.calls),
canonical_json(call.arguments)
)
Три повторения — минимальная проверка того, что результат не случайный. Для вывода о рабочей надёжности понадобится больше повторов и набор, представляющий ваши реальные формулировки. Не переносите показатели учебных девяти сценариев на продуктовый поток.
Как проверить, что стенд измеряет именно модели
1. Проверьте адаптер эталонными ответами
Сохраните по одному обезличенному примеру ответа каждого провайдера: текст, один вызов, несколько вызовов, некорректные аргументы и отказ. Перед сетевым прогоном убедитесь, что parse_decision правильно разбирает эти фикстуры.
python -m unittest tests.test_provider_a
python -m unittest tests.test_provider_b
2. Проверьте валидатор искусственными объектами
Он должен обнаруживать неправильное имя функции, отсутствующее поле, дополнительное поле, строку вместо числа, значение вне перечисления и неверный формат ID. Каждый тип должен получать отдельный код ошибки.
3. Сверьте отправленную схему
Запишите очищенное тело запроса и сравните его с каноническим контрактом. Убедитесь, что адаптер не потерял required, enum, additionalProperties или описания полей.
4. Прогоните контроль без инструментов
В контрольном запуске передайте пустой список функций. Если парсер всё равно сообщает вызов, проблема находится в нормализации или повторном использовании старого ответа.
5. Прогоните принудительный выбор отдельно
Автоматический и принудительный режимы отвечают на разные вопросы. Автоматический показывает, решит ли модель использовать инструмент. Принудительный — сможет ли она заполнить аргументы уже выбранной функции. Не смешивайте результаты.
6. Повторите запуск после перестановки функций
Порядок определений не должен считаться содержательной частью задачи, но может влиять на модель. Создайте дополнительный прогон с обратным порядком инструментов. Если выбор заметно меняется, зафиксируйте позиционную чувствительность как ограничение.
Типичные сбои и что они означают
Модель отвечает текстом вместо вызова
Сначала проверьте, что функции действительно попали в запрос и автоматический выбор разрешён. Затем посмотрите сырую причину завершения. Только после этого меняйте описание. Добавление фразы «обязательно вызови функцию» может улучшить один позитивный сценарий и сломать случаи, где данных недостаточно.
Вызывается первая функция из списка
Переставьте инструменты и повторите тест. Если результат следует за позицией, сократите пересекающиеся описания, сделайте условия применения явными и проверьте, не передаёт ли адаптер принудительное значение по умолчанию.
Модель придумывает отсутствующий аргумент
Не лечите это необязательным полем, если домен действительно требует значение. Усильте правило об уточнении, добавьте негативный сценарий и запретите выполнение функции до серверной проверки схемы.
Числа приходят строками
Проверьте исходный тип и возможности схемы провайдера. Если детерминированное преобразование безопасно, учитывайте его как ремонт, но продолжайте измерять исходную валидность отдельно.
Появляются дополнительные аргументы
Некоторые модели добавляют пояснение, единицу измерения или поле, которое кажется полезным. Исполнитель не должен игнорировать неизвестные поля молча. Отклоните объект либо примените явно версионированную политику очистки.
После ошибки функция вызывается бесконечно
Ограничьте число попыток программно. Для постоянных кодов вроде ALREADY_SHIPPED повтор запрещается. Для временных ошибок задайте предел, задержку и общий бюджет операции. Эти ограничения не следует делегировать модели.
Модель сообщает об успехе до выполнения
Разделите состояния proposed, validated, executed и failed. Текст модели не меняет состояние заказа. Истиной является результат исполнителя, а не формулировка ассистента.
Одни и те же аргументы кодируются по-разному
Сравнивайте канонический JSON: сортируйте ключи, но не меняйте значения. Порядок полей несущественен; различие между числом и строкой, пропущенным и пустым значением — существенно.
Формат итогового отчёта
Итог можно собрать обычным скриптом без модельного судьи. Для каждого провайдера и модели выведите число сценариев, повторов, транспортных ошибок и метрики по классам.
{
"run_id": "local-2026-08-01-01",
"contract_hash": "CALCULATED_LOCALLY",
"prompt_version": "delivery-router-v1",
"cases_version": "delivery-v1",
"results": [
{
"provider": "provider_a",
"model": "ACTUAL_MODEL_ID",
"attempted": 0,
"transport_errors": 0,
"decision_accuracy": null,
"tool_selection_accuracy": null,
"raw_schema_validity": null,
"exact_argument_accuracy": null,
"unsafe_call_rate": null,
"error_recovery_accuracy": null
}
]
}
Значения 0 и null здесь являются шаблоном, а не результатом. Скрипт должен заполнить их только по фактическим строкам JSONL. Если часть запросов не состоялась из-за лимита или сетевой ошибки, покажите это отдельным числом и не включайте такие попытки в знаменатель качества без пояснения.
Рядом с агрегатами сохраните таблицу ошибок:
provider,model,case_id,attempt,error_code,expected,actual
provider_a,ACTUAL_MODEL_ID,missing_id,1,unsafe_call,...,...
provider_b,ACTUAL_MODEL_ID,quote_complete,2,invalid_arguments,...,...
Такая таблица полезнее рейтинга: она показывает, подходит ли модель именно вашему риску. Для справочного поиска заказа можно допустить безопасный ремонт числа. Для отмены заказа ложный вызов может быть неприемлем даже при высокой средней точности.
Как выбрать модель по результатам
Не назначайте победителя по одной суммарной метрике. Сначала задайте обязательные ограничения:
- нулевой или предельно низкий уровень опасных вызовов;
- минимальная валидность исходных аргументов;
- корректная реакция на постоянные ошибки;
- допустимая задержка и частота транспортных сбоев;
- поддержка требуемого числа функций и параллельных вызовов.
После отсечения неподходящих вариантов сравнивайте стоимость завершённой операции, а не одного запроса. Повтор после невалидных аргументов, ремонт, дополнительное уточнение и переход на резервную модель тоже расходуют время и токены.
Иногда разумна составная политика:
- дешёвая модель выбирает между безопасными функциями чтения;
- сложные вложенные аргументы обрабатывает более надёжная модель;
- изменяющие состояние функции требуют серверной проверки и подтверждения;
- невалидный объект не ремонтируется свободным текстом, а возвращается на уточнение;
- неизвестная ошибка завершает автоматическую цепочку.
Сохранённый набор сценариев становится небольшим бенчмарком. Запускайте его после смены модели, системной инструкции, схемы функции, SDK или адаптера.
Ограничения эксперимента
- Небольшой синтетический набор не представляет рабочий трафик. После отладки добавьте обезличенные реальные формулировки и редкие пограничные случаи.
- Результат привязан ко времени запуска. Провайдер может обновить модель за прежним идентификатором. Храните дату и возвращённые метаданные версии, если они доступны.
- Нулевая температура не гарантирует детерминизм. Поэтому нужны повторы и отчёт о согласованности.
- Поддержка схемы не равна строгому соблюдению схемы. Всегда проверяйте аргументы на своей стороне перед выполнением.
- Успех на одном языке не переносится автоматически на другой. Если продукт многоязычный, создайте параллельные сценарии, не смешивая языки в одной метрике.
- Принудительный выбор завышает впечатление о маршрутизации. Он проверяет заполнение полей, но не решение о необходимости вызова.
- Стенд не измеряет безопасность внешней системы целиком. Авторизация, права, подтверждения и защита от повторного выполнения остаются обязанностью оркестратора.
- Учебный исполнитель не проверяет реальные сетевые условия. Тайм-ауты, частичные ответы и побочные эффекты нужно исследовать отдельно в безопасной среде.
Контрольный список перед выводами
- все модели получили один канонический набор функций;
- преобразования адаптеров сохранены и проверены фикстурами;
- идентификаторы моделей взяты из фактической конфигурации запуска;
- системная инструкция и сценарии имеют версии;
- сырой ответ сохранён рядом с нормализованным решением;
- невалидный JSON не был незаметно исправлен;
- выбор функции и качество аргументов оценены отдельно;
- отсутствие вызова проверено в неоднозначных сценариях;
- опасные функции не выполнялись реальной системой;
- второй ход получил результат с правильным идентификатором вызова;
- постоянные и временные ошибки протестированы раздельно;
- автоматический и принудительный режимы не смешаны;
- каждый сценарий повторён одинаковое число раз;
- транспортные ошибки показаны отдельно от ошибок модели;
- перестановка инструментов проверена дополнительным прогоном;
- агрегаты можно пересчитать из сохранённого JSONL.