☰
Go AI Agent 实战 06|JSON 结构化输出与 Go 后端校验
2026/10/22 7:27:59 网站建设 项目流程

本系列共 10 阶段 ,面向有 Go 后端基础的开发者,全程用 Go 手写实现。
当前进度:阶段一 大模型基础 · 第 6 课
上一篇:Temperature 与生成参数

前面我们学习了 Prompt、消息角色和模型生成参数。现在进入 AI 应用开发中非常重要的一项能力:让大模型返回 Go 程序能够可靠处理的数据,而不仅仅是一段自然语言。

这一课会开始把 AI 和传统 Go 后端工程连接起来,为后面的 Tool Calling 做准备。

一、本课学习目标

学完之后,你应该能够:

  1. 理解普通文本输出与 JSON 结构化输出的区别。
  2. 使用 Go 的encoding/json解析模型返回的数据。
  3. 区分 JSON 语法正确、字段合法和业务逻辑正确。
  4. 处理模型返回格式错误、缺少字段和非法枚举值等问题。
  5. 理解为什么真实项目中应优先使用模型服务支持的结构化输出能力,而不能只依赖 Prompt。

二、为什么需要结构化输出?

假设我们正在开发一个 Go AI 技术助手,用户问:

帮我判断这段问题属于什么技术领域,并给出简短总结:Redis 缓存穿透是什么?

如果模型返回普通文本:

这个问题属于 Redis 技术领域。 缓存穿透指查询一个不存在的数据,导致请求绕过缓存,频繁访问数据库。

人类可以直接理解,但 Go 程序如果想单独获取技术领域、难度和总结,就需要进一步解析文本。

如果模型返回 JSON:

{"category":"redis","difficulty":"beginner","summary":"缓存穿透是指请求的数据不在缓存和数据库中,导致请求持续访问数据库。"}

Go 程序就可以把它解析为结构体,并将字段用于业务逻辑。

例如:

  • category用于分类统计。
  • difficulty用于决定解释深度。
  • summary用于页面展示。
  • 校验通过后,可以将结果存入数据库。

这就是结构化输出的价值:让模型输出的数据能够被程序稳定地消费。

三、必须区分的三个层次

JSON 结构化输出不只是"让模型输出大括号"。

第一层:JSON 语法正确

例如双引号、逗号、括号等符合 JSON 语法。Go 能否解析它?

第二层:数据结构合法

必须包含需要的字段,字段类型正确,枚举值在允许范围内。

第三层:业务逻辑正确

即使 JSON 格式正确,内容也可能是错的。例如把"缓存穿透"错误地归类为"网络协议"。

这三层是逐步递进的。

json.Unmarshal主要负责解析 JSON 和转换数据类型,它不会替你判断业务结论是否正确。

四、Go 实战:解析模型返回的 JSON

我们先不接入真实模型 API,而是用模拟响应验证 Go 后端的处理流程。

1. 创建项目

mkdirgo-json-outputcdgo-json-output go mod init example.com/go-json-output

新建main.go,写入以下完整代码:

packagemainimport("encoding/json""errors""fmt""strings")typeAIResultstruct{Categorystring`json:"category"`Difficultystring`json:"difficulty"`Summarystring`json:"summary"`}// 校验模型返回的数据是否符合业务要求。funcValidateAIResult(result AIResult)error{ifstrings.TrimSpace(result.Category)==""{returnerrors.New("category 不能为空")}ifstrings.TrimSpace(result.Summary)==""{returnerrors.New("summary 不能为空")}switchresult.Category{case"redis","go","mysql","network":// 合法分类default:returnfmt.Errorf("不支持的 category: %q",result.Category)}switchresult.Difficulty{case"beginner","intermediate","advanced":// 合法难度default:returnfmt.Errorf("不支持的 difficulty: %q",result.Difficulty)}returnnil}funcParseAIResult(rawstring)(AIResult,error){varresult AIResult// 第一层:解析 JSON。iferr:=json.Unmarshal([]byte(raw),&result);err!=nil{returnAIResult{},fmt.Errorf("解析 JSON 失败: %w",err)}// 第二层:校验字段与业务约束。iferr:=ValidateAIResult(result);err!=nil{returnAIResult{},fmt.Errorf("结果校验失败: %w",err)}returnresult,nil}funcmain(){// 模拟模型返回的数据,不是真实模型 API 响应。raw:=`{ "category": "redis", "difficulty": "beginner", "summary": "缓存穿透是指请求的数据在缓存和数据库中都不存在。" }`result,err:=ParseAIResult(raw)iferr!=nil{fmt.Println("处理模型响应失败:",err)return}fmt.Println("解析成功")fmt.Println("技术领域:",result.Category)fmt.Println("难度:",result.Difficulty)fmt.Println("总结:",result.Summary)}

运行:

go run.

预期输出:

解析成功 技术领域: redis 难度: beginner 总结: 缓存穿透是指请求的数据在缓存和数据库中都不存在。

2. 逐段解释关键代码

第一步:定义响应结构体

typeAIResultstruct{Categorystring`json:"category"`Difficultystring`json:"difficulty"`Summarystring`json:"summary"`}

这里和你在 Gin 项目中定义请求 DTO、响应 DTO 的方式很相似。

JSON Tag 告诉encoding/json:JSON 中的category对应 Go 结构体中的Category。

如果 JSON 返回:

{"category":"redis"}

那么解析后:

result.Category=="redis"

需要注意,Go 的string字段如果没有出现在 JSON 中,通常会保持零值""。因此,解析成功不代表字段一定齐全。

第二步:解析 JSON

iferr:=json.Unmarshal([]byte(raw),&result);err!=nil{returnAIResult{},fmt.Errorf("解析 JSON 失败: %w",err)}

几个关键点:

  • []byte(raw):把字符串转换为字节切片。
  • &result:传入结构体指针,让Unmarshal将解析结果写入结构体。
  • err != nil:处理语法错误或不兼容的类型等问题。
  • %w:包装原始错误,便于后续使用errors.Is或errors.As检查错误链。

例如,如果模型返回:

{category: redis}

这不是合法 JSON,因为键名没有使用双引号,解析就会失败。

第三步:校验业务字段

switchresult.Category{case"redis","go","mysql","network":default:returnfmt.Errorf("不支持的 category: %q",result.Category)}

JSON 即使能解析,也可能返回:

{"category":"unknown_technology","difficulty":"beginner","summary":"这是一个技术问题。"}

它在语法上完全正确,但category不属于我们允许的分类,所以需要拒绝。

这种设计与传统后端中的参数校验很相似:不能因为请求体能反序列化,就直接相信其中的数据。

第四步:分层返回错误

iferr!=nil{fmt.Println("处理模型响应失败:",err)return}

真实项目中,你还可以将错误分类处理:

  • JSON 解析失败:记录必要的诊断信息,视情况重试。
  • 数据校验失败:拒绝进入后续业务流程。
  • 模型调用超时或限流:由模型适配层处理。
  • 模型返回的结论不可信:通过检索、规则校验或人工确认来处理。

不要遇到所有错误都无限重试。重试次数、超时和成本都需要有上限。

五、动手验证:哪些响应应该通过?

把下面几组模拟响应依次替换进main.go的raw变量,运行程序,对照检查是否符合预期。下表结果是根据上面的校验规则推导的,建议你亲手跑一遍确认。

测试数据预期结果原因
{"category":"redis","difficulty":"beginner","summary":"解释缓存穿透。"}✅ 通过语法正确、字段齐全、枚举值合法
{"category":"redis","difficulty":"beginner",}❌ 解析失败末尾多余逗号,不是合法 JSON
{"category":"redis","difficulty":"beginner"}❌ 校验失败缺少summary,被解析成空字符串后拦下
{"category":"unknown_technology","difficulty":"beginner","summary":"…"}❌ 校验失败category不在允许的分类白名单里
{"category":"redis","difficulty":"expert","summary":"…"}❌ 校验失败difficulty不是允许的三个取值之一

第 3 行是最容易被忽略的一种情况:json.Unmarshal没有报错,但数据已经不可用,只能靠第二层校验兜住。

六、Prompt 要求 JSON,为什么还不够?

你可能会想:既然我能在 Prompt 里要求模型"只返回 JSON",为什么还要写这么多校验代码?

原因是 Prompt 本质上是给模型的指令,而不是 Go 编译器或数据库约束。

模型可能出现以下问题:

  1. 输出 Markdown 代码围栏,例如```json。
  2. 忘记必需字段。
  3. 返回不符合约定的枚举值。
  4. 输出格式正确、内容却错误的数据。
  5. 因输出长度达到上限而被截断。

真实项目中,推荐优先级如下:

  1. API 原生结构化输出能力:如果模型服务支持 JSON Schema 或严格结构化输出,优先使用。
  2. Go 后端解析和校验:即使服务承诺结构化输出,也要对实际响应进行错误处理和业务校验。
  3. 有限重试或降级:对可恢复的格式错误,可以有次数限制地重新请求;业务内容错误则不能仅靠反复重试保证正确。

还有一点:如果模型返回的内容被截断,单纯把它强行解析成 JSON 通常无法解决问题。应该检查响应的结束原因和 Token 限制,必要时重新设计输出大小或请求策略。

结构化输出与 Tool Calling 的关系

后续我们会学习 Tool Calling。模型发起工具调用时,通常也需要按照约定的数据结构表达工具名称和参数。

两者存在共同点:都需要模型按照程序约定提供结构化信息。

但两者不是同一回事:

  • 结构化输出:模型返回符合约定的数据。
  • Tool Calling:模型提出工具调用请求,应用程序校验请求并决定是否执行工具。

模型输出了一个工具调用请求,不等于工具已经执行。真正执行操作的仍然是 Go 后端。

七、工程注意事项

结合你熟悉的 Gin、GORM 和 MySQL 项目,真实服务中还需要关注:

  • 不要直接信任模型输出:进入数据库、权限判断或其他业务操作前必须校验。
  • 不要把解析成功当作事实正确:格式校验和事实验证是两件事。
  • 控制重试成本:为重试次数和总耗时设置上限。
  • 保护敏感信息:记录模型错误时,不要无差别记录 API Key、用户隐私或完整敏感上下文。
  • 保留可观测性:至少关注模型耗时、错误类型、Token 使用量和重试次数。
  • 使用服务原生能力:实际接入模型时,根据对应 API 的官方文档使用结构化输出功能,不要凭空假设字段名称。

八、本课总结

本课完成了一个最小的 Go JSON 处理实验:

模拟模型响应 → JSON 解析 → 字段校验 → 业务校验 → 返回结果

你需要掌握的重点是:

  • json.Unmarshal负责将 JSON 转成 Go 数据结构。
  • JSON 语法正确,不代表字段齐全。
  • 字段合法,也不代表业务结论正确。
  • Prompt 可以提高格式遵循程度,但不能替代程序校验。
  • 原生结构化输出能力和 Go 后端校验应该配合使用。

九、课后练习

练习 1

模型返回了合法 JSON:

{"category":"redis","difficulty":"beginner"}

json.Unmarshal是否一定报错?为什么?

参考思路:不会报错。JSON 语法完全合法,只是少了一个字段;Summary会保持string的零值""。这也是结构化输出最隐蔽的坑——解析成功的绿灯会让人误以为数据可用。真正拦住它的是第二层校验(strings.TrimSpace(result.Summary) == ""时报错)。如果业务要求字段必填,也可以把字段声明成指针或自定义类型,用nil判断"没传"和"传了空串"的区别。

练习 2

模型返回:

{"category":"mysql","difficulty":"beginner","summary":"Redis 是关系型数据库。"}

JSON 解析成功,业务字段也都存在。这个结果是否一定可以接受?你会如何处理?

参考思路:不能接受。三层校验它只过了两层:语法合法、字段合法、业务结论错误。程序校验抓不到语义错误,这正是幻觉最难处理的地方。处理方向:① 涉及事实性结论时走 RAG,要求给出可核查来源;② 用规则或交叉校验兜底(例如分类与 summary 关键词一致性检查);③ 高风险场景加人工确认;④ 记录模型输出与最终结果,用真实失败案例回头评测 Prompt 和模型。

练习 3

在一个 Gin 接口中,模型返回的 JSON 解析失败了。你会如何设计错误处理,避免直接把错误数据写入 MySQL,同时避免无限重试?

参考思路:分三层。① 解析层:json.Unmarshal失败立即返回,不进业务逻辑;② 重试层:只对"可恢复"错误(截断、围栏包裹)做至多 1 次重试,并设总耗时上限;③ 降级层:重试仍失败就返回明确的业务错误码,前端展示兜底文案,绝不把半成品写库。写入 MySQL 前再加一道事务 + 校验,保证"要么完整数据,要么什么都没有"。


下一篇:Go AI Agent 实战 07|模型幻觉、能力边界与成本控制

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询