登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  科技周边 >  人工智能

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 大小写,可在确认协议后扩展允许列表,而不是无限放宽。

AI JSON 围栏容错解析中的模型文本、外层 Markdown 围栏、Go 去包装器、JSON 解码器和业务对象静态关系
图1:围绕解析边界查看模型文本与外层围栏,去包装器只把干净 JSON 交给解码器。

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

帮助读者区分 Structured Outputs schema、拒答、截断、围栏文本、解码结果和业务校验的静态责任边界。
图2:把模型响应状态、JSON 解码和字段级校验分成独立边界,避免把格式正确当成业务可执行。

围栏只是文本层问题。实际接入 API 时,还要先区分三种状态:模型明确拒答、响应因长度或内容过滤而不完整、响应完成但 JSON 语法错误。对于支持 Structured Outputs 的接口,JSON Schema 可以减少缺键和非法枚举,但官方示例仍要求程序检查拒答与 incomplete 状态;这两个分支都不应该继续调用 json.Unmarshal

可以把供应商响应转换成应用自己的结果类型,再做一次字段检查。示例中的 RefusalIncomplete 只是应用层状态,具体字段名要按所用 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 解码和业务校验拆开,容错逻辑就有了清晰边界:能修复的只是传输包装,不能替模型或业务规则“猜答案”。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>