Структурированный вывод (--json-schema)
Ограничьте итоговый ответ модели JSON-схемой, которую вы предоставляете. Qwen Code регистрирует синтетический терминальный инструмент, который модель обязана вызвать, разбирает аргументы вызова по вашей схеме и выводит проверенный нагрузочный объект в stdout (или в обёртку JSON / stream-json). Первый успешный вызов завершает сессию.
Только безголовый режим — работает с qwen -p, позиционным промптом или промптом,
переданным через stdin.
Быстрый старт
qwen --prompt "Опиши изменения в HEAD с уровнем риска" \
--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 или другой потребитель.
В режиме text 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-литерал inline
qwen -p "…" --json-schema '{"type":"object", "properties":{…}}'
# Чтение из файла
qwen -p "…" --json-schema @./schemas/summary.jsonФорма @путь раскрывает ~, нормализует путь и читает файл в кодировке utf8.
Замечание по задержке: При успешных запусках возникает задержка завершения не более ~500 мс — фоновые агенты, ещё работающие, сбрасывают свои финальные уведомления, прежде чем будет выдан результат. Задержка завершается раньше, если фоновых задач нет, поэтому простые запуски её почти не замечают; пакетные конвейеры, выполняющие сотни вызовов
--json-schemaс занятыми агентами, должны учитывать эту верхнюю границу.
Замечание по безопасности: Схемы могут содержать пользовательские регулярные выражения в ключевых словах
pattern. Ajv компилирует их с помощью движка регулярных выражений ECMAScript, который уязвим к катастрофическому возврату. Поскольку аргументы инструмента всегда являются объектами, ключевое словоpatternсрабатывает только внутри строковых свойств — злонамеренная схема вида{"type":"object","properties":{"value":{"type":"string","pattern":"(a+)+b"}}}может «повесить» CLI, если модель предоставит достаточно длинное совпадающее значение. Запускайте--json-schemaтолько со схемами из надёжных источников.
Валидация на этапе разбора:
- Файл должен быть обычным файлом (не FIFO, символьные устройства или директории).
- Размер файла ограничен 4 МиБ. Реальные JSON-схемы значительно меньше этого лимита; файлы размером в несколько МиБ почти всегда означают ошибку с путём.
- Схема должна быть корректным JSON. Для ввода через
@путьсообщение об ошибке разбора будет общим («содержимое<путь>не является корректным 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 | Отклоняется на этапе разбора. Сообщение синтетического инструмента «session ends now» не имеет терминатора в цикле TUI. |
--json-schema + --input-format stream-json | Отклоняется на этапе разбора. Контракт однократного завершения несовместим с долгоживущим протоколом ввода stream-json. |
--json-schema + --acp / --experimental-acp | Отклоняется на этапе разбора. ACP запускает собственный цикл обработки, который не соблюдает контракт завершения синтетического инструмента. |
--json-schema без подсказки и без stdin через конвейер | Отклоняется на этапе разбора. Безголовому режиму нужна подсказка — передайте -p, позиционный аргумент или подайте через конвейер. |
--bare + --json-schema | Поддерживается. Синтетический инструмент регистрируется наряду с тремя базовыми (read_file, edit, run_shell_command). |
--json-schema внутри субагента | Инструмент НЕ регистрируется. Только основные / сливные ходы верхнего уровня выполняют контракт завершения; субагент, вызывающий инструмент, получит «session ends now» и продолжит работу, так как его цикл не имеет терминатора. |
Повторные попытки и режимы ошибок
Замечание о затратах. Два фактора умножают расход токенов в запуске
--json-schema, и оба стоит учитывать заранее:
- Схема встроена в каждый ход. Схема передаётся как блок
parametersобъявления функцииstructured_outputв каждом запросе к модели, а не только в первом. Большие схемы (до предела разбора в 4 МиБ) пропорционально увеличивают входные токены на каждый ход в течение всего сеанса.- Каждая новая попытка проверки — это полный ход модели. Схему, которую модель постоянно пропускает, умножают на каждый сбой (запрос + вывод + ответ). Ограничьте схемы так, чтобы они направляли модель и были достаточно простыми для успеха с первой попытки; увеличьте
--max-session-turns, если ожидаются повторные попытки.
Сессия завершается при первом успешном вызове. До этого:
- Параметры не проходят проверку.
structured_outputвозвращает ошибку инструмента с сообщением Ajv, модель видит её на следующем ходу и может исправить аргументы и вызвать снова. - Модель вызывает инструмент с побочными эффектами в том же ходу, что и
structured_output. Предварительный просмотр подавляет родственный вызов — он никогда не выполняется, независимо от того, прошла ли в итоге проверка структурированного вызова. Два пути расходятся в том, что модель увидит дальше:- Проверка успешна: сеанс завершается немедленно, и модель больше не получает хода — подавленный родственный вызов молча отбрасывается.
- Проверка не удалась: модель получает ещё один ход и видит синтезированный
tool_resultс текстом «Skipped:» для подавленного вызова, чтобы она могла повторно отправить этот вызов в отдельном ходу (который не включает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. Обычный режим игнорирует большинство входных данных, полученных из настроек, включая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 "High-risk diff; pausing pipeline." >&2
exit 2
fiСм. также
- Headless Mode — основанный на
-pпоток, на который опирается--json-schema. - Dual Output — записывает JSON-событие в sidecar рядом с TUI (другой подход к машиночитаемому выводу; не требует
--json-schema).