Anthropic SDK 结构化输出

QuibblerAgentQuibblerAgent 2026-11-01 约 24 分钟 3 次阅读

前两篇拿回的都是自然语言:第 1 篇的第一次调用返回一段解释,第 2 篇让这段话逐字上屏。可真实工程里,模型的输出多数时候不是给人读的,而是给程序用的——分类结果要落进布尔分支,抽取的字段要写进数据库,Agent 的决策要能被调度代码分发。这些场景需要的不是"一段话",而是一个字段名固定、类型可预期的对象,这就是结构化输出要解决的问题。

本文是《Anthropic SDK 从入门到实战》第 3 篇,把 Anthropic 当前的结构化输出方案讲全:先对照 OpenAI 把三条路线定位清楚,再逐条落地——原生的 parse 方法、工具加 strict 模式、纯文本加 json.loads 兜底,最后用一个发票字段提取器把校验循环串起来。全文按"路线定位 → 原生姿势 → 工具姿势 → 文本兜底 → 实战 → 总结"组织。

1、三条路线与 OpenAI 对照

先说结论:Anthropic 现在有原生结构化输出,不再是"只能靠工具曲线救国"的旧格局。2025 年 11 月,Anthropic 为 Messages API 上线了结构化输出(structured outputs),先公开测试后正式发布;到 2026 年 10 月,官方文档把它与严格工具(strict tool use)并列为两个特性。两家厂商的官方答案对比如下:

对比项 OpenAI Anthropic
原生 JSON 模式 response_format 传 JSON Schema output_config.format 传 JSON Schema
SDK 便捷方法 parse 辅助方法 client.messages.parse 传 Pydantic 模型
工具参数强校验 工具定义加 strict 工具定义加 strict
强制指定工具 tool_choice 可指定 当前代模型已移除,改由提示词点名

历史脉络值得知道。在原生模式出现之前,Anthropic 没有独立的 JSON 模式,社区通行做法是"工具强制"——把想要的 JSON 写成某个工具的参数 schema,再用 tool_choice 逼模型调用它,从 tool_use 块里取参数当结果。这条路曾经是唯一解,旧教程至今仍在教,但它有一半已经失效:

注意:tool_choice={"type": "tool", "name": ...} 与 {"type": "any"} 在 Fable 5.1、Opus 5.5、Sonnet 5.5 上会直接返回 400,报错为 tool_choice: type "tool" and "any" are not supported for this model.。这与第 1 篇提醒过的采样参数移除同属一类:照抄 2026 年之前的教程,必错。仍在维护旧模型(Opus 5、Sonnet 5 及更早)的代码可以继续用,但新代码不要依赖它。

所以现在的决策树简单了:单纯要一段 JSON,走原生结构化输出;任务本身要挂一堆工具、JSON 只是其中一环,走工具加 strict;快速原型对格式要求松,才考虑纯文本。流程如下:

flowchart TD A[需要结构化数据] --> B{是否只取 JSON} B -->|是| C[parse 方法] B -->|否| D{是否混用工具} D -->|是| E[工具加 strict] D -->|否| F[文本加 loads] C --> G[类型有保证] E --> G F --> H[需自行容错]

解读:前两条路由服务端兜底,字段与类型有保证,解析不会翻车;第三条路全靠提示词与容错代码,能用但不保险。下面三节按这个顺序逐条落地。

2、主姿势:原生结构化输出

原生模式的核心是把 schema 随请求发给服务端,由服务端约束生成。SDK 在这上面封了一层 parse 方法,直接吃 Pydantic 模型。最小示例如下:

from typing import List
import anthropic
from dotenv import load_dotenv
from pydantic import BaseModel

load_dotenv()
client = anthropic.Anthropic()

class LeadInfo(BaseModel):
    name: str
    email: str
    plan: str
    interests: List[str]

response = client.messages.parse(
    model="claude-sonnet-5-5",
    max_tokens=1024,
    messages=[{
        "role": "user",
        "content": "提取:王雷(lei@co.com)想要企业版,关注 API 与 SDK。",
    }],
    output_format=LeadInfo,        # 目标结构直接传模型类
)

lead = response.parsed_output      # 已通过校验的 LeadInfo 实例
print(lead.name, lead.interests)

解读:parse() 的参数与非流式 create() 完全一致(max_tokens 依旧必填),多出的 output_format 接一个 Pydantic 模型。SDK 在幕后做三件事:把模型类转成 JSON Schema,作为 output_config.format 发给服务端;服务端保证输出严格符合 schema;响应回来后校验并反序列化,挂到 parsed_output 上。你拿到的直接就是一个类型完备的对象,json.loads 与手工校验全省了。

API 层的原始形态也值得看一眼,create() 直接传 output_config 的写法如下:

response = client.messages.create(
    model="claude-sonnet-5-5",
    max_tokens=1024,
    messages=[{"role": "user", "content": "……同上一条……"}],
    output_config={
        "format": {
            "type": "json_schema",
            "schema": {
                "type": "object",
                "properties": {
                    "name": {"type": "string"},
                    "email": {"type": "string"},
                    "plan": {"type": "string"}
                },
                "required": ["name", "email", "plan"],
                "additionalProperties": False
            }
        }
    },
)

import json
text = next(b.text for b in response.content if b.type == "text")
data = json.loads(text)             # 首个 text 块保证是合法 JSON

解读:output_config.format 是 API 层的正式参数,parse() 的 output_format 只是它的便捷包装;这条路径下保证的是"第一个 text 块是合法 JSON",解析仍要自己做。因此日常优先 parse(),output_config 的价值在于不想引入 Pydantic、或用其他语言对接时手写 schema 直接发。两个参数的完整定义见官方 Messages API 参考,特性说明见官方结构化输出文档。

三个使用细节要有数。一是 schema 子集:只支持基础类型、enum、anyOf、$ref 与常见字符串格式(date、email 等),不支持递归 schema 与数值区间、字符串长度约束;Python SDK 会自动剥掉不支持的约束、改在客户端校验,Pydantic 模型里写 Field(gt=0) 不会报错。二是延迟:每个新 schema 有一次编译开销,相同 schema 24 小时内走缓存,高频任务不受影响。三是它并不排斥工具——output_config 与 tools 可以同用,工具负责查数据,output_config 负责约束最终文本,一次请求两头都拿到保证。

三是照旧检查 stop_reason——refusal 时输出可能不符合 schema,max_tokens 时 JSON 可能被截断,第 1 篇养成的"先看停止原因再消费内容"在这里同样适用。该特性与引用(citations)互斥,与流式、批处理、思考模式兼容——第 2 篇的打字机与它不冲突。

3、工具路线与 strict 模式

第二条路线服务于"本来就要用工具"的任务:管线里已经挂着查库存、发邮件等真实工具,结构化抽取只是其中一环。做法是把想要的输出结构定义成一个工具,schema 即答案的形状:

tools = [{
    "name": "record_contact",
    "description": "把用户提供的联系方式登记为一条记录",
    "strict": True,                # 参数必须严格符合 schema
    "input_schema": {
        "type": "object",
        "properties": {
            "name": {"type": "string"},
            "email": {"type": "string"},
            "plan": {"type": "string", "enum": ["Pro", "Max", "企业版"]}
        },
        "required": ["name", "email", "plan"],
        "additionalProperties": False
    }
}]

response = client.messages.create(
    model="claude-sonnet-5-5",
    max_tokens=1024,
    system="收到联系方式时,必须调用 record_contact 工具完成登记。",
    tools=tools,
    messages=[{"role": "user", "content": "李娜(na@co.com)要企业版"}],
)

解读:工具定义四件套——name、description、input_schema,再加 strict: True。strict 有两个前置条件:schema 必须声明 additionalProperties: False,且 required 覆盖全部字段;满足后服务端保证工具参数严格校验通过,这与原生模式是同一套保证机制。提示词里点名工具,替代的正是被移除的强制调用。tool_choice 现存取值如下:

取值 行为 当前代模型
auto 模型自行决定是否调用,默认值 可用
any 必须调用至少一个工具 返回 400
tool 必须调用指定名称的工具 返回 400
none 禁止调用任何工具 可用

auto 意味着"不保证调用",代码里必须检查并兜底。若要把每轮调用压到最多一次,可在 tool_choice 里附上 disable_parallel_tool_use: True——它与 auto 搭配依然有效,结构化抽取场景建议开着,省去处理并行调用的分支。结果从 content 块数组里取,第 1 篇的遍历判型在这里正式派上用场:

from pydantic import BaseModel

class Contact(BaseModel):
    name: str
    email: str
    plan: str

tool_blocks = [b for b in response.content if b.type == "tool_use"]
if not tool_blocks:                       # auto 没触发调用,重试或改写提示词
    raise RuntimeError("模型没有调用工具")

block = tool_blocks[0]
contact = Contact.model_validate(block.input)   # input 已是字典,直接校验
print(contact.email, block.id)

解读:tool_use 块携带 name、id 与 input 三个字段,input 已被 SDK 解析成 Python 字典,不需要再 json.loads;把它交给 Pydantic 的 model_validate 做二次校验,等于在服务端保证之上再加一层业务规则。工具定义与选型的完整说明见官方工具使用文档。

4、次选:预格式 JSON 文本

第三条路线最朴素:不传任何特殊参数,只在 system 里要求"只输出 JSON",然后自己解析。示例如下:

import json

response = client.messages.create(
    model="claude-sonnet-5-5",
    max_tokens=1024,
    system="只输出一个 JSON 对象,不要代码块围栏,不要任何解释文字。",
    messages=[{
        "role": "user",
        "content": "把'北京明天小雨,12 到 19 度'整理为 city、weather、low、high 字段",
    }],
)

FENCE = "`" * 3                     # 围栏标记:三个反引号
raw = next(b.text for b in response.content if b.type == "text").strip()
if raw.startswith(FENCE):           # 容错一:剥掉围栏的首尾行
    raw = "\n".join(raw.splitlines()[1:-1])
start, end = raw.find("{"), raw.rfind("}")
data = json.loads(raw[start:end + 1])   # 容错二:只保留大括号内
print(data)

解读:两处容错对应两类高发翻车——模型手滑加了 markdown 围栏,或在 JSON 前后缀了一句寒暄。这套代码能跑,但没有任何类型与字段保证:多一个字段、少一个字段、数字变字符串,都要自己写判断兜底。还有一类更隐蔽的风险是字段漂移——换个模型或改两句提示词,字段名悄悄变了,下游不会报错,只会静默写错数据;提示词给不了 schema 那样的硬边界。它够用的场景很有限:一次性脚本、快速原型、对格式宽容的展示用途。只要结果要进生产链路,就回到前两条路线——省下的不是几行容错代码,而是整类故障。

5、实战:发票字段提取器

把前面的零件拼成完整工具:从一段发票文字里抽出结构化账目,要求字段合法、数量为正、失败自动重试。先定义业务模型:

from typing import List
from pydantic import BaseModel, Field, ValidationError

class InvoiceItem(BaseModel):
    name: str
    qty: int = Field(gt=0)          # 业务规则:数量必须为正
    unit_price: float = Field(ge=0)

class Invoice(BaseModel):
    invoice_no: str
    date: str
    currency: str
    total: float
    items: List[InvoiceItem]

解读:qty 与 unit_price 的约束写在 Pydantic 里而不是工具 schema 里——第 2 节说过,数值区间不在服务端支持范围内,这类业务规则正适合放在客户端校验层。工具定义沿用第 3 节的格式,name 取 record_invoice,schema 换成发票字段并保持 strict: True,items 的元素结构同样写全 required 与 additionalProperties。主循环如下:

def extract_invoice(text: str) -> Invoice:
    messages = [{"role": "user", "content": f"提取发票字段:\n{text}"}]
    for _ in range(3):                       # 校验循环:最多三轮
        resp = client.messages.create(
            model="claude-haiku-5-5",        # 抽取类任务用轻量档
            max_tokens=1024,
            system="必须调用 record_invoice 工具完成提取。",
            tools=[INVOICE_TOOL],
            messages=messages,
        )
        blocks = [b for b in resp.content if b.type == "tool_use"]
        if not blocks:                       # 没触发调用:点名重试
            messages += [
                {"role": "assistant", "content": resp.content},
                {"role": "user", "content": "请调用 record_invoice 工具。"},
            ]
            continue
        try:
            return Invoice.model_validate(blocks[0].input)
        except ValidationError as err:       # 校验失败:带错误重试
            messages += [
                {"role": "assistant", "content": resp.content},
                {"role": "user", "content": f"校验失败:{err},请修正后重新调用。"},
            ]
    raise RuntimeError("三轮未得到合法发票数据")

解读:循环处理两类失败——auto 没触发调用时点名重试;strict 保证了 schema 形状,但 gt=0 这类业务规则仍可能不满足,此时把 ValidationError 原文回传,模型能看到具体错在哪一字段。抽取任务选 claude-haiku-5-5:第 1 篇的档位表里它的定位就是分类与抽取,输入单价只有均衡档的二十分之一。进循环前照例先看 stop_reason,max_tokens 截断的半截参数不值得送去校验;跑批时批处理接口与工具调用兼容,量大可以排队异步消化。三轮上限防止死循环,超限抛错交由上层处理。这个"调用、校验、反馈、重试"的骨架,把第 3 节的所有零件都串了起来。

6、总结

本篇把 Anthropic 结构化输出的 2026 年 10 月口径讲清了:原生模式已经上线,client.messages.parse() 传 Pydantic 模型、拿 parsed_output,是取 JSON 的第一选择;工具路线用 strict: True 保证参数形状,配合提示词点名与"检查后重试",替代已被当前代模型移除的强制 tool_choice;纯文本加 json.loads 只配做原型。三句话带走:要 JSON 用 parse,工具混用开 strict,生产别裸解析。

留三个练习:给第 2 节的 LeadInfo 加一个嵌套的 company 对象字段,观察 Pydantic 嵌套模型如何自动转成 schema;把第 5 节的发票提取改成 parse() 版本,去掉工具与重试循环,对同一批样例对比两条路线的耗时与字段错误率;再给 Invoice 加一条模型级校验——total 必须等于各行 qty 与 unit_price 乘积之和,体验比 schema 更进一步的业务规则怎么进重试循环。对于正在把模型输出接进数据库或分支逻辑的开发者,建议今天就把手头的 json.loads 换成 parse()——那一层"也许能解析"的不确定性,本就不该由你的代码承担。工具的另一面——真正执行函数并把结果回传给模型——是后续篇章的主题,本篇的 tool_use 解析已经把门票买好了。

相关推荐

获取系统SDK版本、判断手机ROM
精选Code

获取系统SDK版本、判断手机ROM

Build获取系统SDK版本Android中部分API的使用,需要在特定的SDK版本之后才能使用,因此在兼容老版本SDK的时候,经常需要判断API的版本。各种Android版本的对应关系参考《Android各版本对应的SDK版本》判断手机ROM有时候需要判断手机系统的ROM,检测ROM是MIUI、EMUI还是Flyme,可以使用getprop命令,去系统build.prop文件(关于build.p

4.2k
AndroidStudio中各种中文乱码问题
精选Code

AndroidStudio中各种中文乱码问题

AndroidStudio中各种中文乱码问题1、编译Java错误信息乱码在出现这个Annotation processors must be explicitly declared now...问题的时候,正好也发现AndroidStudio Build Output错误信息都乱码。经常遇到各种问题,习惯了,现在遇到的问题,都是以后的答案。 1.1、修改项目build.gradle(无效)在整个p

2.8k
PydanticAI 详解:类型安全的 Python AI SDK
精选AI

PydanticAI 详解:类型安全的 Python AI SDK

PydanticAI 详解:类型安全的 Python AI SDK写 LLM 应用最疼的两件事:模型返回的 JSON 不合预期、换一家模型商要改一串代码。pydantic/pydantic-ai(PydanticAI)用 Pydantic 团队的老本行回答了第一件——输出用 BaseModel 定义、运行前先校验,错误从运行时提前到编码时;第二件则用"模型即字符串"回答:换个厂商只改一个参数。如今

57