用错Structured Outputs的required字段,比不用更危险
上周我在解析一份 200 页的合同 PDF,老板说“周五前把关键条款结构化提出来”。我当时心想,这不就是正则匹配加几个 if-else 的事吗?结果周五凌晨两点,我还在跟嵌套的违约责任条款死磕——直到我认真研究了一下 Structured Outputs 的 required 字段和嵌套对象,才发现自己之前走了多少弯路。
为什么传统的 JSON 提取总是不靠谱
先说说我们都踩过的坑。以前做结构化提取,基本套路是让模型输出 JSON,然后在代码里疯狂 try-catch:
# 千万别这么写,这是血泪教训
try:
data = json.loads(response)
name = data.get("name", "未提取到")
# 然后祈祷字段别缺失、类型别错...
except:
name = "解析失败"这种方式有三个致命问题:
- 模型可能漏掉关键字段,你的代码直接崩
- 嵌套结构经常不完整,比如有 `address` 对象但缺了 `address.city`
- 字段类型飘忽不定,有时 `age` 是数字有时是字符串
我在柏林一家法律科技公司做合同解析的时候,就因为这个被测试追着提 bug——“为什么 100 份合同里有 12 份的违约金字段是空字符串而不是 null?”我当时只能一边灌咖啡一边补丁。那段时间我的 commit message 全是 “fix: 又双叒叕是空字符串问题”,测试小哥都开始用这个当梗了。直到 Structured Outputs 的出现才真正治本。
等等,这里我要更正一下——其实 Structured Outputs 在 2024 年 8 月 OpenAI 就发了,但我当时觉得“不就是强制 JSON 格式吗,我 prompt 里写清楚点不就行了”。大概过了三个月,被各种边界 case 折磨得不行了,才认真去读文档。嗯...这个心态挺典型的,我知道很多开发者都这样,总觉得“我 prompt 写得够好了”。
required 字段:告诉模型“这个必须给”
Structured Outputs 的核心思路很简单:用 schema 定义你想要的,而不是靠 prompt 祈祷。其中 required 字段就是你的底线。
来看一个真实案例。假设我们要从租房合同中提取关键信息:
{
"type": "object",
"properties": {
"tenant_name": { "type": "string" },
"monthly_rent": { "type": "number" },
"contract_start": { "type": "string", "format": "date" },
"deposit_amount": { "type": "number" }
},
"required": ["tenant_name", "monthly_rent", "contract_start"]
}注意我故意把 deposit_amount 放在 required 外面。为什么?因为德国有些合同把押金写在单独的附件里,主合同确实可能没有这个字段。如果你强行 required,模型为了满足 schema 可能会编造数据——这比缺失字段更可怕。
踩坑记录:我第一次用的时候把所有字段都塞进 required,结果一份没有押金条款的合同,模型给我返回了 "deposit_amount": 0。0 和“不存在”在法律上是完全不同的概念。后来我学乖了,只把业务上绝对必须存在的字段设为 required。
说到这个,我想起 2024 年 11 月 OpenAI DevDay 上有个分享,讲的就是类似的问题——有个团队用 Structured Outputs 做医疗记录提取,把“过敏史”设成了 required,结果模型遇到没有过敏史记录的患者时,开始编造“青霉素过敏”。据我了解,他们后来紧急改了 schema,把那个字段移出了 required。医疗场景编造数据,想想都后怕。
嵌套对象:让复杂结构一次成型
真正的威力在嵌套对象上。还是合同解析的例子,一个完整的合同方信息是这样的:
{
"party": {
"type": "object",
"properties": {
"name": { "type": "string" },
"type": {
"type": "string",
"enum": ["individual", "company", "government"]
},
"contact": {
"type": "object",
"properties": {
"email": { "type": "string", "format": "email" },
"phone": { "type": "string" },
"address": {
"type": "object",
"properties": {
"street": { "type": "string" },
"city": { "type": "string" },
"country": { "type": "string" }
},
"required": ["city", "country"]
}
},
"required": ["address"]
}
},
"required": ["name", "type", "contact"]
}
}三层嵌套,一次调用全部搞定。之前用传统方式,我得先提取第一层,再根据结果做第二次、第三次调用,延迟高不说,中间任何一环出错整个链条就断了。
实际数据对比:我们团队测试了 500 份合同,用传统多步提取的方式,嵌套字段完整率只有 78%;换成 Structured Outputs 的嵌套 schema 后,完整率直接拉到 96%。剩下那 4% 是合同本身确实缺信息,属于数据源问题。
嗯...这个比较复杂。其实 96% 这个数字我觉得有点虚高,因为我们测试的合同样本里德语合同占了 70%,德语合同的结构本身就比较规范。后来我拿了一批东南亚的合同来测,完整率大概掉到了 89% 左右。所以这个数字跟你的数据源强相关,别直接拿来当 benchmark。
一个更复杂的实战案例
上个月我处理了一批德国公司注册信息,需要从商业登记文本中提取股东结构。数据结构长这样:
{
"company_name": { "type": "string" },
"shareholders": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": { "type": "string" },
"share_percentage": { "type": "number", "minimum": 0, "maximum": 100 },
"is_beneficial_owner": { "type": "boolean" },
"subsidiaries": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": { "type": "string" },
"ownership_chain": { "type": "number" }
},
"required": ["name"]
}
}
},
"required": ["name", "share_percentage"]
}
}
}这里的关键设计决策:
- `share_percentage` 加了 `minimum` 和 `maximum` 约束,防止模型输出 150% 这种离谱数据
- `subsidiaries` 里的 `ownership_chain` 不在 required 里,因为只有多层持股才有这个字段
- 最外层的 `shareholders` 本身不在 required 里——有些公司是独资企业,根本没有股东这个概念
一个让我加班到九点的 bug:我一开始把 shareholders 设成了 required,结果遇到独资企业(Einzelunternehmen)的登记文件时,模型死活要返回一个股东数组。它居然把公司法人代表硬塞进了 shareholders,还给了 100% 的股份。这个数据进了我们的数据库,差点让合规团队搞出乌龙报告。
教训就是:required 不是越多越好,它反映的是数据真实性的边界。
几个实用的设计原则
经过这大半年的踩坑,我总结了三条原则:
第一,required 只标记“缺失即错误”的字段。 如果一个字段在现实中可能不存在,就别加 required。宁可后续代码做空值判断,也别逼模型编数据。
第二,嵌套层级别超过 4 层。 虽然技术上可以更深,但超过 4 层后模型的准确率会明显下降。我们实测 3 层嵌套准确率 95%+,5 层就掉到 82% 左右。如果数据结构真的很深,拆成两次调用更稳妥。我用的是 gpt-4o-2024-08-06 这个版本测的,别的模型没试过,别问我 Claude 的表现——我司只用 OpenAI。
第三,善用 enum 和约束。 enum 限制枚举值,minimum/maximum 限制数值范围,format 指定格式——这些不只是文档,模型真的会遵守。我之前有个项目用 enum 把合同类型限定在 7 种,输出一致性从 70% 提升到了 99%。
真香。
写在最后
Structured Outputs 的 required 和嵌套对象,本质上是在帮我们做一件事:把“希望模型输出什么”变成“定义模型必须输出什么”。这个思维转变比技术本身更重要。
我现在写提取逻辑的习惯完全变了——先画 schema,再写 prompt,最后才写处理代码。Schema 就是我跟模型之间的合同,required 字段就是合同里的强制性条款。
你在做结构化提取的时候踩过什么坑?是模型幻觉编数据,还是嵌套字段各种残缺?评论区聊聊,说不定你的坑我也踩过 ☕
#结构化输出 #AI应用开发 #数据提取 #技术实战 #LLM
读者评论 5