V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
爱意满满的作品展示区。
IndexOutOfBounds
V2EX  ›  分享创造

Golang 中强制 LLM 返回 JSON 的无感解法

  •  
  •   IndexOutOfBounds · 16 天前 · 2269 次点击

    https://github.com/glidea/llm-structed

    背景

    在 chat 场景中,通常模型不需要返回结构化的数据。但在 LLM 应用开发里,模型通常被视为提供某种原子能力的 API Service ,此时我们希望直接得到一个 JSON ,通常的解法有:

    1. 直接在 Prompt 里强调输出格式

    • 优:简单,对模型 API 没有任何额外要求
    • 缺:格式不稳定,特别是对于能力较差的模型

    2. 使用 response_format: { type: "json_object" } + Prompt 说明具体字段

    • 优:总是确保返回合法 JSON
    • 缺:字段不稳定,特别是对于能力较差的模型

    3. 使用 response_format: { type: "json_schema", json_schema: {"strict": true, "schema": ...} }

    • 优:确保返回合法 JSON ,且字段稳定
    • 缺:仅部分模型支持

    SDK

    • 在 OpenAI 提供的 SDK 中直接支持 Class 作为 Response Format ,但仅支持 Python
    • go-openai 中,使用方式过于通用繁琐
    • llm-structed 专门针对结构化场景优化,对方案 3 和方案 2 提供原生支持

    例子

    package main
    
    import (
    	"context"
    	"fmt"
    
    	"github.com/glidea/llm-structed"
    )
    
    type Summary struct {
    	Title    string   `json:"title" desc:"The title of the summary"`
    	Content  string   `json:"content" desc:"A concise summary of the article content"`
    	Keywords []string `json:"keywords" desc:"Key topics mentioned in the article"`
    	Score    int      `json:"score" desc:"The quality score of the article (1-10)"`
    	Category string   `json:"category" desc:"The category of the article" enum:"Technology,Science,Business,Health,Education,Other"`
    }
    
    func main() {
    	// New client (In minimal configuration, you only need to set the APIKey)
    	cli, _ := llmstructed.New(llmstructed.Config{
    		BaseURL:                   "https://openrouter.ai/api/v1",
    		APIKey:                    "sk-...",
    		Model:                     "google/gemini-flash-1.5",
    		Temperature:               0.3,
    		StructuredOutputSupported: true, // 使用方案 3
    		Retry:                     1,
    		Debug:                     true,
    		// See source code comments of llmstructed.Config for these config detail
    	})
    	ctx := context.Background()
    
    	// Structured Outputed
    	var summary Summary
    	_ = cli.Do(ctx, []string{`Please generate a summary of this article: Artificial Intelligence (AI) is transforming the way we live and work. It refers to
    	computer systems that can perform tasks that normally require human intelligence. These
    	tasks include visual perception, speech recognition, decision-making, and language
    	translation. Machine learning, a subset of AI, enables systems to learn and improve
    	from experience without being explicitly programmed. Deep learning, particularly,
    	has revolutionized AI by using neural networks to process complex patterns in data.`,
    	}, &summary)
    	fmt.Printf("Go Struct: %v\n\n", summary)
    
    	// Simple method for single value
    	str, _ := cli.String(ctx, []string{"Hello, who are you?"})
    	fmt.Printf("String: %s\n\n", str)
    	languages, _ := cli.StringSlice(ctx, []string{"List some popular programming languages."})
    	fmt.Printf("String Slice: %v\n\n", languages)
    	count, _ := cli.Int(ctx, []string{`How many words are in this sentence: "Hello world, this is a test."`})
    	fmt.Printf("Integer: %d\n\n", count)
    	yes, _ := cli.Bool(ctx, []string{"Are you happy?"})
    	fmt.Printf("Boolean: %v\n\n", yes)
    	trues, _ := cli.BoolSlice(ctx, []string{"Are these statements true? [\"The sky is blue\", \"Fish can fly\", \"Water is wet\"]"})
    	fmt.Printf("Boolean Slice: %v\n\n", trues)
    	pi, _ := cli.Float(ctx, []string{"What is the value of pi (to two decimal places)?"})
    	fmt.Printf("Float: %.2f\n\n", pi)
    }
    
    第 1 条附言  ·  15 天前
    收藏比 star 数多系列,有需要帮忙点点 star
    https://github.com/glidea/llm-structed

    下个饼是直接使用现有第三方 sdk 作为 tool ,尽量减少类似 langchain 等其它框架的额外适配
    14 条回复    2025-02-10 14:05:39 +08:00
    abc634
        1
    abc634  
       16 天前   ❤️ 1
    我的一个小经验:
    让 api 返回 markdown 或者 html 或者 xml ,都比 json 好。
    然后再解析 xml 就简单了。
    IndexOutOfBounds
        2
    IndexOutOfBounds  
    OP
       16 天前
    @abc634 我理解场景有些区别,markdown 是半结构化的,主要用于直接展示,比如直接让 AI 用 markdown 写篇文章,这是很好的选择

    json 是开发内部使用的,比如你需要提供接口给前端做二次展示

    另外通过 json schema 可以做到很强的约束,比如文中给文章分类的例子,通过 enum:"Technology,Science,Business,Health,Education,Other" 强限制分类范围
    otakustay
        4
    otakustay  
       16 天前   ❤️ 1
    用 function calling 呢?你需要的模型里有不支持 function 的吗
    还有个办法是 Assistant Prefill ,不过我估计也不稳
    otakustay
        5
    otakustay  
       16 天前
    @abc634 XML 层次深了也不行,我还特地为这个调过专门的方案
    reeco
        6
    reeco  
       16 天前
    还是 1 的方案吧,2 ,3 的方案你换个非 openai 的模型照样不能用
    ychost
        7
    ychost  
       16 天前
    如果输出的 JSON 没有嵌套,就没必要让它输出 JSON 了,直接输出 kv 对,然后代码解析这样容错性更好
    IndexOutOfBounds
        8
    IndexOutOfBounds  
    OP
       16 天前
    @otakustay 暂时不支持 function call ,不过确实有这个想法,自动注入结构体 Method
    IndexOutOfBounds
        9
    IndexOutOfBounds  
    OP
       16 天前
    @reeco 非 openai ,比如 gemini ,deepseek 也支持 json 输出的,算是一个通用的规范了

    https://openrouter.ai/models?fmt=cards&order=newest&supported_parameters=structured_outputs
    otakustay
        10
    otakustay  
       16 天前
    要不这样,用写代码的思路,把你要的 schema 变成 TS interface ,然后下面写个 function 定义接收这个类型的参数,再一段注释说明你的需求,最后是 functionName({起头,让模型给你补……
    74123gzy
        11
    74123gzy  
       15 天前
    @otakustay #10 那不还是 1. 直接在 Prompt 里强调输出格式
    lovestudykid
        12
    lovestudykid  
       15 天前
    还有个办法是给 json 开个头
    neptuno
        13
    neptuno  
       14 天前
    试了很多,function calling 是最优解
    abc634
        14
    abc634  
       12 天前
    @otakustay 谢谢,
    我之前还没有碰到 这种情况,我碰到的是上下文太长超出窗口,
    (多页内容)
    然后我就把前一页的 xml 内容和新的页面 丢给 gpt 让他自己补全,效果还可以。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2897 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 29ms · UTC 06:13 · PVG 14:13 · LAX 22:13 · JFK 01:13
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.