AI 输出 JSON 偶尔多出 Markdown 围栏怎么做容错解析
来源:17golang原创
时间:2026-09-08 20:46:56 398浏览 收藏
调用大模型生成结构化数据时,最常见的解析故障不是 JSON 语法本身,而是模型在对象外面加了一层 Markdown 代码围栏:```json、JSON 内容、```。处理方式是把围栏当作传输包装,仅在边界处剥离,然后交给 encoding/json;不要对整段文本做全局替换。
可靠的容错解析要分三层:先判断响应是否拒答或截断,再只清理最外层围栏,最后执行 JSON 解码和业务字段校验。这样即使模型偶尔加了 Markdown,也不会把字符串值里的合法字符误删。
- 只删除首尾成对的围栏,不删除 JSON 字符串内部的反引号。
- Structured Outputs 能约束 JSON Schema,但拒答和不完整响应仍要单独判断。
- JSON 解码成功不等于业务数据可用,字段、枚举和空值要继续校验。
先把围栏当作传输包装,而不是 JSON 内容
如果直接把 ```json\n{...}\n``` 交给 json.Unmarshal,开头的反引号会让解码失败。最小修复是先做边界清理:去掉首尾空白和 UTF-8 BOM,确认第一行是围栏,再确认最后一行是单独的结束围栏。不要使用 strings.ReplaceAll(raw, "```", ""),因为字段值本身可能合法地包含反引号。
下面的函数只负责“去包装”,不负责猜测缺失逗号、补引号或截取半个对象。解析失败时保留原始错误,调用方才能知道是围栏问题还是模型输出了不完整 JSON。
package responseparse
import (
"bytes"
"encoding/json"
"fmt"
"strings"
)
// unwrapJSONFence 只移除最外层、成对出现的 Markdown 围栏。
func unwrapJSONFence(raw string) (string, error) {
// BOM 和外围空白属于传输噪声,不改变 JSON 字符串内部内容。
text := strings.TrimSpace(strings.TrimPrefix(raw, "\ufeff"))
if !strings.HasPrefix(text, "```") {
return text, nil
}
// 第一行只允许是 ``` 或 ```json 这类语言提示。
firstBreak := strings.IndexByte(text, '\n')
if firstBreak 160 {
preview = preview[:160]
}
return fmt.Errorf("decode model JSON near %q: %w", preview, err)
}
return nil
}
这个边界有两个故意的“严格”:只接受明确的首行和尾行,不替模型修复半截 JSON;只把清理后的文本交给标准库,不用宽松正则代替语法分析。生产环境若还要兼容 ```JSON 大小写,可在确认协议后扩展允许列表,而不是无限放宽。

解析成功不等于业务结果有效

围栏只是文本层问题。实际接入 API 时,还要先区分三种状态:模型明确拒答、响应因长度或内容过滤而不完整、响应完成但 JSON 语法错误。对于支持 Structured Outputs 的接口,JSON Schema 可以减少缺键和非法枚举,但官方示例仍要求程序检查拒答与 incomplete 状态;这两个分支都不应该继续调用 json.Unmarshal。
可以把供应商响应转换成应用自己的结果类型,再做一次字段检查。示例中的 Refusal 和 Incomplete 只是应用层状态,具体字段名要按所用 SDK 的响应结构映射,不要把某一家 API 的字段名硬编码到所有供应商适配器。
type Answer struct {
Action string `json:"action"`
Score int `json:"score"`
}
type ModelResult struct {
Text string
Refusal string
Incomplete bool
}
func DecodeAnswer(result ModelResult) (Answer, error) {
// 拒答和截断不是 JSON 解析错误,分别交给策略层处理。
if result.Refusal != "" {
return Answer{}, fmt.Errorf("model refusal: %s", result.Refusal)
}
if result.Incomplete {
return Answer{}, fmt.Errorf("model response is incomplete")
}
var answer Answer
if err := DecodeModelJSON(result.Text, &answer); err != nil {
return Answer{}, err
}
// 格式合法之后,再判断业务字段是否满足本地契约。
if answer.Action == "" || answer.Score 100 {
return Answer{}, fmt.Errorf("answer fields are out of range")
}
return answer, nil
}
如果是普通文本模式,容错函数可以接住围栏;如果是 Structured Outputs,仍建议保留这层防御,因为上游可能切换模型、SDK 或降级路径。尤其不要把“解析成功”直接当成“可以执行动作”:涉及发消息、写库或调用工具时,字段校验和业务授权必须独立存在。
用检查表决定重试还是落库
| 现象 | 判断层 | 处理建议 |
|---|---|---|
| 首尾是成对围栏,内部 JSON 合法 | 传输包装 | 剥离后正常解码 |
| 拒答字段有值 | 模型状态 | 记录原因,不重试同一请求 |
| 响应 incomplete 或缺少结束标记 | 响应完整性 | 按额度和幂等策略决定重试 |
| JSON 合法但 action 为空 | 业务契约 | 拒绝进入后续副作用操作 |
日志中可以记录解析阶段、错误类型和请求关联 ID;原始输出可能含用户隐私或提示词,不建议默认完整落盘。重试也要有上限,并使用幂等键,避免一次截断触发多次业务动作。
常见问题
能不能直接把所有 ``` 删除?
不建议。全局替换可能破坏 JSON 字符串中的反引号,也会掩盖围栏未闭合的问题。只处理首行和尾行更容易定位故障。
Structured Outputs 还需要自己解析吗?
需要保留响应状态判断和业务校验;在普通文本或降级模型路径中,还要保留最外层围栏清理与标准 JSON 解码。
JSON 解码失败要不要自动重试?
先区分截断、拒答、围栏错误和真正的语法错误,再按请求成本、幂等性和重试上限决定。不要对同一份未改变的输出无限重试。
把围栏清理、响应状态判断、JSON 解码和业务校验拆开,容错逻辑就有了清晰边界:能修复的只是传输包装,不能替模型或业务规则“猜答案”。
-
501 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
259 收藏
-
259 收藏
-
433 收藏
-
307 收藏
-
281 收藏
-
462 收藏
-
132 收藏
-
244 收藏
-
215 收藏
-
科技周边 · 人工智能 | 14小时前 | 人工智能 · rag · 模型评测 · RAG 召回率 evaluation Context Recall Answer Correctness 答案正确率293 收藏
-
203 收藏
-
科技周边 · 人工智能 | 16小时前 | 人工智能 · ai agent · json schema · 工程实践 · 函数调用 · 参数校验 AI Agent JSON Schema 工具调用 tool use316 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习