Tool calling и структурированные выходные данные: контракт, который держится
24 вызова, ни одного сломанного JSON и две пригодные даты. Затем тот же endpoint с лучшим описанием — и что schema не исправит.
На этой странице
Дайте модели инструмент для поиска авиабилетов и попросите найти рейс из Мадрида в Берлин. Вот что вернётся:
<tool_call>
{"name": "search_flights",
"arguments": {"from": "Madrid", "to": "Berlin", "date": "3rd October 2026"}}
</tool_call>JSON валиден. Имя инструмента правильное. Все обязательные поля на месте. И вызов бесполезен: ни один API авиабилетов не примет "Madrid" там, где нужен код аэропорта, или "3rd October 2026" там, где нужна дата.
Этот разрыв — синтаксически идеально, семантически непригодно — и есть тема этой главы. И первое, что нужно зафиксировать: это не проблема JSON. За двадцать четыре запроса с этим инструментом модель выдала 24 валидных tool calls и ни одного сломанного JSON. Ни разу она не провалилась на той части, которую все отлаживают.
Модель ничего не выполняет
Ссылка на раздел: Модель ничего не выполняетПрежде чем перейти к механике, фраза, которая предотвращает больше всего путаницы: tool call — это запрос, а не действие.
Модель выдаёт структурированное сообщение: я бы хотела, чтобы search_flights вызвали с такими аргументами. Затем она останавливается. Ваш код получает это сообщение, решает, выполнять ли его, вызывает то, что должен вызвать, и отправляет результат обратно как ещё одно сообщение. Модель никогда не касалась вашей базы данных, никогда не делала HTTP-запрос, никогда не имела учётных данных.
Всё про безопасность agent в главе 30 следует из этого разделения, как и всё про дизайн agent в главе 23: модель предлагает, а ваш код распоряжается, и именно в коде живут все гарантии.
Так что инструмент, если убрать терминологию, — это две вещи:
Schema. JSON Schema, описывающая функцию: её имя, что она делает и какие аргументы принимает, с их типами и ограничениями. Это попадает в prompt, и это единственное, что модель когда-либо видит.
Endpoint. Функция в вашем коде, которая принимает эти аргументы и что-то возвращает. Модель её никогда не видит, не знает, на каком она языке, и не отличит запрос к базе данных от захардкоженной строки.
Вы отправляете schemas вместе с запросом
Ссылка на раздел: Вы отправляете schemas вместе с запросомОпределения инструментов идут в prompt, сериализованные в тот формат, на котором обучали модель. Они стоят tokens при каждом вызове — факт, к которому мы вернёмся с числом позже в этой главе.
Модель отвечает вызовом вместо текста
Ссылка на раздел: Модель отвечает вызовом вместо текстаВместо прозы ответ содержит структурированный запрос, а API сообщает соответствующую причину завершения. Причина важна: по ней ваш код понимает, что нужно запустить инструмент, а не показать пользователю ответ.
Ваш код запускает его — или отказывает
Ссылка на раздел: Ваш код запускает его — или отказываетНа этом шаге модели уже нет. Проверьте аргументы по schema, решите, разрешено ли этому вызывающему это делать, и выполните.
Вы отправляете результат обратно как сообщение
Ссылка на раздел: Вы отправляете результат обратно как сообщениеРезультат становится ещё одним ходом в разговоре, в специально отведённой для него роли. Модель читает его как любой другой контекст.
Модель отвечает или просит другой инструмент
Ссылка на раздел: Модель отвечает или просит другой инструментЭто и есть цикл из главы 23, и причина, по которой один запрос может превратиться в дюжину round trips.
Ничто здесь не является emergent. Как было установлено в главе 11, tool calling — это обученное поведение:1 во время post-training модель видела тысячи разговоров, устроенных ровно так. Поэтому формат зависит от модели, поэтому надёжность так сильно различается между моделями сходного размера, и поэтому модель может вызвать инструмент, которого никогда раньше не видела: форма была обучена, конкретный инструмент приходит из вашего prompt.
Во что обходится плохая schema, измерено
Ссылка на раздел: Во что обходится плохая schema, измереноВот инструмент в том виде, в каком большинство людей пишет его сначала. Обратите внимание: в нём нет ничего неправильного; он просто тонкий:
{
name: "search_flights",
description: "Search for flights.",
parameters: {
type: "object",
properties: {
from: { type: "string", description: "Airport." },
to: { type: "string", description: "Airport." },
date: { type: "string", description: "The date." },
},
required: ["from", "to", "date"],
},
}Двадцать четыре запроса, шесть пар городов, скрещённых с четырьмя способами выразить дату ("3-е число следующего месяца", "следующая пятница", "15 декабря", "завтра"), greedy decoding, чтобы результаты воспроизводились:
| tool called | broken JSON | date in ISO | airports as IATA | everything correct | |
|---|---|---|---|---|---|
| schema выше | 24/24 | 0 | 2/24 | 4/24 | 1/24 |
Сначала прочитайте первые две колонки, а уже потом последние три. Модель каждый раз вызывает правильный инструмент и каждый раз выдаёт корректно сформированный JSON. Сбой целиком в значениях, и эти значения непригодны: "Madrid" вместо MAD, "3rd October 2026" вместо 2026-10-03.
На этом стоит настоять, потому что от этого зависит, куда вы смотрите, когда что-то ломается. Инстинкт — добавить JSON-парсер с повторной попыткой или строже попросить модель вернуть валидный JSON. Ни то ни другое не относится к тому, что произошло здесь.
Теперь измените только описание
Ссылка на раздел: Теперь измените только описаниеТот же endpoint. Тот же код за ним. Та же модель, те же prompts, то же decoding. Меняется только текст в schema:
{
name: "search_flights",
description: "Search scheduled flights between two airports on a given day.",
parameters: {
type: "object",
properties: {
from: {
type: "string",
description: "Departure airport as a three-letter IATA code, e.g. MAD for Madrid. Never a city name.",
pattern: "^[A-Z]{3}$",
},
to: { /* same */ },
date: {
type: "string",
description: "Departure date as an ISO 8601 calendar date, YYYY-MM-DD. Resolve relative dates against today before calling.",
format: "date",
pattern: "^\\d{4}-\\d{2}-\\d{2}$",
},
},
required: ["from", "to", "date"],
},
}| date FORMAT | date VALUE | airport FORMAT | airport VALUE | |
|---|---|---|---|---|
| thin schema | 2/24 | 1/24 | 4/24 | 4/24 |
| described schema | 24/24 | 12/24 | 16/24 | 8/24 |
Формат даты меняется с 2 из 24 на 24 из 24. Идеально — от изменения текста, без правок кода и без логики повторных попыток. Если вы унесёте из этой главы одну рабочую привычку, пусть она будет такой: когда инструмент вызывается неправильно, исправление почти всегда находится в описании, и это самое дешёвое исправление в системе.
Теперь прочитайте вторую колонку — более важную половину.
Schema ограничивает форму. Она не может дать знания.
Ссылка на раздел: Schema ограничивает форму. Она не может дать знания.Дата в ISO-формате 24 раза из 24. Это правильный день 12 раз из 24.
То есть половина вызовов теперь несёт идеально отформатированную дату, которая является неверной датой. Описание сказало модели, какую форму выдать, и модель выдала её безупречно — но превращение "следующей пятницы" в 2026-09-11 требует знать сегодняшнюю дату и выполнить календарную арифметику, а никакое описание этого не даст. С аэропортами та же история: формат вырос с 4 до 16, а значение только с 4 до 8, потому что написать MAD требует знать, что аэропорт Мадрида — MAD.
Это различие — несущая идея главы:
Schema — это контракт о форме. Она может сделать output модели разбираемым, типизированным и согласованным. Она не может сделать его истинным, и любой режим отказа, который переживает хорошую schema, — это сбой знания, а не сбой формата.
Для них нужны разные исправления, и путаница между ними тратит недели. Сбои формата исправляются в описании или с помощью constrained decoding — ниже. Сбои знания исправляются тем, что знание помещают в prompt: текущая дата в system message, поиск аэропорта как второй инструмент, который модель вызывает первым, enum в schema, когда набор достаточно мал, чтобы его перечислить. Обратите внимание, что общего у всех трёх: они переносят проблему из памяти модели во вход модели, а это вся глава 24.
Структурированные выходные данные и что на самом деле такое "constrained decoding"
Ссылка на раздел: Структурированные выходные данные и что на самом деле такое "constrained decoding"Всё выше всё ещё полагается на то, что модель выберет правильную форму. Есть более сильная гарантия, и это лучшая отдача от главы 17.
Вспомните, как работает генерация: на каждом шаге модель выдаёт logit для каждого token в словаре, а sampler выбирает один. Constrained decoding вставляет между ними ещё один шаг. Имея грамматику — выведенную из вашей JSON Schema, — он вычисляет, какие tokens могут легально идти дальше, выставляет logits всех остальных в минус бесконечность и позволяет sampler выбрать из оставшегося.
Если schema говорит, что следующим должен быть {, то каждый token, который не является {, получает вероятность ноль. Не "маловероятно": ноль. Модель не может выдать невалидный JSON, потому что невалидные tokens были удалены из распределения до sampling.
Именно это лежит под "structured outputs", "JSON mode" и "guided generation", и это объясняет их два свойства. Гарантия полная для всего, что грамматика может выразить: типы, обязательные поля, enums, вложенность, — потому что она обеспечивается механически, а не вежливой просьбой. И она ничего не говорит о содержании: грамматика может заставить "date" быть строкой, совпадающей с паттерном даты, но не может заставить её быть правильным днём. Это та же стена, что и в предыдущем разделе, только мы пришли к ней с другой стороны.
Две практические заметки. Это не бесплатно: маску нужно вычислять на каждом шаге, а сложные грамматики дают измеримую задержку. И это меняет то, что делает модель: модель, которую уводят от preferred token, может выдавать худшее содержание при идеальной структуре. Поэтому "вежливо попросить и валидировать" всё ещё разумный default для простых форм, а constrained decoding окупает себя, когда форма сложная или consumer строгий.
Побочные эффекты и одно свойство, которое важно
Ссылка на раздел: Побочные эффекты и одно свойство, которое важноГлава 14 измерила timeout, за которым последовал retry, и в итоге за один ответ были оплачены две генерации. С инструментами тот же сбой становится хуже, потому что инструмент может что-то сделать.
Если ваш код вызывает charge_card, получает timeout и повторяет попытку, у вас две оплаты. Модель понятия не имеет, что всё это произошло; она видит один результат инструмента. Исправление такое же, как в любой distributed system, и это не проблема модели: сделайте операцию идемпотентной, передав вызову ключ, чтобы второе выполнение распознало первое и вернуло его результат вместо повторной работы.
Правило дизайна, которое отсюда следует, стоит сформулировать прямо. Разделяйте чтение и запись в каталоге инструментов. Чтение можно свободно повторять, запускать параллельно и кэшировать. Запись — нельзя, и у неё должен быть ключ, проверка прав и — для всего, о чём пользователь хотел бы знать до выполнения, — шаг approval, который ставит человека между запросом и действием. Этот шаг approval — не любезность: это одна из немногих вещей между prompt injection и реальным последствием, и, как измеряет глава 30, самая слабая из них.
Сколько инструментов нужно, чтобы началась деградация?
Ссылка на раздел: Сколько инструментов нужно, чтобы началась деградация?Фольклор говорит, что загрузка большого числа инструментов заставляет модель хуже выбирать. Это стоит измерить, а не повторять. Итак: те же двадцать четыре запроса, инструмент авиабилетов плюс растущий набор других — включая три намеренно похожих (расписания поездов, паромные переправы, автобусные маршруты).
| tools loaded | prompt tokens | chose search_flights | date in ISO |
|---|---|---|---|
| 1 | 353 | 24/24 | 24/24 |
| 5 | 730 | 24/24 | 24/24 |
| 10 | 1,193 | 21/24 | 21/24 |
| 20 | 2,119 | 24/24 | 24/24 |
Выбор не ухудшился. С двадцатью инструментами, три из которых правдоподобно можно спутать, модель на полмиллиарда parameters выбрала правильный инструмент двадцать четыре раза из двадцати четырёх. Просадка на десяти — это три вызова, назвавшие другой инструмент, и она не сохраняется при переходе к двадцати.
Это отрицательный результат, и его нужно так и сообщать: на этой задаче, с этими инструментами, "слишком много инструментов" не было проблемой. Что действительно росло — монотонно и в шесть раз, — так это prompt: с 353 tokens до 2,119, оплачиваемых при каждом запросе в разговоре, всегда, независимо от того, используется ли хоть один инструмент.
Так что честная версия фольклора — про стоимость и контекст, а не про точность. Двадцать инструментов — это постоянный налог на каждое сообщение, а глава 16 уже показала, что постоянный префикс делает со счётом на сорока ходах. Когда люди говорят, что множество инструментов портит качество, механизм обычно в том, что определения вытеснили важный контекст — это проблема главы 24 в костюме главы 18. Инструменты, которые действительно почти дублируют друг друга, тоже реальная проблема, и исправление для них — не меньше инструментов, а лучшие описания и namespaces: префиксуйте их по системе (crm.search_customer, billing.search_customer), чтобы два каталога, объединённые от двух команд, не конфликтовали, и чтобы модели было за что различать.
Три вида инструментов и тот, который открывает следующую часть
Ссылка на раздел: Три вида инструментов и тот, который открывает следующую частьПолезно сортировать инструменты по тому, что они делают с миром, потому что инженерия для каждого отличается.
Инструменты данных читают: ищут, получают, запрашивают. Их можно повторять, параллелить, кэшировать. Они ломаются, возвращая ничего полезного, а главный риск в том, что они приносят недоверенный текст в контекст — это вся поверхность атаки главы 30.
Инструменты действий пишут: отправляют, создают, списывают, удаляют. Их нельзя безопасно повторять без ключа, нельзя безопасно параллелить, и именно из-за них существуют approval flows.
Инструменты оркестрации вызывают другие модели. Инструмент, реализация которого — другой agent, со своим prompt, своими инструментами и своим циклом, — и для вызывающей модели он выглядит ровно как первые два, потому что schema и endpoint — всё, что она когда-либо видит.
Этот третий вид — не курьёз. Это механизм за половиной agent-as-a-tool из главы 25: другая топология, handoff, отдаёт разговор и никогда не получает его обратно. И это работает именно потому, что интерфейс в этой главе достаточно узок, чтобы за ним поместился целый agent.
Куда дальше
Ссылка на раздел: Куда дальшеТеперь у вас есть модель, которая может просить о вещах, и контракт, который делает просьбу разбираемой. Чего у вас нет — так это предмета, о котором ей спрашивать, кроме того, что помещается в её prompt.
Самый распространённый инструмент в production, с большим отрывом, — поиск по корпусу текста, которого модель никогда не видела во время обучения: ваша документация, ваши тикеты, ваши контракты. Это звучит как решённая задача — embedded его, нашли ближайших соседей, вставили их, — но нерешённые части как раз определяют, можно ли доверять ответу: как текст нарезается перед embedding, какой порог сходства достаточно низок, чтобы означать я не знаю, и как цитата прикрепляется к утверждению, чтобы читатель мог его проверить.
Глава 19 — про retrieval, и это глава, где неправильный ответ перестаёт быть любопытным случаем и становится ответственностью.
Источники и метод
Ссылка на раздел: Источники и методИзмерения в этой главе получены из Qwen/Qwen2.5-0.5B-Instruct с greedy decoding, по 24 сгенерированным запросам: шесть пар городов, скрещённые с четырьмя формулировками дат, с использованием собственного chat template модели для определений инструментов. Они воспроизводятся точно, и это маленькая модель: читайте разделение format/value как демонстрацию механизма, а не как benchmark того, что делают современные модели. Frontier model корректно разрешает "следующую пятницу" гораздо чаще — и всё равно её нельзя заставить сделать это с помощью schema, а именно эта часть обобщается.
Словарь JSON Schema, использованный выше (type, properties, required, pattern, format, enum), задан в draft JSON Schema, который называет документация вашего provider; полезное подмножество невелико и одинаково у разных providers, а существующие различия — какие keywords enforced через constrained decoding, а какие лишь передаются модели, — стоит читать в structured-output guide provider, а не предполагать.
Для constrained decoding как техники библиотеки guidance-стиля и проект outlines документируют построение grammar-to-logit-mask так, что оно напрямую ложится на sampler из главы 17. А для самого round trip самая ясная спецификация — не tutorial, а протокол: глава 26 читает его построчно.
Сноски
Ссылка на раздел: Сноски-
Ouyang, L. et al. Training language models to follow instructions with human feedback. arXiv:2203.02155 (2022). Статья, которая сделала рецепт post-training стандартом; форма tool call выучивается там, на demonstrations, ровно как форма ответа. ↩