从0到1:我是如何设计大模型结构化输出系统的
从0到1:我是如何设计大模型结构化输出系统的
前言
最近在做智能客服系统时遇到个问题:大模型返回的自然语言回复不好解析。
用户问"今天天气怎么样",模型可能返回:"今天北京晴转多云,最高温度28度"。
但我需要把温度、天气状况提取出来作为结构化数据。
经过调研,我设计了一套基于Agent拓扑的结构化输出方案,准确率提升到95%以上。
一、底层原理
1.1 核心机制
结构化输出的关键是让大模型按照指定格式返回数据:
graph TD A[用户输入] --> B[Prompt模板] B --> C[大模型] C --> D{输出格式校验} D -->|通过| E[JSON解析] D -->|失败| F[重试机制] F --> B E --> G[业务逻辑]Prompt工程要点:
{ "temperature": 0.0, "max_tokens": 512, "response_format": { "type": "json_object" } }1.2 与同类方案的对比
| 方案 | 准确率 | 灵活性 | 成本 |
|---|---|---|---|
| JSON模式 | 高 | 中 | 低 |
| 函数调用 | 高 | 高 | 中 |
| XML标签 | 中 | 低 | 低 |
| 正则提取 | 低 | 低 | 低 |
二、快速上手
package main import ( "encoding/json" "fmt" ) type WeatherResponse struct { City string `json:"city"` Temperature int `json:"temperature"` Condition string `json:"condition"` Humidity int `json:"humidity"` } func main() { // 大模型返回的JSON字符串 modelOutput := `{ "city": "北京", "temperature": 28, "condition": "晴转多云", "humidity": 45 }` // 解析为结构化数据 var weather WeatherResponse err := json.Unmarshal([]byte(modelOutput), &weather) if err != nil { panic(err) } fmt.Printf("城市: %s\n", weather.City) fmt.Printf("温度: %d°C\n", weather.Temperature) fmt.Printf("天气: %s\n", weather.Condition) fmt.Printf("湿度: %d%%\n", weather.Humidity) }三、核心 API / 深水区
3.1 核心方法速查
| 方法 | 功能 | 适用场景 |
|---|---|---|
json.Unmarshal() | JSON解析 | 标准JSON输出 |
json.Marshal() | JSON序列化 | 构造请求体 |
json.Decoder.Decode() | 流式解析 | 大文件处理 |
reflect.StructTag() | 标签解析 | 动态字段映射 |
3.2 生产级配置
// 带校验的结构化输出解析器 type StructuredOutputParser struct { model *LLMClient schema interface{} maxRetries int } func NewParser(model *LLMClient, schema interface{}) *StructuredOutputParser { return &StructuredOutputParser{ model: model, schema: schema, maxRetries: 3, } } func (p *StructuredOutputParser) Parse(prompt string) (interface{}, error) { for i := 0; i < p.maxRetries; i++ { response, err := p.model.Generate(p.buildPrompt(prompt)) if err != nil { return nil, err } result, err := p.validateAndParse(response) if err == nil { return result, nil } log.Printf("解析失败,第%d次重试: %v", i+1, err) } return nil, fmt.Errorf("超过最大重试次数") } func (p *StructuredOutputParser) buildPrompt(userInput string) string { schemaJSON, _ := json.Marshal(p.schema) return fmt.Sprintf(` 请按照以下JSON格式输出结果: %s 用户输入:%s `, string(schemaJSON), userInput) }3.3 高级定制
// 自定义JSON schema校验 func validateSchema(data []byte, schema interface{}) error { // 使用json-schema库进行校验 loader := jsonschema.NewLoader() schemaDoc, err := loader.LoadFromBytes(data) if err != nil { return err } result, err := schemaDoc.Validate(bytes.NewReader(data)) if err != nil { return err } if !result.Valid() { return fmt.Errorf("JSON不符合schema") } return nil }四、实战演练
场景:智能客服意图识别
type IntentResult struct { Intent string `json:"intent"` Confidence float64 `json:"confidence"` Entities map[string]string `json:"entities"` } func parseIntent(userInput string) (*IntentResult, error) { prompt := fmt.Sprintf(` 请分析用户意图: 用户输入:%s 输出格式: { "intent": "意图名称", "confidence": 置信度(0-1), "entities": {"实体名": "实体值"} } `, userInput) response, err := callLLM(prompt) if err != nil { return nil, err } var result IntentResult err = json.Unmarshal([]byte(response), &result) if err != nil { return nil, err } return &result, nil }五、避坑指南与最佳实践
💡 技巧:使用JSON Schema约束输出
// 定义输出schema var weatherSchema = `{ "type": "object", "properties": { "city": {"type": "string"}, "temperature": {"type": "integer", "minimum": -40, "maximum": 60}, "condition": {"type": "string", "enum": ["晴", "多云", "阴", "雨", "雪"]}, "humidity": {"type": "integer", "minimum": 0, "maximum": 100} }, "required": ["city", "temperature", "condition"] }`⚠️ 警告:处理JSON解析错误
func safeParse(data string, v interface{}) error { // 清理可能的干扰字符 cleaned := strings.TrimSpace(data) cleaned = strings.TrimPrefix(cleaned, "```json") cleaned = strings.TrimSuffix(cleaned, "```") err := json.Unmarshal([]byte(cleaned), v) if err != nil { return fmt.Errorf("解析失败: %w, 原始数据: %s", err, data) } return nil }✅ 推荐:设置temperature为0
// 结构化输出时降低随机性 request := &LLMRequest{ Model: "gpt-4", Temperature: 0.0, // 确定性输出 MaxTokens: 512, }六、综合实战演示
package main import ( "encoding/json" "fmt" "log" "strings" ) type Parser struct { maxRetries int } type OutputSchema struct { Type string `json:"type"` Properties map[string]Schema `json:"properties"` Required []string `json:"required"` } type Schema struct { Type string `json:"type"` } func NewParser(maxRetries int) *Parser { return &Parser{maxRetries: maxRetries} } func (p *Parser) Parse(userInput string, schema OutputSchema) (map[string]interface{}, error) { for attempt := 0; attempt < p.maxRetries; attempt++ { prompt := buildPrompt(userInput, schema) response := mockLLMCall(prompt) result, err := parseResponse(response) if err == nil { if validateResult(result, schema) { return result, nil } log.Printf("第%d次尝试:结果不符合schema", attempt+1) continue } log.Printf("第%d次尝试:解析失败 %v", attempt+1, err) } return nil, fmt.Errorf("超过最大重试次数") } func buildPrompt(userInput string, schema OutputSchema) string { schemaJSON, _ := json.MarshalIndent(schema, "", " ") return fmt.Sprintf(`用户输入:%s 请按照以下JSON schema输出结果: %s 输出要求: 1. 只输出JSON,不要任何其他文本 2. 必须包含所有required字段 3. 字段类型必须匹配`, userInput, string(schemaJSON)) } func mockLLMCall(prompt string) string { // 模拟大模型响应 return `{ "city": "上海", "temperature": 32, "condition": "晴", "humidity": 65 }` } func parseResponse(response string) (map[string]interface{}, error) { response = strings.TrimSpace(response) response = strings.TrimPrefix(response, "```json") response = strings.TrimSuffix(response, "```") var result map[string]interface{} err := json.Unmarshal([]byte(response), &result) return result, err } func validateResult(result map[string]interface{}, schema OutputSchema) bool { for _, required := range schema.Required { if _, ok := result[required]; !ok { return false } } return true } func main() { schema := OutputSchema{ Type: "object", Properties: map[string]Schema{ "city": {Type: "string"}, "temperature": {Type: "integer"}, "condition": {Type: "string"}, "humidity": {Type: "integer"}, }, Required: []string{"city", "temperature", "condition"}, } parser := NewParser(3) result, err := parser.Parse("今天上海天气怎么样?", schema) if err != nil { log.Fatal(err) } fmt.Printf("解析结果:\n") for k, v := range result { fmt.Printf(" %s: %v\n", k, v) } }七、总结
结构化输出是大模型落地的关键一步。
核心要点:
- 使用JSON模式约束输出格式
- 设置temperature为0确保确定性
- 实现重试机制处理解析失败
- 使用JSON Schema进行校验
核心收获:好的Prompt工程能让大模型变成可靠的数据处理器。
