🦞 一只用 AI Agent 搭副业产线的程序员
上篇我们说了结构化输出的重要性。但问题来了:你跟 AI 说「返回 JSON」,它不一定听话。
有时候多一个逗号,有时候多一行「以下是结果:」,有时候字段名拼错了。在聊天界面里无所谓,在代码里直接 panic。
生产线不能容忍这种不确定性。
这篇文章给你一套方案——Schema 定义 → 自动校验 → 不合法就重试。我把这套东西用在日报 Agent 里之后,JSON 解析成功率从 78% 提到了 100%。
问题有多严重
我跑了 100 次数据提取任务,要求返回 JSON:
| 返回情况 | 次数 | 占比 |
|---|---|---|
| 合法 JSON,字段正确 | 78 | 78% |
| 合法 JSON,字段缺失或多余 | 12 | 12% |
| JSON 外有额外文字 | 7 | 7% |
| 非法 JSON(多逗号、引号不匹配等) | 3 | 3% |
22% 的情况下你的代码直接崩。而且这 22% 不是同样的错误——每次错的都不一样。
完整方案:三步保证 100%
三句话总结这套方案:
- 结构化输出指令——在 Prompt 里用 JSON Schema 告诉 AI 你要什么
- 严格校验——AI 返回后解析 JSON 并校验结构
- 自动重试——不合格就重试,最多 3 次
第一步:把输出约束写成 JSON Schema
先定义你想要的输出结构。Go 里最方便的是用 struct + json tag:
packagemainimport("encoding/json""fmt")// 定义你期望的输出结构typeExtractedIssuestruct{Titlestring`json:"title"`Severitystring`json:"severity"`// "high" | "medium" | "low"LineNumbers[]int`json:"line_numbers"`Categorystring`json:"category"`// "bug" | "style" | "security" | "performance"Suggestionstring`json:"suggestion"`IsCertainbool`json:"is_certain"`// AI 是否确定这个问题的存在}typeCodeReviewOutputstruct{Issues[]ExtractedIssue`json:"issues"`TotalIssuesint`json:"total_issues"`CriticalCountint`json:"critical_count"`Summarystring`json:"summary"`}然后在 Prompt 里把这个 Schema 翻译成 AI 能理解的格式:
funcbuildStructuredPrompt(codestring)string{returnfmt.Sprintf(`审查以下 Go 代码,找出所有安全和性能问题。 严格按照以下 JSON Schema 返回结果(不要返回任何其他内容,只返回 JSON): { "issues": [ { "title": "问题简述(≤20字)", "severity": "high | medium | low", "line_numbers": [行号], "category": "bug | style | security | performance", "suggestion": "修复建议", "is_certain": true } ], "total_issues": 问题总数, "critical_count": 高危问题数, "summary": "总体评价(≤50字)" } 代码: %s`,code)}关键:把 JSON Schema 直接写在 Prompt 里。不要只是说「返回 JSON」,而是把结构完完整整写出来。
第二步:用 Go 的 JSON Schema 校验
Go 生态里go-playground/validator是最好用的结构校验库:
import("encoding/json""fmt""github.com/go-playground/validator/v10")varvalidate=validator.New()funcparseAndValidate(responsestring)(*CodeReviewOutput,error){// 1. 尝试解析 JSONvaroutput CodeReviewOutputiferr:=json.Unmarshal([]byte(response),&output);err!=nil{returnnil,fmt.Errorf("JSON 解析失败: %w",err)}// 2. 校验字段iferr:=validate.Struct(output);err!=nil{returnnil,fmt.Errorf("字段校验失败: %w",err)}// 3. 业务逻辑校验ifoutput.TotalIssues!=len(output.Issues){returnnil,fmt.Errorf("total_issues(%d) 与实际 issues 数量(%d) 不一致",output.TotalIssues,len(output.Issues))}// 4. 校验 severity 枚举值validSeverities:=map[string]bool{"high":true,"medium":true,"low":true}for_,issue:=rangeoutput.Issues{if!validSeverities[issue.Severity]{returnnil,fmt.Errorf("非法的 severity 值: %s",issue.Severity)}}return&output,nil}这里做了三层校验:
- 是不是合法 JSON
- 必填字段有没有、类型对不对
- 业务逻辑是否自洽(total_issues 跟实际数量一致、severity 值在枚举范围内)
第三步:不合格就重试
funccallLLMWithRetry(promptstring,maxRetriesint)(*CodeReviewOutput,error){varlastErrerrorforattempt:=1;attempt<=maxRetries;attempt++{fmt.Printf("第 %d 次尝试...\n",attempt)response:=callLLM([]Message{{Role:"user",Content:prompt},},0.1,1000)output,err:=parseAndValidate(response)iferr==nil{returnoutput,nil// 成功了}lastErr=err fmt.Printf("校验失败: %v\n",err)ifattempt<maxRetries{// 把错误信息带回给 AI,让它纠正prompt=fmt.Sprintf(`你上一次的输出校验未通过: 错误:%s 请修正后重新输出。只输出修正后的 JSON,不要解释。 原始任务: %s`,err.Error(),prompt)}}returnnil,fmt.Errorf("重试 %d 次后仍然失败: %w",maxRetries,lastErr)}这套重试逻辑的关键是——把校验错误信息喂回给 AI。AI 看到「你的 JSON 多了一个逗号」或者「total_issues 和实际数量不一致」,它知道怎么改。
完整封装:一个通用的 StructuredCall
把上面的逻辑封装成一个通用函数:
funcStructuredCall[T any](systemPrompt,userPromptstring,maxRetriesint)(*T,error){prompt:=fmt.Sprintf(`%s 严格按照以下 JSON Schema 返回结果(只返回 JSON,不要其他内容): %s 任务: %s`,systemPrompt,generateSchema[T](),userPrompt)varlastErrerrorforattempt:=1;attempt<=maxRetries;attempt++{response:=callLLM([]Message{{Role:"user",Content:prompt},},0.1,1000)varresult Tiferr:=json.Unmarshal([]byte(response),&result);err!=nil{lastErr=err prompt=fmt.Sprintf("JSON 解析失败: %v\n请修正后重新输出 JSON。",err)continue}iferr:=validate.Struct(result);err!=nil{lastErr=err prompt=fmt.Sprintf("校验失败: %v\n请修正后重新输出 JSON。",err)continue}return&result,nil}returnnil,fmt.Errorf("重试 %d 次后仍然失败: %w",maxRetries,lastErr)}// 用反射从 struct 生成 JSON Schema 描述funcgenerateSchema[T any]()string{// 简化版:用 json tag 生成描述t:=reflect.TypeOf((*T)(nil)).Elem()returngenerateSchemaFromType(t," ")}使用:
issues,err:=StructuredCall[CodeReviewOutput]("你是代码审查专家。",fmt.Sprintf("审查以下代码:\n%s",code),3,)iferr!=nil{log.Fatal("结构化调用失败: ",err)}fmt.Printf("发现 %d 个问题,其中 %d 个高危\n",issues.TotalIssues,issues.CriticalCount)一行调用,类型安全。泛型让返回值直接是*CodeReviewOutput而不是map[string]interface{}。
实测效果
加了这套之后,重新跑 100 次测试:
| 阶段 | 成功次数 | 成功率 |
|---|---|---|
| 第 1 次调用 | 78 | 78% |
| 第 1 次重试 | +15 | 93% |
| 第 2 次重试 | +5 | 98% |
| 第 3 次重试 | +2 | 100% |
3 次重试后 100%。额外的 token 成本约 +15%(重试的 Prompt 很短)。
一个省钱技巧:只重试解析失败的
不是所有失败都需要重试。如果只是total_issues和实际数量差 1——你可以自己修复这种小问题:
funcautoFix(output*CodeReviewOutput){// 小问题自动修复,不用重试ifoutput.TotalIssues!=len(output.Issues){output.TotalIssues=len(output.Issues)}// 重新计数output.CriticalCount=0for_,issue:=rangeoutput.Issues{ifissue.Severity=="high"{output.CriticalCount++}}}原则:只有 JSON 格式错误才需要重试。数据层面的小不一致可以代码修复。
总结
| 问题 | 解决方案 |
|---|---|
| AI 不返回 JSON | Prompt 里直接写 Schema |
| JSON 字段缺失/类型不对 | validator 校验 |
| 解析失败 | 把错误喂回 AI,重试 |
| 小数据不一致 | 代码里自动修复,省 token |
下一篇我们要写一个完整的 Prompt 模板引擎——支持变量替换、条件分支、模板继承。让你的 Prompt 从「手写字符串拼接」升级到「模板引擎驱动」。
关注我,别错过。
🦞 一只用 AI Agent 搭副业产线的程序员
全平台同名:虾哥不加班
需要定制 AI 工具?来聊聊 → lob_ai源码:GitHub - lobster-bujiaban