让 Agent 稳定吐出结构化 JSON:Claude Agent SDK 的 outputFormat 怎么配

2026-08-18

一、它解决的是「解析别人写的散文」这件苦差事

用 agent 做自动化,最后一公里往往卡在同一个地方:agent 干得挺好,但它把结果写成了一段话。你要把它塞进数据库、塞进前端组件、塞进下一个流程节点,就得自己写解析——把标题抠出来、把「15 分钟」转成数字、把材料和步骤分开,还得应付每次回复格式都不太一样。官方文档在这一页开头举的正是这个例子:一个查菜谱的应用,没有结构化输出时拿回来的是一段带标题和列表的文本,有结构化输出时拿回来的是带 nameprep_time_minutesingredients 的 JSON。

文档对这个能力的描述是:你定义一个 JSON Schema,agent 中途该用什么工具就用什么工具,最后你仍然拿到一份经过校验的、匹配你 schema 的 JSON。校验不通过时 SDK 会重新提示(文档原文是 re-prompting on mismatch);如果在重试上限内始终没成功,结果就是一个错误,而不是结构化数据。

顺带说清一件容易混的事:官方文档把「不带工具调用的单轮请求」指向了另一条路——platform.claude.com 上的 API Structured Outputs。这一页讲的是 Agent SDK 这条路,特点是中间可以跑多轮工具调用,结构化只发生在收尾。

二、前置条件

这一段最容易被跳过,但这里恰恰有一个版本坎。

这一页讲的入口只有 SDK 的 query() 文档示例里 TypeScript 从 @anthropic-ai/claude-agent-sdk 引入 query,Python 从 claude_agent_sdk 引入 queryClaudeAgentOptionsResultMessage;两边配置的入口都是传给 query() 的 options。这一页没有出现任何命令行侧的等价开关,所以下文讲的一切都以 SDK 调用为前提;命令行侧有没有对应能力,这一页没有说明。

版本:官方文档在这一页两次点名了 v2.1.205 这个分界。 一处是无效 schema 的处理,一处是 format 关键字的处理,两处的旧行为都和现在不同(细节放在第四节)。所以如果你手上的 SDK 版本比这个老,本文第四节描述的边界行为对你不成立——升级之前先别急着照着排查。

可选的类型层。 TypeScript 侧可以用 Zod,Python 侧可以用 Pydantic,作用是替你生成 JSON Schema,并把返回值解析成带类型的对象。这两个不是必需品,手写 JSON Schema 一样能跑。

平台差异:官方文档在这一页没有说明 Windows 与 Linux/macOS 有任何区别。 这一页写的全是 SDK 的 option 名和返回字段,通篇不涉及 shell 命令、路径分隔符或环境变量,也没有出现任何 Windows 专属参数。所以别照着别处的经验去找平台开关——这一页没有给出这类信息,真遇到平台相关的差异,以官方文档最新内容为准。

三、按文档写明的步骤走一遍

第一步:声明 schema

最直白的方式是手写一个 JSON Schema 对象。官方文档的快速开始示例是这样的(TypeScript):

const schema = {
  type: "object",
  properties: {
    company_name: { type: "string" },
    founded_year: { type: "number" },
    headquarters: { type: "string" }
  },
  required: ["company_name"]
};

Python 侧是等价的 dict:

schema = {
    "type": "object",
    "properties": {
        "company_name": {"type": "string"},
        "founded_year": {"type": "number"},
        "headquarters": {"type": "string"},
    },
    "required": ["company_name"],
}

注意 required 里只写了 company_name。文档在另一个示例(TODO 追踪 agent)里解释了这个取舍:那个 schema 把 authordate 设成可选,因为 git blame 信息未必对所有文件都拿得到,「agent 填它能找到的,其余略过」。这条在第四节的避坑建议里被再次强调——任务可能拿不到的信息,就别放进 required

第二步:把 schema 挂到 outputFormat / output_format

这是唯一的配置入口。文档在「Output format configuration」一节写明这个对象接受两个键:

取值说明
type"json_schema"结构化输出就设成这个值
schemaJSON Schema 对象定义输出结构;可由 z.toJSONSchema(schema, { target: "draft-7" }) 或 Pydantic 的 .model_json_schema() 生成

TypeScript 里这个选项叫 outputFormat,Python 里叫 output_format——文档就是这么分别写的,一个驼峰一个下划线,别写串了。文档没有解释为什么要分成两种拼法,这里也不替它猜:

options: {
  outputFormat: {
    type: "json_schema",
    schema: schema
  }
}
options=ClaudeAgentOptions(
    output_format={"type": "json_schema", "schema": schema}
)

第三步:从结果消息里取 structured_output

agent 跑完之后,结果消息上会多一个 structured_output 字段,里面是校验过的数据。TypeScript 侧文档示例的判定条件是三个条件同时成立:

if (message.type === "result" && message.subtype === "success" && message.structured_output) {
  console.log(message.structured_output);
}

Python 侧是 isinstance(message, ResultMessage) and message.structured_output。这里的 subtype 是关键,第四节会讲它为什么不能省。

第四步(可选):换成 Zod 或 Pydantic

如果你不想手写 JSON Schema,可以让这两个库替你生成。但 TypeScript 这边有一个必须记住的参数,文档写得很直接:SDK 用 JSON Schema draft-07 校验 schema,声明了更新版本的 schema 会被拒绝;而 Zod 默认按 draft 2020-12 生成,所以转换时要显式传 target: "draft-7"

const FeaturePlan = z.object({
  feature_name: z.string(),
  summary: z.string(),
  steps: z.array(
    z.object({
      step_number: z.number(),
      description: z.string(),
      estimated_complexity: z.enum(["low", "medium", "high"])
    })
  ),
  risks: z.array(z.string())
});

// Convert to JSON Schema using the draft-07 target the SDK expects
const schema = z.toJSONSchema(FeaturePlan, { target: "draft-7" });

拿到结果后再用 FeaturePlan.safeParse(message.structured_output) 解析一次,parsed.success 为真时 parsed.data 就是带类型的对象。Python 侧对应的是 FeaturePlan.model_json_schema() 生成 schema、FeaturePlan.model_validate(message.structured_output) 解析结果。文档把这一步的收益列成四条:完整的类型推断与类型提示、运行时校验、更好的错误信息、schema 可组合复用。

以上代码片段均原样取自官方文档的示例;组合使用时请以官方文档与你所用 SDK 版本的实际行为为准,本文未经实测。

四、边界:什么时候它不按你想的来

这一节是本文的落点,也是文档里信息密度最高的部分。

schema 本身写错了会怎样。 文档写明:不是合法 JSON Schema 的 schema 会在启动时让这次 run 失败,并报出指明问题的错误。同时补了一句历史行为——在 v2.1.205 之前,无效 schema 会被静默忽略,agent 照常返回非结构化文本。这句话的实际意义是:如果你在老版本上遇到过「配了 schema 却拿回一段文本、又没有任何报错」,那可能不是 agent 不听话,而是 schema 压根没被接受。

format 关键字不做强制校验。"format": "email" 这种写法,文档说 SDK 的校验器只把它当注解接受,并不强制执行。也就是说别指望它替你挡住不合法的邮箱。同样是 v2.1.205 这个分界:在那之前,任何含 format 的 schema 会被直接当成无效。

支持哪些 JSON Schema 特性。 文档列出的是:全部基础类型(object、array、string、number、boolean、null)、enumconstrequired、嵌套对象,以及 $ref 定义。完整的支持列表与限制,文档把你指向了 platform.claude.com 上的 JSON Schema limitations 一节——那一页不在本文的依据范围内,所以这里不替它复述内容。

失败时结果消息长什么样。 文档给了一张 subtype 对照表,只有两行:

subtype含义
success输出已生成并通过校验
error_max_structured_output_retries多次尝试后仍没有留下有效输出(校验失败,或 model fallback 撤回后没有成功的重试)

还有第三种情况,比上面两种更阴险:subtypesuccess,但 structured_output 没有值。 文档举的场景是 run 正常结束、agent 却没产出结构化输出,并明确要求「把这种情况也当作失败处理」。所以判定条件必须是 subtype === "success" structured_output 存在,少一个就会漏。

失败的原因不止一种,别一上来就改 schema。 文档特意点出:除了校验失败,还有一条路径会走到同一个错误——model fallback 可能在流式过程中把一个已经完成的输出撤回,若没有重试补上,这次 run 就以同样的错误收尾。文档给的分辨方法是:先看结果消息上的 errors 列表,确认是哪一类,再决定要不要动 schema。这一条挺反直觉的——错误码一样,根因不一样。

异常与结果消息的关系。 文档在每段示例的注释里都重复了同一句:单次的 query()先 yield 出一个错误结果之后才抛出(Python 是 raise)异常。所以正确的写法是循环里先按 subtype 分支处理,外面再包 try/catch 兜住连接或进程级失败——后者不会产生任何结果消息。只写 try/catch 不写分支,你会丢掉区分根因的那点信息。

几个文档没有说明的点,如实标注: 重试上限具体是多少次、重试时会怎样重新提示、校验器对不合法输出给出的错误文本长什么样——官方文档在这一页都没有说明。这一页也没有把这个能力标为 betapreviewexperimental

文档最后给了三条避免出错的建议,都属于「先改任务、再改 schema」的思路:schema 保持聚焦,嵌套很深、必填很多的 schema 更难被满足,从简单开始;schema 要匹配任务,任务未必拿得到的信息就设成可选;提示词写清楚,含糊的提示会让 agent 猜不出该产出什么。

五、怎么确认自己配对了

不需要复杂的验证脚本,按文档写明的行为设四个断言点就够:

其一,故意写坏一个 schema。 按文档的描述,一个不合法的 JSON Schema 应当在启动时就让 run 失败,并报出指明问题的错误。如果你看到的是「没报错、返回了一段自由文本」,按文档记录的历史行为,这正是 v2.1.205 之前那套「静默忽略」的表现——先去核 SDK 版本,别急着改 prompt。

其二,正常跑一次,把判定条件写全。 用文档的错误处理示例作模板,三条分支各打一条日志:

if (msg.type === "result") {
  if (msg.subtype === "success" && msg.structured_output) {
    console.log(msg.structured_output);
  } else if (msg.subtype === "error_max_structured_output_retries") {
    console.error("Could not produce valid output");
  } else {
    console.error("Run ended without a structured output");
  }
}

第三条分支要是被打中了,说明你撞上了「success 但没有 structured_output」那种情况,不是配错,而是这次 run 没产出。

其三,如果用了 Zod,检查 target: "draft-7" 有没有传。 这是 TypeScript 侧最容易漏的一处:Zod 默认生成 draft 2020-12,而 SDK 按 draft-07 校验并拒绝声明了更新版本的 schema。漏了这个参数,症状会表现为 schema 无效——排查时很容易跑偏到「是不是我字段写错了」。

其四,遇到 error_max_structured_output_retries 时,先读 errors 列表。 确认是校验失败还是 model fallback 撤回,再决定动不动 schema。省掉这一步,你可能会把一个和 schema 无关的问题,用简化 schema 的方式「修」半天。


本文依据 Claude Code 官方文档(code.claude.com/docs)于 2026-08-17 的公开内容整理。 该产品闭源,本文只复述官方文档写明的机制,不推断其内部实现我们没有对文中涉及的功能做过实测,因此不涉及界面外观、操作手感与运行速度的任何描述。 该产品迭代频繁,文中涉及的命令、配置项与默认值随版本变动,请以官方文档最新内容为准。 本文不涉及价格、额度与限流的具体数值,相关信息请以官方定价与用量说明页为准。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。