Структурированный вывод (--json-schema)
Ограничьте окончательный ответ модели JSON Schema, которую вы предоставляете. Qwen Code регистрирует синтетический терминальный инструмент, который модель обязана вызывать, разбирает аргументы этого вызова на основе вашей схемы и выводит проверенный полезный груз на stdout (или в конверт результата JSON / stream-json). Первый валидный вызов завершает выполнение.
Только headless-режим — работает с qwen -p, позиционным промптом или промптом,
переданным через stdin.
Быстрый старт
qwen --prompt "Summarize the changes in HEAD with risk_level" \
--json-schema '{
"type": "object",
"properties": {
"summary": { "type": "string" },
"risk_level": { "type": "string", "enum": ["low", "medium", "high"] }
},
"required": ["summary", "risk_level"],
"additionalProperties": false
}'Вывод на stdout (по умолчанию --output-format text):
{ "summary": "…", "risk_level": "low" }Строка содержит ровно JSON-строкифицированный полезный груз + символ новой строки — без
конверта, без журнала событий. Передавайте её напрямую в jq или другой потребитель.
В текстовом режиме stdout зарезервирован для полезного груза JSON при успехе
и пуст при неудаче; сообщения об ошибках и строки лога пишутся в stderr.
Это делает конструкции вида $(qwen --json-schema …) || exit 1 безопасными
в текстовом режиме — ошибки попадают в stderr, а не смешиваются в захваченную переменную.
Попутная проза модели во время планирования не дублируется в stderr —
текстовый режим отбрасывает её; используйте --output-format json или stream-json,
если вам нужно её видеть.
В --output-format json и stream-json сообщение о неудачном результате выводится
на stdout вместе с путём успеха (как последний элемент JSON-массива или завершающая
строка result в JSONL-потоке). Не все сценарии сбоя выводят результат на stdout —
превышение максимального количества сессионных шагов (код выхода 53) и прерывания по сигналу
(код выхода 130) завершаются только выводом в stderr. Сначала проверяйте код выхода;
is_error на объекте результата позволяет различить ошибки в рамках того подмножества сбоев,
которые всё же генерируют событие результата.
Пустая схема: Передача
{}выводит{}(пустой JSON-объект) на stdout. Модель вызываетstructured_outputбез аргументов; путь нормализации аргументов преобразует пустой вызов функции в пустой объектный полезный груз, который проходит валидацию по пустой схеме и выводится как есть.
Передача схемы
Две равнозначные формы:
# Встроенный литерал JSON
qwen -p "…" --json-schema '{"type":"object", "properties":{…}}'
# Чтение из файла
qwen -p "…" --json-schema @./schemas/summary.jsonФорма @path раскрывает ~, нормализует путь и читает файл в кодировке utf8.
Замечание по задержке: Успешные запуски включают удержание завершения ограниченное ~500 мс, пока фоновые агенты, находящиеся в обработке, сбрасывают свои финальные уведомления перед выводом результата. Удержание завершается раньше, если нет ожидающих фоновых задач, поэтому простые запуски почти не замечают его; пакетные пайплайны, запускающие сотни вызовов
--json-schemaпротив занятых агентов, должны учитывать эту верхнюю границу.
Замечание по безопасности: Схемы могут содержать предоставленные пользователем регулярные выражения в ключевых словах
pattern. Ajv компилирует их с помощью ECMAScript-движка регулярных выражений, который уязвим к катастрофическому возврату. Поскольку аргументы инструментов всегда являются объектами, ключевое словоpatternсрабатывает только внутри строковых свойств — злонамеренная схема вида{"type":"object","properties":{"value":{"type":"string","pattern":"(a+)+b"}}}может повесить CLI, когда модель предоставляет достаточно длинное подходящее значение. Запускайте--json-schemaтолько со схемами из источников, которым вы доверяете.
Валидация во время разбора:
- Файл должен быть обычным файлом (не FIFO, символьные устройства или каталоги).
- Размер файла ограничен 4 МиБ. Реальные JSON-схемы значительно меньше этого; файлы размером в несколько МБ почти всегда указывают на ошибку с путём.
- Схема должна быть допустимым JSON. Для ввода
@pathсообщение об ошибке разбора является общим («содержимое<путь>не является допустимым JSON») без повторения деталей SyntaxError, чтобы процесс-обёртка, отображающий stderr, не мог прочитать префикс содержимого файла из ошибки. - Схема должна компилироваться в строгой конфигурации Ajv — опечатки вроде
properteesотображаются, но допустимые согласно спецификации шаблоны (например,requiredбез перечисления каждого ключа вproperties) принимаются. - Корень схемы должен принимать значения объектного типа. API для вызова функций (Gemini, OpenAI, Anthropic) требуют, чтобы аргументы инструментов были JSON-объектами, поэтому корень, не являющийся объектом, зарегистрировал бы непригодный инструмент.
Проверка приёма корня проходит по type, const, enum, anyOf,
oneOf, allOf, not и if/then/else (максимально возможные усилия для разрешимых случаев).
В сомнительных случаях решение откладывается на Ajv во время выполнения.
Корневой
$refотклоняется проверкой во время разбора. Если ваша схема повторно использует определение через$ref, оберните его вallOf:// Отклонено: { "$ref": "#/$defs/MyObj", "$defs": { "MyObj": { "type": "object", "properties": { "name": { "type": "string" } } } } } // Принято (корень принимает объекты через ветку allOf): { "allOf": [{ "$ref": "#/$defs/MyObj" }], "$defs": { "MyObj": { "type": "object", "properties": { "name": { "type": "string" } } } } }
$refвнутриanyOf/oneOf/allOfпередаётся Ajv во время выполнения, поэтому обёрнутая форма проходит проверку приёма корня.
Форма вывода в зависимости от формата
--output-format | Что выводится на stdout |
|---|---|
text (по умолчанию) | JSON.stringify(payload) + "\n" — одна строка, проверенный объект. |
json | Один JSON-массив объектов сообщений (полный журнал событий). Последний элемент — сообщение type: "result", которое содержит как result (JSON.stringify(payload)), так и structured_result (сырой объект). |
stream-json | Каждое событие на отдельной строке в формате JSONL. Завершающая строка result содержит result (строкифицированный) и structured_result (сырой объект). |
В обоих JSON-форматах предпочтительнее читать structured_result, а не result,
когда вам нужен объект; result — это строкифицированная форма для потребителей,
которые ожидают строку в этом поле. Для --output-format json читайте последний элемент
массива и извлекайте structured_result (например, jq '.[-1].structured_result');
для stream-json читайте последнюю строку type: "result" в потоке.
Ограничения
| Комбинация | Поведение |
|---|---|
--json-schema + -i / --prompt-interactive | Отклоняется во время разбора. Сообщение синтетического инструмента «сеанс заканчивается сейчас» не имеет терминатора в цикле TUI. |
--json-schema + --input-format stream-json | Отклоняется во время разбора. Однократный терминальный контракт несовместим с долгоживущим протоколом ввода stream-json. |
--json-schema + --acp / --experimental-acp | Отклоняется во время разбора. ACP запускает собственный цикл шагов, который не соблюдает терминальный контракт синтетического инструмента. |
--json-schema без промпта и без ввода через stdin | Отклоняется во время разбора. Headless-режиму нужен промпт — передайте -p, позиционный аргумент или подайте через stdin. |
--bare + --json-schema | Поддерживается. Синтетический инструмент регистрируется вместе с базовыми тремя (read_file, edit, run_shell_command). |
--json-schema внутри под-агента | Инструмент НЕ регистрируется. Только основные / дренажные шаги верхнего уровня соблюдают терминальный контракт; под-агент, вызвавший инструмент, получит сообщение «сеанс заканчивается сейчас» и продолжит работу, так как его цикл не имеет терминатора. |
Повторные попытки и сценарии сбоя
Замечание по затратам. Два фактора умножают расход токенов в запуске
--json-schema, и оба стоит учитывать:
- Схема встроена в каждый шаг. Схема передаётся как блок
parametersобъявления функцииstructured_outputв каждом запросе к модели, а не только в первом. Большие схемы (до лимита разбора в 4 МиБ) пропорционально увеличивают количество входных токенов на шаг для всего сеанса.- Каждая повторная попытка валидации — это полный шаг модели. Схема, которую модель постоянно упускает, умножается на количество сбоев (запрос + инференс + ответ). Соблюдайте схемы достаточно ограниченными, чтобы направлять модель, и достаточно простыми, чтобы попасть с первой попытки; увеличивайте
--max-session-turns, когда ожидаются повторные попытки.
Сеанс завершается на первом валидном вызове. До тех пор:
- Аргументы не проходят валидацию.
structured_outputвозвращает ошибку результата инструмента с сообщением Ajv; модель видит её на следующем шаге и может исправить аргументы и вызвать снова. - Модель вызывает инструмент с побочным эффектом на том же шаге, что и
structured_output. Предварительное сканирование подавляет этот вызов — он никогда не выполняется, независимо от того, пройдёт ли структурированный вызов валидацию в итоге. Два пути расходятся на том, что модель увидит далее:- Валидация успешна: сеанс немедленно завершается, и модель не получает следующего шага — подавленный вызов молча отбрасывается.
- Валидация не удалась: модель получает следующий шаг и видит синтезированный
tool_resultс пометкой «Пропущено:» для подавленного вызова, чтобы она могла повторно выполнить этот вызов на отдельном шаге (который не включаетstructured_output).
- Модель выводит обычный текст вместо вызова
structured_output. Код выхода1. Сообщение об ошибке включает номер шага и усечённый предпросмотр вывода модели, чтобы вы могли увидеть, что она на самом деле сказала. - Сеанс достигает
maxSessionTurns. Код выхода53. Стандартный выход «Достигнут максимум сессионных шагов», плюс специфичная для--json-schemaподсказка, указывающая на три распространённые причины застревания: модель ни разу не вызвала инструмент,structured_outputзапрещён правилами разрешений или схема невыполнима. - Сеанс прерван (SIGINT / Ctrl-C). Код выхода
130. Структурированный результат обычно не выводится, но цикл удержания завершения не опрашивает сигнал прерывания, поэтому SIGINT, поступивший после того, как успешный вызов был захвачен, но до того, как результат достиг stdout, всё же может попасть на stdout. Рассматривайте код выхода как источник истины.
Конфиденциальность
Аргументы, которые вы передаёте через structured_output, И являются структурированным
полезным грузом — уже выведенным на stdout. Чтобы избежать сохранения того же полезного
груза второй раз в локальных хранилищах, которые могут быть экспортированы с устройства,
аргументы редактируются с помощью { __redacted: 'structured_output payload (see stdout result)' } в:
- Данные телеметрии
ToolCallEvent(экспорты OTLP, QwenLogger, поток ui-telemetry, UI-зеркало в журнале чата). - Файл записи чата на диске в формате JSONL по пути
~/.qwen/projects/<sanitized-cwd>/chats/<sessionId>.jsonl(передаётся обратно в контекст модели при--continue/--resume), включая все повторные попытки при неудачной валидации.
Метрики вызовов инструментов (длительность, успех, решение) и метаданные окружающих событий сохраняются.
Схема отправляется провайдеру модели. Редактирование распространяется только на аргументы вызова на локальных поверхностях. Сама схема передаётся с каждым запросом модели как блок
parametersобъявления функцииstructured_output— поэтому любые литеральные значения, которые вы помещаете внутрь неё (enum,const,default,examples,description,$commentи т.д.), достигают провайдера в открытом виде, как и текст промпта. Схемы должны описывать структуру и ограничения; относитесь к ним как к открытой информации для провайдера и не включайте секреты, записи клиентов и другие конфиденциальные полезные грузы в тело схемы.
Хуки видят сырые аргументы. Редактирование, описанное выше, применяется только к телеметрии и записи чата. Хуки
PreToolUse,PostToolUseиPostToolUseFailure(включая HTTP-хуки, которые могут пересылать полезные грузы с устройства) получают неотредактированныйtool_inputдляstructured_output, поскольку контракт хука таков: «видеть то же, что видит инструмент». Если вы используете перехватывающие хуки типа аудита, либо отключите их дляstructured_output(фильтруйте поtool_name), либо добавьте редактирование на стороне хука перед запуском--json-schemaс конфиденциальными данными.
Возобновление сеанса (--continue / --resume)
--json-schema — это флаг запуска, а не свойство сеанса.
Синтетический инструмент регистрируется, когда CLI разбирает свои аргументы, поэтому:
- Заново передавайте
--json-schemaпри каждом--continue/--resume, к которому вы хотите применить терминальный контракт. Та же схема, что и в исходном запуске, является безопасным значением по умолчанию — замена схемы в середине сеанса разрешена, но меняет контракт, которого должна придерживаться модель. - Если вы делаете
--continueбез--json-schema, возобновлённый сеанс является обычным headless-сеансом:structured_outputпросто не существует как инструмент, и модель будет отвечать в свободной текстовой форме. - Заполнитель
__redactedв возобновлённой записи чата практически не влияет на возможность возобновления. Успешный вызовstructured_outputнемедленно завершает сеанс, поэтому единственные отредактированные аргументы, которые может увидеть возобновлённый сеанс, — это от неудачных попыток. Модель по-прежнему имеет ошибку валидации Ajv каждой попытки в записанномtool_resultи живую схему параметров (заново зарегистрированную из--json-schema), чего достаточно для повторной попытки.
Управление разрешениями
structured_output намеренно обходит белый список --core-tools:
инструмент существует только тогда, когда установлен --json-schema, поэтому
исключение его оставило бы сеанс без терминального контракта.
Явные правила permissions.deny и настройки --exclude-tools ДЕЙСТВУЮТ —
оба используют один и тот же механизм запрета и оба предотвращают регистрацию
structured_output, так что модель никогда не видит объявления инструмента.
Типичный результат заключается в том, что модель отвечает простым текстом (код выхода 1).
Если модель зацикливается на других инструментах, так и не создав текст, она в конечном
итоге достигнет maxSessionTurns (код выхода 53), и подсказка --json-schema в
сообщении об ошибке укажет вам источник проблемы.
Оговорка для
--bare. Bare-режим игнорирует большинство входных данных, полученных из настроек, включаяpermissions.denyиtools.excludeна уровне настроек. Синтетический инструмент остаётся зарегистрированным, поэтому явный запретstructured_outputна уровне настроек будет молча игнорироваться в режиме--bare. Флаг на уровне--exclude-tools structured_outputпо-прежнему применяется в bare-режиме — используйте флаг, а не настройки, если нужно заблокировать bare-запуск.
Конфликт с MCP-инструментами
Если MCP-сервер регистрирует инструмент с буквальным именем structured_output,
проверка коллизий реестра инструментов переименовывает MCP-инструмент в
mcp__<server-name>__structured_output, чтобы синтетический инструмент сохранил
собственное имя. Предоставленная пользователем схема всегда доступна модели.
Пример: защита многошагового запуска на основе структурированного вывода
RESULT=$(qwen --prompt "Audit this diff and rate its risk." \
--json-schema @./schemas/audit.json) || exit 1
risk=$(jq -r '.risk_level' <<<"$RESULT")
if [ "$risk" = "high" ]; then
echo "Высокорисковый diff; приостановка пайплайна." >&2
exit 2
fiСмотрите также
- Headless-режим — поток на основе
-p, на котором строится--json-schema. - Двойной вывод — запись JSON-события в sidecar вместе с TUI
(другой подход к машиночитаемому выводу; не требует
--json-schema).