从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)
    }
}

七、总结

结构化输出是大模型落地的关键一步。

核心要点:

  1. 使用JSON模式约束输出格式
  2. 设置temperature为0确保确定性
  3. 实现重试机制处理解析失败
  4. 使用JSON Schema进行校验

核心收获: 好的Prompt工程能让大模型变成可靠的数据处理器。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐