
本系列共 10 阶段 面向有 Go 后端基础的开发者全程用 Go 手写实现。当前进度阶段一 大模型基础 · 第 6 课上一篇Temperature 与生成参数前面我们学习了 Prompt、消息角色和模型生成参数。现在进入 AI 应用开发中非常重要的一项能力让大模型返回 Go 程序能够可靠处理的数据而不仅仅是一段自然语言。这一课会开始把 AI 和传统 Go 后端工程连接起来为后面的 Tool Calling 做准备。一、本课学习目标学完之后你应该能够理解普通文本输出与 JSON 结构化输出的区别。使用 Go 的encoding/json解析模型返回的数据。区分 JSON 语法正确、字段合法和业务逻辑正确。处理模型返回格式错误、缺少字段和非法枚举值等问题。理解为什么真实项目中应优先使用模型服务支持的结构化输出能力而不能只依赖 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/jsonerrorsfmtstrings)typeAIResultstruct{Categorystringjson:categoryDifficultystringjson:difficultySummarystringjson:summary}// 校验模型返回的数据是否符合业务要求。funcValidateAIResult(result AIResult)error{ifstrings.TrimSpace(result.Category){returnerrors.New(category 不能为空)}ifstrings.TrimSpace(result.Summary){returnerrors.New(summary 不能为空)}switchresult.Category{caseredis,go,mysql,network:// 合法分类default:returnfmt.Errorf(不支持的 category: %q,result.Category)}switchresult.Difficulty{casebeginner,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{Categorystringjson:categoryDifficultystringjson:difficultySummarystringjson:summary}这里和你在 Gin 项目中定义请求 DTO、响应 DTO 的方式很相似。JSON Tag 告诉encoding/jsonJSON 中的category对应 Go 结构体中的Category。如果 JSON 返回{category:redis}那么解析后result.Categoryredis需要注意Go 的string字段如果没有出现在 JSON 中通常会保持零值。因此解析成功不代表字段一定齐全。第二步解析 JSONiferr: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{caseredis,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 编译器或数据库约束。模型可能出现以下问题输出 Markdown 代码围栏例如json。忘记必需字段。返回不符合约定的枚举值。输出格式正确、内容却错误的数据。因输出长度达到上限而被截断。真实项目中推荐优先级如下API 原生结构化输出能力如果模型服务支持 JSON Schema 或严格结构化输出优先使用。Go 后端解析和校验即使服务承诺结构化输出也要对实际响应进行错误处理和业务校验。有限重试或降级对可恢复的格式错误可以有次数限制地重新请求业务内容错误则不能仅靠反复重试保证正确。还有一点如果模型返回的内容被截断单纯把它强行解析成 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模型幻觉、能力边界与成本控制