前面我们已经让模型按照 JSON Schema 返回结构化结果,但“能解析成 JSON”不等于“业务上可用”:年份可能超出范围,邮箱可能不是合法格式,必填字段也可能是空字符串。生产代码不能只相信模型的承诺,而要把输出当作不可信的外部输入,先校验,再决定是否接受。本篇只聚焦一个核心闭环:模型输出 → 程序校验 → 有限次修复 → 最终失败处理 。
结构正确不代表内容正确 模型输出至少有三层可靠性:第一层是传输成功,接口返回了响应;第二层是语法正确,例如文本可以被 json.loads 解析;第三层是符合业务约束,例如 score 必须是 0 到 100 的整数、email 必须像邮箱地址。
结构化输出主要帮助第二层,甚至可以帮助字段类型和必填项,但它不会替我们判断事实是否正确,也不会自动理解全部业务规则。因此,模型输出应当和用户输入、HTTP 请求一样,经过明确的边界校验。校验失败时,不能把错误数据继续传给数据库、邮件系统或下游工具。
用 Pydantic 声明可执行的规则 Pydantic 的模型既是文档,也是可执行的校验器。下面这个例子从一段文本中提取联系人信息,并额外约束姓名非空、年龄范围和邮箱格式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 from pydantic import BaseModel, ConfigDict, EmailStr, Fieldclass Contact (BaseModel ): model_config = ConfigDict(extra="forbid" ) name: str = Field(min_length=1 , max_length=50 ) age: int = Field(ge=0 , le=120 ) email: EmailStr raw = '{"name":"小林","age":28,"email":"lin@example.com"}' contact = Contact.model_validate_json(raw) print (contact.name, contact.email)
model_validate_json() 会先解析 JSON,再按照模型字段校验类型和约束;成功后得到的是 Contact 实例,而不是一段未经检查的字典。extra="forbid" 会拒绝未声明的额外字段,避免模型悄悄塞进程序没有考虑过的数据。运行示例前安装依赖:pip install pydantic email-validator。这里没有放任何密钥,校验逻辑可以脱离模型接口单独测试。
注意,类型转换不是万能的。Pydantic 可能把某些数字字符串转换为整数,但不会替你判断“这个人是否真的 28 岁”。格式校验和事实核验是两回事,后者需要数据库、规则或人工确认。
第一次失败:把错误变成明确反馈 调用模型后,不要直接访问 data["name"]。先捕获校验异常,并把结构化的错误信息整理成下一次请求能理解的反馈:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import jsonfrom pydantic import ValidationErrordef validate_contact (raw: str ) -> Contact: try : return Contact.model_validate_json(raw) except ValidationError as exc: errors = [ {"field" : "." .join(str (x) for x in item["loc" ]), "message" : item["msg" ]} for item in exc.errors() ] raise ValueError(json.dumps(errors, ensure_ascii=False )) from exc
错误信息应该只包含字段路径和规则,不要把整段敏感原文无条件写入日志或重新发送。对于模型来说,“输出必须是 JSON”太宽泛;“age 必须是 0 到 120 的整数,email 必须是有效邮箱”才是可执行的修复指令。
自动修复必须有上限 修复不是让模型无限循环重试,而是把上一次的输出和校验错误一起交给模型,请它只改错处。下面的完整示例使用 OpenAI Python SDK 的 Chat Completions 接口;模型、密钥和兼容服务地址全部来自环境变量。它展示流程,实际使用前仍应确认所选模型和服务商支持相应接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 import jsonimport osfrom openai import OpenAIfrom pydantic import BaseModel, ConfigDict, EmailStr, Field, ValidationErrorclass Contact (BaseModel ): model_config = ConfigDict(extra="forbid" ) name: str = Field(min_length=1 , max_length=50 ) age: int = Field(ge=0 , le=120 ) email: EmailStr client = OpenAI( api_key=os.environ["OPENAI_API_KEY" ], base_url=os.environ.get("OPENAI_BASE_URL" ), ) model = os.environ.get("MODEL_NAME" , "gpt-4o-mini" ) def ask (text: str , previous: str | None = None , errors: list [dict ] | None = None ) -> str : instruction = ( "从用户文本提取 name、age、email,只返回一个 JSON 对象," "不要 Markdown,不要解释。" ) if previous and errors: instruction += ( f"\n上次 JSON 是:{previous} \n校验错误是:" f"{json.dumps(errors, ensure_ascii=False )} \n只修复这些错误。" ) response = client.chat.completions.create( model=model, messages=[ {"role" : "system" , "content" : instruction}, {"role" : "user" , "content" : text}, ], response_format={"type" : "json_object" }, ) return response.choices[0 ].message.content or "" def extract_contact (text: str , max_attempts: int = 2 ) -> Contact: raw = ask(text) for attempt in range (max_attempts + 1 ): try : return Contact.model_validate_json(raw) except (ValidationError, ValueError) as exc: if attempt == max_attempts: raise RuntimeError("模型输出连续校验失败" ) from exc if isinstance (exc, ValidationError): errors = exc.errors() else : errors = [{"loc" : [], "msg" : "不是合法 JSON" }] raw = ask(text, previous=raw, errors=errors) raise AssertionError("unreachable" ) if __name__ == "__main__" : print (extract_contact("小林 28 岁,邮箱 lin@example.com" ))
这里的 max_attempts=2 表示初次生成后最多再修复两次。每次修复都可能消耗 token、增加延迟,且模型可能把一个错误改成另一个错误,所以必须设置上限。达到上限后应记录请求标识和校验摘要,返回可控的失败结果或进入人工处理,绝不能把最后一段原始文本当成成功数据。
何时不该自动修复 有些失败不是格式问题。模型拒答、响应为空、内容被截断、接口超时,都应该分别记录并走对应的错误处理;把它们统统塞给“修复提示词”只会掩盖真正原因。涉及转账、删除数据、发送外部消息等高风险动作时,即使校验通过,也应增加业务规则和人工审批,不能因为 JSON 合法就自动执行。
还要区分“可修复字段”和“不可猜字段”。如果输入没有提供年龄,模型不应凭常识补一个数字。可以把字段改成可选类型,要求缺失时返回 null,再由业务层决定是否继续。校验器负责发现问题,不能凭空创造事实。
常见问题 为什么已经使用 JSON Mode 还要校验? JSON Mode 只解决语法层面,通常不保证字段、范围和业务含义。它不能替代 Pydantic 或其他程序校验。
修复时能否只把错误消息发给模型? 最好同时提供原始 JSON、字段规则和错误位置,否则模型不知道要在什么上下文中修改。发送前应脱敏,并限制输入长度。
应该重试多少次? 交互式场景通常初次生成加一到两次修复即可。若连续失败,问题可能是提示词、Schema 或输入本身,而不是“再试一次”能解决的。
EmailStr 报缺少依赖怎么办? 安装 email-validator。依赖缺失属于部署问题,应在环境构建或启动检查阶段暴露,而不是运行到用户请求时才发现。
小结
模型返回成功、JSON 合法、符合业务规则,是三个不同层次,必须逐层验证。
用 Pydantic 把类型、范围、必填和额外字段规则写成可执行约束。
校验失败时可以把错误反馈给模型进行有限次修复,但必须设置次数上限并准备失败分支。
校验通过不等于事实真实,更不等于可以执行高风险操作;关键数据仍需业务核验和审批。
下一步将学习提示词版本管理与基础测试,让这些约束能够持续回归,而不是改一次提示词就重新冒险。