结构化输出校验失败时应用层怎么保留原始响应
来源:17golang原创
时间:2026-09-10 09:43:01 324浏览 收藏
结构化输出并不等于应用层可以只保存一个解析后的对象。模型可能拒答,生成也可能在达到上限时提前结束;即使拿到了合法 JSON,字段值仍可能不符合业务规则。若代码只记录“校验失败”,下一次排查时既不知道供应商返回了什么,也无法判断该不该重试。
更稳妥的做法是:请求刚返回就记录请求元数据、状态字段和脱敏后的原始响应;随后依次区分拒答或截断、JSON 解析失败、Schema 校验失败和业务校验失败。原文用于诊断,结构化对象用于业务,两者不要互相替代。
- 保存原始响应的摘要、长度和哈希,必要时再保存受控原文。
- 先判断 refusal、incomplete、finish_reason,再尝试解析 content。
- 结构错误、Schema 错误和业务错误分别决定告警、修复或重试。
先把原始响应留在请求边界内
应用层至少应保留四类信息:供应商 request id、模型与 schema 版本、响应状态字段,以及脱敏后的原始响应。日志不必无条件写入完整 prompt;可以保存原文长度、SHA-256、前后截取片段和对象存储引用,让值班人员先确认“是不是同一份响应”。

下面的示例用一个归一化后的响应字典表示不同供应商的共同字段。真正接入 SDK 时,先把 SDK 对象转换成这样的内部记录。
import hashlib
import json
def snapshot_response(raw_response: dict) -> dict:
# 只保存诊断需要的元数据;生产环境还应在这里做字段脱敏。
raw_text = json.dumps(raw_response, ensure_ascii=False, sort_keys=True)
return {
"request_id": raw_response.get("id", "unknown"),
"model": raw_response.get("model", "unknown"),
"raw_length": len(raw_text),
"raw_sha256": hashlib.sha256(raw_text.encode("utf-8")).hexdigest(),
# 原文进入受控存储,不直接拼进普通业务日志。
"raw_excerpt": raw_text[:1200],
}
拒答和截断要先于 JSON 解析判断
结构化输出的“校验失败”不一定来自 JSON。若响应带有 refusal,应把它记录为模型拒答;若完成原因表示长度耗尽或 Responses API 状态为 incomplete,则更像一次不完整生成。两种情况都不适合直接把空内容交给 JSON 解析器,否则日志会被一个次生的“JSONDecodeError”带偏。
| 观察到的证据 | 应用层分类 | 处理建议 |
|---|---|---|
| refusal 有值 | 拒答 | 记录拒答文本,按业务决定提示用户或换任务 |
| incomplete 或 finish_reason 非正常结束 | 截断/未完成 | 保留原文,检查 token 上限后再有限重试 |
| content 不是合法 JSON | 结构层失败 | 记录解析位置,检查协议适配和响应原文 |
| JSON 合法但字段不满足约束 | Schema/业务失败 | 保留对象和错误路径,不要盲目重发 |
把失败分成三层,重试策略才不会混乱
建议把检查拆成三层。第一层是响应状态:HTTP、refusal、incomplete 和 content 是否存在;第二层是结构:字符串能否解析成 JSON、字段类型是否满足 Schema;第三层是业务:例如订单抽取结果中的金额是否为非负数、日期是否落在允许范围。每一层都写入同一条诊断记录,但错误代码不要混用。

def classify_structured_response(response: dict) -> dict:
# 先判状态,再判结构,避免把拒答或截断误报成 JSON 错误。
choice = (response.get("choices") or [{}])[0]
message = choice.get("message") or {}
if message.get("refusal"):
return {"kind": "refusal", "retryable": False, "detail": message["refusal"]}
if response.get("status") == "incomplete" or choice.get("finish_reason") not in (None, "stop"):
return {"kind": "incomplete", "retryable": True, "detail": choice.get("finish_reason")}
content = message.get("content")
if not isinstance(content, str):
return {"kind": "missing-content", "retryable": False, "detail": "content is absent"}
try:
value = json.loads(content)
except json.JSONDecodeError as exc:
# 保留错误位置;原文摘要已经在请求边界记录,不在此处重复打印全文。
return {"kind": "json-parse", "retryable": True, "detail": {"pos": exc.pos}}
return {"kind": "json-ready", "retryable": False, "value": value}
让日志既能重放,又不变成敏感数据仓库
保留原始响应时要同时做三件事:为 prompt、用户输入、邮箱、手机号和 token 做脱敏;给每份原文设置长度上限和过期时间;把 request id、schema 版本、错误路径、哈希和重试次数放到结构化字段中。这样开发者能用哈希把应用日志与受控原文对上,却不会让普通日志检索系统承载整段上下文。
重试也要有边界:截断可以在提高输出上限或缩短输入后重试一次;协议解析失败可以检查 SDK 适配;业务校验失败通常应进入人工或补偿队列;拒答则不应靠无限重发规避。最后把诊断记录和业务结果分开存储,避免一次脏响应覆盖上一条成功结果。
常见问题
只保存解析后的 Pydantic 或 Zod 对象可以吗?
不建议。成功对象适合业务使用,但失败时它根本不存在;至少要保存 request id、状态字段、错误路径和原文摘要。
Schema 校验失败要不要立刻重试?
先看失败层次。截断或临时传输异常可以有限重试,业务规则不满足时应先修正输入或进入补偿流程。
为什么要保存原文哈希?
哈希能在不扩散完整响应的前提下确认日志、对象存储和告警引用的是同一份数据,也方便后续去重。
真正可维护的结构化输出链路,不是“让模型永远不出错”,而是让每次异常都留下足够证据:发生在哪一层、原响应是哪一份、下一步是修请求、改 Schema、重试,还是交给业务处理。
-
463 收藏
-
474 收藏
-
387 收藏
-
314 收藏
-
155 收藏
-
260 收藏
-
437 收藏
-
392 收藏
-
277 收藏
-
科技周边 · 人工智能 | 1天前 | openai · function calling · 结构化输出 · Responses API · OpenAI JSON Schema 工具调用 Responses API Structured Outputs274 收藏
-
192 收藏
-
386 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习