OpenAI Agents SDK 工程笔记:AgentOutputSchema 结构化输出声明、strict 校验失败与生产禁区

很多团队在 Agent 上写了 output_type=某个 Pydantic 模型,就默认终稿一定是结构化对象。声明期那一层 AgentOutputSchema 才管这件事。它生成 JSON Schema,再在模型返回后做 validate_json。跑完再用别的方法强转,是另一条路径,本文不展开。

先说清本机核对结果。openai-agents 0.23.1 里,Agent 的 output_type 可以是普通类型、AgentOutputSchemaBase 子类,或 None。你传入 Pydantic 模型时,SDK 会包成 AgentOutputSchema,构造参数里的 strict_json_schema 默认为 True。官方注释写得很硬,强烈建议保持开启。校验失败抛的是 ModelBehaviorError,不是静默的 None。标题里的 strict,指的就是这个开关。非对象类型(例如 list[int])会被包进带 response 键的字典再出 schema,这点和标题简写不完全一样,写进审计时要写清包装键。

离线 smoke 在 _w/output-schema-smoke/,不连模型接口。好路径跑 P0 到 P4,反例种植 F1 到 F4。

图注 左侧为声明期必须守住的校验面,右侧为 smoke 里种植的四类假成功。

目标说明

读完你应能做到四件事。

  1. 在 Agent 上正确声明 output_type 或显式传入 AgentOutputSchema。
  2. 分清 strict 开启时哪些返回会变成 ModelBehaviorError。
  3. 拒绝只用 json.loads 判断结构化是否成功的审计口径。
  4. 把校验异常上抛并记账,不吞成空对象。

声明期和跑后期差在哪

声明期做两件事。构造 schema,把模型输出约束成可校验的形状。模型一返回字符串,立刻 validate_json,失败就中断本轮。

跑后期强转是另一条故事。你已经拿到自然语言或半成品,再手工解析。那条路径适合补救与迁移,不适合当生产默认。生产默认应在 Agent 创建时就把结构钉死,让失败发生在 SDK 边界,而不是业务代码里随手 dict.get。

我在本机用一个最小的 Ticket 模型走了一遍构造与校验。合法样本能拿到 order_id 和整型的 amount_cents。三条坏样本分别是自然语言整句、缺少 order_id、把 amount_cents 写成字符串。三条都在 validate_json 处变成 ModelBehaviorError。这个结果和文档一致,也说明标题里写的校验失败有明确的异常类型可抓。

python 复制代码
from pydantic import BaseModel
from agents import Agent, AgentOutputSchema

class Ticket(BaseModel):
    order_id: str
    amount_cents: int
    currency: str = "CNY"

# 推荐:直接传类型,走默认 strict=True
agent = Agent(name="billing", instructions="...", output_type=Ticket)

# 需要非 strict 时显式包一层(官方不推荐当默认)
loose = AgentOutputSchema(Ticket, strict_json_schema=False)
agent2 = Agent(name="billing_loose", instructions="...", output_type=loose)

strict_json_schema=True 时,schema 会经过 ensure_strict_json_schema。类型本身不满足严格约束,构造阶段就会抛 UserError,提示你改类型,或显式关掉 strict。这比跑起来再炸更早暴露问题。

离线 smoke 结果

text 复制代码
P0  PASS strict schema 可生成 -> required=['order_id', 'amount_cents', 'currency']
P1  PASS 合法 JSON 通过校验 -> order_id=A1004 cents=135700
P2  PASS 自然语言拒结构化应抛 ModelBehaviorError
P3  PASS 缺 order_id 应失败 -> accepted=False
P4  PASS amount_cents 字符串应失败 -> ModelBehaviorError
F1  FAIL 种植:把自然语言复述当已过 schema -> 假成功 keys=['reply']
F2  FAIL 种植:宽松 loads 收下缺必填 -> keys=['amount_cents']
F3  FAIL 种植:类型漂移仍当 dict 收下 -> amount_cents='1357.00'
F4  FAIL 种植:校验异常吞成 {} -> result={}

schema 快照在 schema_snapshot.json,失败表在 output_schema_fail.csv。反例函数只有二十行,刻意和好路径并排放,方便对照。

四类失败样本拆解

F1 自然语言被记成结构化成功。 模型回了一句「订单 A1004 金额一千三百五十七元」。好路径 validate_json 直接抛 ModelBehaviorError。反例把这段话塞进带 reply 字段的对象,只做 json.loads,审计日志里写成结构化已通过。线上告警按成功率算,这块永远是绿的,账单系统却收不到 order_id。

F2 缺必填被宽松收下。 原始串是 {"amount_cents": 100},没有 order_id。好路径拒绝。反例 loads 之后当 dict 继续往下游传,下游用 .get("order_id") 得到 None,再拼进 SQL 或缓存键,事故会晚几个小时才爆。

F3 类型漂移。 amount_cents 变成了字符串 "1357.00"。strict 校验按整数约束拒绝。反例照单全收。财务对账按分计算时,字符串和整数混加,或者被静默转成浮点,三月汇总就会偏出一分钱到几块钱不等。业务里常见的「金额字段看起来像数字」都落在这一类。

F4 校验异常吞成空对象。 反例包了一层 try/except ModelBehaviorError: return {}。调用方看到非空返回,监控看不到异常率,值班的人只能在三天后的空账单里找线索。正确做法是让异常冒泡,或在边界转成带错误码的领域异常,并打一条带 schema 版本号的日志。

生产禁区表

编号 禁区 smoke 证据 替代做法
D1 把自然语言复述记成已通过 schema F1 只认 validate_json 成功后的对象
D2 用 json.loads 代替 schema 校验 F2 统一走 AgentOutputSchema.validate_json
D3 类型漂移当成功样本进审计 F3 strict 开启,漂移直接失败
D4 except 后返回 None 或 {} F4 上抛或转领域错误,并计数告警
D5 改字段不版本化 schema (构造对比) schema 快照入库,变更记版本

和 final_output_as 怎么分工

声明期负责「这一轮必须长什么样」。final_output_as 一类跑后强转,负责你已经拿到结果之后的二次解释。两套都做校验可以,但生产主路径应以前者为准。主路径失败时,不要悄悄落到「把整段文本塞进某个字段」的兜底,除非产品明确要求降级,并且降级事件可观测。

跨版本改字段时,旧 schema 快照要留着。smoke 里把 json_schema() 写进 schema_snapshot.json,就是为了让 CI 能 diff。字段改名却共用一个审计表,是另一类慢性事故。改 schema 等于改契约,PR 里应能看见快照差异。

有人会问,关掉 strict 是不是更灵活。灵活的代价是模型更容易返回「大体像那么回事」的对象。演示环境可以关,生产默认建议开。若某个字段确实无法满足严格 JSON Schema 约束,优先改模型定义,其次才是对该 Agent 单独包一层 strict_json_schema=False,并在监控里给这个 Agent 打上宽松标记。

预发检查

  1. AgentOutputSchema(YourModel, strict_json_schema=True) 能构造成功,失败则先改模型定义。
  2. 准备三条坏样本(自然语言、缺必填、类型漂移),断言都抛 ModelBehaviorError。
  3. 业务边界禁止裸 except Exception 后返回空 dict。
  4. 把 schema 快照提交到仓库,PR 里能看见字段变更。
  5. 监控单独统计 ModelBehaviorError 次数,与业务成功率分开看。

五条都能在本目录的 smoke.py 里找到对应写法。换成你的票据、工单、报告模型时,只改 Ticket 定义和样本 JSON。

一次真实的对接顺序

我建议新项目按这个顺序接,而不是先写业务再补校验。

第一步,把终稿对象写成 Pydantic 模型,字段名与下游 API 对齐,单位写进字段名(例如 amount_cents),避免元和分混用。第二步,用 AgentOutputSchema 打一份 schema 快照,提交进库。第三步,写四条单测,覆盖本文 F1 到 F4 的形状。第四步,才把 Agent 接到 Runner。这样上线第一周,你看到的异常会集中在模型不守结构,而不是下游空指针。

若团队已经有大量「先 loads 再 .get」的代码,迁移时可以双跑一段时间。一边走 schema 校验,一边保留旧解析,对比两者不一致的比例。不一致的样本逐条归类,缺字段、类型错、多出来的叙述字段,三类分开看。比例降到可接受再撤旧路径。

监控和告警怎么挂

结构化失败和业务失败要分开看。ModelBehaviorError 升高,通常是提示词漂移、模型换代或下游字段变更,不该和「用户没填完整」混在一个错误率里。我习惯至少拆三条序列。

  1. schema 构造失败(UserError),发版期就该为零。
  2. validate_json 失败次数,按 Agent 名和 schema 版本号分组。
  3. 业务侧在拿到已校验对象之后仍失败的次数。

第三条才是真正的业务问题。前两条是契约问题。混在一起时,值班的人会先去翻订单表,浪费半小时。

演示环境可以短期关掉 strict,方便看模型「原生态」返回。切回生产配置时,用同一份坏样本套件再跑一遍。若演示环境养成了吞异常的习惯,同步到生产会非常快。smoke 目录里的 F4 就是专门挡这件事的。

字段变更时的双写窗口

下游若已经按旧字段消费,新 schema 不要在同一天硬切。可以让模型同时输出新旧两套字段,校验仍按新 schema 的必填来,旧字段只读不写新逻辑。窗口期结束,再从 schema 里删掉旧字段,并更新快照。

双写窗口里最容易犯的错,是校验只检查新字段,审计却还在报旧字段的成功率。两套口径并存一周,看起来都绿,实际上有一半流量已经读不到新字段。smoke 里把 required 列表打进 CSV,就是为了让「当前契约」有一份可 diff 的真相。

上线后第一周,把 ModelBehaviorError 按提示词版本分组看。若某一版提示词失败率明显高于其他版本,优先回滚提示词,而不是先怪模型。结构化输出对提示词里的「请用 JSON」依赖很低,因为 schema 已在 API 侧约束。真正影响失败率的,往往是必填字段太多、字段语义含糊,或枚举值与业务口语不一致。把枚举写进模型的 Literal,比在提示词里用自然语言列举更稳。

若你在多语言仓库里用同一套 schema,注意字段说明与枚举值的语言一致性。模型按英文枚举训练更稳时,就让 schema 用英文枚举,展示层再翻译。中英混用的枚举是类型漂移之外的另一类慢性病,strict 也救不了语义错位。

联调时若上游坚持返回额外叙述字段,不要为了迁就它关掉整个 strict。可以在模型里增加可选字段承接叙述,必填字段仍严格。多余字段进可选,总好过把校验面拆掉。

小结

AgentOutputSchema 的价值在声明期把失败提前。strict 默认开启是有意的。假成功几乎都来自只做了 loads,以及异常被吞。把校验面当成生产边界来守,终稿结构才站得住。本机 0.23.1 的类名与异常类型如上,升级 SDK 后请先复跑 _w/output-schema-smoke/smoke.py。

相关推荐
ZGi.ai20 小时前
Agent 输出格式总变,工作流怎么接?
工作流·数据校验·aiagent·结构化输出·zgi·agentruntime
爱编程的小新☆1 天前
LangChain 智能体实战:构建技术学习规划 Agent
langchain·agent·工具调用·结构化输出·deepseek·流式输出
AIGC大时代3 天前
OpenAI Agents SDK 工程笔记:TraceProcessor 导出、敏感字段脱敏与生产禁区
脱敏·tracing·agents sdk·生产禁区·traceprocessor
AIGC大时代3 天前
OpenAI Agents SDK 工程笔记:自定义 Session 接口(get_items / add_items / clear)与多租户隔离的生产禁区
session·多租户·agents sdk·生产禁区·get_items
xrlfreedom10 天前
MCP 面试高频考点:把知识库 RAG 检索封装为 Tool,Docker 与结构化输出怎么配合不踩坑
docker·结构化输出·mcp
死磕音视频11 天前
让大模型稳定输出 JSON:结构化输出的三种方法与 Python 校验实践
json·大模型api·pydantic·结构化输出
AIGC大时代13 天前
OpenAI Agents SDK 工程笔记:SQLiteSession 多轮会话持久化与生产禁区
agents sdk·生产禁区·sqlitesession·会话持久化
AIGC大时代13 天前
OpenAI Agents SDK 工程笔记:AgentsException 族、失败分类与生产恢复禁区
agents sdk·agentsexception·maxturns·behaviorerror
AIGC大时代13 天前
OpenAI Agents SDK 工程笔记:WebSearchTool 与 FileSearchTool 托管工具边界与生产禁区
agents sdk·websearchtool·filesearchtool·托管检索