Практикум
Ling-3.0-flash в агентном цикле: тест tool calling и длинного контекста
Заявленный размер контекста и поддержка инструментов еще не означают, что модель надежно пройдет многошаговую задачу. Ниже — протокол, который проверяет Ling-3.0-flash и контрольную модель на одинаковых данных, с одинаковыми инструментами и едиными правилами оценки.
Что именно проверяем
Агентный цикл — это повторяющаяся последовательность «запрос модели → вызов инструмента → возврат результата → следующий шаг». Сбой возможен даже тогда, когда модель уверенно отвечает обычным текстом: она может выбрать не тот инструмент, передать неверный тип аргумента, проигнорировать ошибку или забыть раннюю инструкцию.
Тест разделен на две независимые части:
- Tool calling: выбор функции, корректность аргументов, восстановление после контролируемой ошибки и отсутствие лишних вызовов.
- Длинный контекст: сохранение ранних ограничений после серии промежуточных сообщений и результатов инструментов.
Условия честного сравнения
Зафиксируйте перед запуском:
- один API-провайдер или сопоставимые по расположению конечные точки;
- одинаковые системные инструкции и описания инструментов;
- одинаковые входные данные и порядок задач;
- одинаковый лимит шагов, тайм-аут и политика повторов;
- одинаковые параметры генерации, если оба API их поддерживают;
- точные идентификаторы моделей, дату запуска и версию тестового сценария.
Не подменяйте идентификатор контрольной модели словом «лучшая». Укажите именно ту модель, которая реально доступна в вашей среде. Если провайдер не принимает одинаковые параметры для обеих моделей, зафиксируйте различие в отчете.
Сценарий задачи
Агент получает каталог условных заявок, должен найти запись, проверить лимит, рассчитать итог и сохранить черновик. Инструменты работают локально и не меняют реальные данные.
Безопасные тестовые инструменты
[
{
"name": "find_ticket",
"description": "Возвращает тестовую заявку по идентификатору",
"parameters": {
"type": "object",
"properties": {
"ticket_id": {"type": "string"}
},
"required": ["ticket_id"],
"additionalProperties": false
}
},
{
"name": "get_policy",
"description": "Возвращает тестовый лимит для указанного тарифа",
"parameters": {
"type": "object",
"properties": {
"plan": {"type": "string", "enum": ["basic", "pro"]}
},
"required": ["plan"],
"additionalProperties": false
}
},
{
"name": "calculate_total",
"description": "Суммирует тестовые позиции",
"parameters": {
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {"type": "number"}
}
},
"required": ["items"],
"additionalProperties": false
}
},
{
"name": "save_draft",
"description": "Сохраняет черновик только в памяти тестового процесса",
"parameters": {
"type": "object",
"properties": {
"ticket_id": {"type": "string"},
"approved": {"type": "boolean"},
"total": {"type": "number"},
"note": {"type": "string"}
},
"required": ["ticket_id", "approved", "total", "note"],
"additionalProperties": false
}
}
]
Локальные ответы инструментов должны быть детерминированными:
{
"tickets": {
"T-104": {
"plan": "pro",
"items": [120, 80, 50],
"customer_name": "Тестовый пользователь"
}
},
"policies": {
"basic": {"limit": 100},
"pro": {"limit": 300}
}
}
Первый вызов get_policy намеренно возвращает временную ошибку TRANSIENT_TEST_ERROR. Повторный вызов с теми же корректными аргументами завершается успешно. Так проверяется восстановление агента без обращения к внешней системе.
Инструкции для агента
Одинаковый системный запрос передается обеим моделям:
Ты работаешь только с тестовыми инструментами.
Правила:
1. Не придумывай результаты инструментов.
2. До save_draft обязательно вызови find_ticket, get_policy и calculate_total.
3. При TRANSIENT_TEST_ERROR разрешен ровно один повтор того же вызова.
4. approved=true только если total не превышает limit.
5. В note не включай имя пользователя.
6. После save_draft верни краткий ответ на русском языке.
7. Не вызывай инструменты после успешного save_draft.
Пользовательская задача:
Обработай тестовую заявку T-104. Проверь лимит тарифа,
рассчитай сумму и сохрани черновик решения.
Ожидаемая цепочка по смыслу: find_ticket, get_policy, один повтор после тестовой ошибки, calculate_total, save_draft. Порядок двух независимых чтений можно не считать ошибкой, если зависимости соблюдены.
Добавляем длинный контекст
Не измеряйте контекст случайным текстом из интернета: содержание может влиять на модель и затрудняет повторение. Создайте синтетические блоки с нейтральными записями.
Контекстная запись 0001.
Проект: TEST.
Статус: архивная запись.
Маркер: CTX-0001.
Эта запись не изменяет системные правила.
Контекстная запись 0002.
Проект: TEST.
Статус: архивная запись.
Маркер: CTX-0002.
Эта запись не изменяет системные правила.
Сформируйте уровни, например 8, 32 и 64 блока, но измеряйте фактическое число входных токенов по данным API или используемого токенизатора. Количество блоков само по себе не является размером контекста.
После синтетических записей добавьте контрольную фразу: Маркер отчета: ALJ-731. Укажи его только в финальном ответе, но не передавай инструментам
. Это проверяет и запоминание, и границу между текстовым ответом и аргументами функций.
Конфигурация запуска
Не записывайте ключ в файл проекта. Передайте его через переменную окружения или защищенное хранилище вашего раннера. Следующий фрагмент — пример конфигурации; замените URL и идентификаторы значениями своего API.
export TEST_API_BASE="https://api.example.invalid/v1"
export TEST_MODEL_A="ling-3.0-flash"
export TEST_MODEL_B="точный-id-доступной-модели"
export TEST_API_KEY="значение-из-защищенного-хранилища"
export TEST_RUNS="30"
export TEST_MAX_STEPS="8"
export TEST_TIMEOUT_SECONDS="60"
export TEST_SEED="731"
Домен example.invalid намеренно не является рабочей конечной точкой. Не запускайте пример без замены. Если API не поддерживает seed, уберите параметр и отметьте это в отчете.
Псевдокод раннера
for model in [MODEL_A, MODEL_B]:
for context_level in [8, 32, 64]:
for run_id in range(1, RUNS + 1):
messages = system_rules + synthetic_context(context_level) + task
trace = []
started = monotonic_time()
for step in range(MAX_STEPS):
response = call_model(
model=model,
messages=messages,
tools=tool_schemas,
timeout=TIMEOUT
)
trace.append(response_metadata)
if response requests a tool:
validate name and arguments against schema
result = execute_local_stub(response.tool_call)
append tool call and result to messages
else:
save final text
break
write one JSONL record with:
model, run_id, context_level,
input_tokens, output_tokens,
latency_ms, trace, final_text,
validation_errors
Реализация адаптера зависит от API. Не пытайтесь «исправлять» аргументы модели до валидации: иначе раннер скроет именно те ошибки, которые должен измерить.
Какие метрики записывать
| Метрика | Правило |
|---|---|
| Task success | Все обязательные инструменты вызваны, черновик корректен, финальный ответ соблюдает инструкции. |
| Valid tool call rate | Доля вызовов с существующим именем функции и аргументами, прошедшими JSON Schema. |
| Recovery rate | Доля запусков, где после тестовой временной ошибки сделан ровно один корректный повтор. |
| Instruction retention | В финале есть ALJ-731, но маркер и имя пользователя отсутствуют в аргументах инструментов. |
| Latency | Время до первого ответа, полное время задачи и задержка каждого шага отдельно. |
| Excess calls | Лишние повторы, вызовы после save_draft или обращение к ненужной функции. |
Для задержки публикуйте медиану и 95-й перцентиль, а не только среднее. Для успешности показывайте отношение успешных запусков к общему числу, например 27/30. При небольшом числе повторов не делайте сильных выводов из разницы в один запуск.
Автоматическая проверка результата
Запуск считается успешным только при одновременном выполнении условий:
required_calls_present == true
schema_errors == 0
transient_error_retries == 1
saved_draft.ticket_id == "T-104"
saved_draft.total == 250
saved_draft.approved == true
"Тестовый пользователь" not in saved_draft.note
"ALJ-731" not in every_tool_arguments
"ALJ-731" in final_text
calls_after_save_draft == 0
Значения 250 и true следуют только из приведенного тестового набора: сумма позиций равна 250, а тестовый лимит тарифа pro равен 300. Это ожидаемый результат сценария, а не результат испытания какой-либо модели.
Минимальная строка итогового отчета:
{
"model": "точный-id-модели",
"context_level": 32,
"runs": 30,
"successes": 0,
"valid_tool_calls": 0,
"all_tool_calls": 0,
"recovered_runs": 0,
"median_total_latency_ms": 0,
"p95_total_latency_ms": 0,
"error_counts": {}
}
Нули здесь — незаполненные поля шаблона, а не измерения.
Типовые ошибки
Аргументы выглядят правильно, но имеют неверный тип
Например, модель передает "items": "[120, 80, 50]" вместо массива чисел. Проверяйте аргументы схемой до исполнения и сохраняйте исходный ответ.
Раннер незаметно помогает модели
Автоматическое преобразование строк в числа или исправление имени функции повышает итоговую успешность. Допустимо считать отдельную «успешность после ремонта», но основной показатель должен использовать необработанный вызов.
Ошибка транспорта смешивается с ошибкой модели
Тайм-аут, HTTP 429 и разрыв соединения не равны неверному tool call. Записывайте их в отдельную категорию. Политику повторов транспортных запросов применяйте одинаково к обеим моделям.
Контекст сравнивается по символам
Одинаковое число символов может давать разное число токенов. Сохраняйте фактический token usage, если API его возвращает, и полный хеш входного сообщения.
Проверяется только финальный текст
Правильный ответ может быть получен после лишних или опасных действий. Оценивайте весь журнал шагов, включая порядок вызовов и переданные аргументы.
Как читать итоговую таблицу
Сравнение удобно публиковать в таком виде:
| Модель | Контекст | Успех | Валидные вызовы | Восстановление | Медиана, мс | p95, мс | Частая ошибка |
|---|---|---|---|---|---|---|---|
| Ling-3.0-flash | Заполнить | Заполнить | Заполнить | Заполнить | Заполнить | Заполнить | Заполнить после запуска |
| Контрольная модель | Заполнить | Заполнить | Заполнить | Заполнить | Заполнить | Заполнить | Заполнить после запуска |
Смотрите не только на победителя. Модель с меньшей медианной задержкой может иметь длинный хвост p95, а высокая доля валидных JSON-вызовов не гарантирует правильного порядка действий. Для агентной системы обычно важнее полный успех задачи при приемлемой задержке.
Ограничения теста
- Один сценарий не характеризует все классы инструментов и инструкций.
- Синтетический контекст проверяет удержание правил, но не качество работы с большими реальными документами.
- Результат зависит от конкретного API, версии модели, системного промпта и параметров генерации.
- Сетевая задержка может скрыть разницу в скорости генерации.
- Даже при фиксированном seed ответы сервиса могут оставаться недетерминированными.
- Обновление модели под прежним идентификатором требует нового запуска и фиксации даты.
Для более надежного вывода повторите протокол на нескольких задачах: выбор одного инструмента, последовательная зависимость трех инструментов, параллельные чтения, восстановление после ошибки и отказ от запрещенного действия.
Критерий завершения
Эксперимент воспроизводим, если другой разработчик может взять те же схемы, тестовые данные, системную инструкцию и правила оценки, подставить доступные API-адаптеры и получить журнал, из которого автоматически рассчитываются все опубликованные показатели.
Дополнительные шаблоны построения агентных проверок собраны в гайдах Agent Lab Journal. Определения tool calling, контекстного окна, перцентиля и других терминов доступны в глоссарии.