使用 Llama.cpp 或 Gemini 的 API 强制 JSON 输出的教程

452 阅读7分钟

如何从 LLM 获取 JSON 输出:实用指南

添加图片注释,不超过 140 字(可选)

欢迎来到雲闪世界。大型语言模型 (LLM) 擅长生成文本,但要获得像 JSON 这样的结构化输出通常需要巧妙的提示,并希望 LLM 能够理解。幸运的是,JSON 模式在 LLM 框架和服务中变得越来越普遍。这让您可以定义所需的精确输出架构。

这篇文章介绍了使用 JSON 模式进行约束生成。我们将使用一个复杂、嵌套且真实的 JSON 模式示例来指导 LLM 框架/API(如 Llama.cpp 或 Gemini API)生成结构化数据,特别是游客位置信息。这篇文章基于之前关于使用Guidance进行约束生成的文章,但重点介绍了更广泛采用的 JSON 模式。

虽然比Guidance更有限,但 JSON 模式的更广泛支持使其更易于访问,尤其是对于基于云的 LLM 提供商而言。

在个人项目中,我发现虽然 JSON 模式对于 Llama.cpp 来说很简单,但要使其与 Gemini API 配合使用则需要一些额外的步骤。本文分享了这些解决方案,以帮助您有效地利用 JSON 模式。

我们的 JSON 模式:旅游地点文档

我们的示例模式代表一个TouristLocation。这是一个非平凡的结构,具有嵌套对象、列表、枚举和各种数据类型(如字符串和数字)。

这是一个简化的版本:

{ 
“name” : “string” ,
“location_long_lat” : [ “number” , “number” ] ,
“climate_type” : { “type” : “string” , “enum” : [ “热带” , “沙漠” , “温带” , “大陆” , “极地” ] } ,
“activity_types” : [ “string” ] ,
“attraction_list” : [ 
{ 
“name” : “string” ,
“description” : “string” 
} 
] ,
“tags” : [ “string” ] ,
“description” : “string” ,
“most_notably_known_for” : “string” ,
“location_type” : { “type” : “string” , “enum” : [ “城市” , “国家” , “机构” , “地标” , “国家公园” , “岛屿” , “地区” , “大陆” ] } ,
“父母” : [ “字符串” ] 
}

您可以手写这种类型的模式,也可以使用 Pydantic 库生成它。下面是一个简化的示例:

从typing导入 列表
从pydantic导入BaseModel,Field 

class  TouristLocation ( BaseModel ): 
    """旅游地点模型"""

     high_season_months: List [ int ] = Field( 
        [], description= "该地点游客最多的月份列表(1-12)"
     ) 

    tags: List [ str ] = Field( 
        ..., 
        description= "描述地点的标签列表(例如可访问、可持续、阳光充足、便宜、昂贵)" , 
        min_length= 1 , 
    ) 
    description: str = Field(..., description= "位置的文本描述" ) 

# 示例用法和架构输出
location = TouristLocation( 
    high_season_months=[ 6 , 7 , 8 ], 
    tags=[ "海滩" , "阳光充足" , "家庭友好型" ], 
    description= "美丽的海滩,拥有白色的沙滩和清澈的蓝色海水。" , 
) 

schema = location.model_json_schema() 
print (schema)

TouristLocation此代码使用 Pydantic 定义了数据类的简化版本。它有三个字段:

  • high_season_months:表示该地点访问量最大的月份(1-12)的整数列表。默认为空列表。
  • tags:使用“可访问”、“可持续”等标签描述位置的字符串列表。此字段是必填项 ( ...),并且必须至少包含一个元素 ( min_length=1)。
  • description:包含位置文本描述的字符串字段。此字段也是必填的。

然后,代码会创建该类的一个实例TouristLocation,并用它model_json_schema()来获取模型的 JSON Schema 表示。该 Schema 定义了该类所需的数据的结构和类型。

model_json_schema()返回:

{ 'description''旅游地点模型''properties':{ 'description':{ 'description''地点的文字描述
                                               ''title''描述''type''string' },
                'high_season_months':{ 'default':[],
                                       'description''月份列表(1-12)''地点
                                                      访问量最大''items':{ 'type''integer' },
                                       'title''旺季月份''type''array' },
                'tags':{ 'description''描述地点的标签列表' 
                                        (例如可访问,可持续,阳光明媚,' 
                                        '便宜,昂贵)',
                         'items':{ 'type' : 'string' }, 
                         'minItems' : 1, 
                         'title' : '标签' , 
                         'type' : 'array' }}, 
 'required' : [ 'tags' , 'description' ], 
 'title' : 'TouristLocation' , 
 'type' : 'object' }

现在我们有了模式,让我们看看如何实施它。首先在 Llama.cpp 中使用其 Python 包装器,其次使用 Gemini 的 API。

方法 1:使用 Llama.cpp 的简单方法

Llama.cpp,一个用于在本地运行 Llama 模型的 C++ 库。它对初学者很友好,并且拥有一个活跃的社区。我们将通过其 Python 包装器使用它。

以下是使用它生成数据的方法TouristLocation:

# 导入和其他内容

# 模型初始化:
 checkpoint = “lmstudio-community/Meta-Llama-3.1-8B-Instruct-GGUF”

 model = Llama.from_pretrained( 
    repo_id=checkpoint, 
    n_gpu_layers=-1, 
    filename= “*Q4_K_M.gguf” , 
    verbose=False, 
    n_ctx=12_000, 
) 

messages = [ 
    { 
        “role” : “system” , 
        “content” : “您是一位以 JSON 格式输出的有用助手。”
         f “遵循此模式 {TouristLocation.model_json_schema()}” , 
    }, 
    { “role” : “user” , “content” : “生成有关美国夏威夷的信息。” }, 
    { "role" : "assistant" , "content" : f "{location.model_dump_json()}" }, 
    { "role" : "user" , "content" : "生成有关卡萨布兰卡的信息" }, 
] 
response_format = { 
    "type" : "json_object" , 
    "schema" : TouristLocation.model_json_schema(), 
} 

start = time.time() 

output = model.create_chat_completion( 
    messages=messages, max_tokens=1200, response_format=response_format 
) 

print(outputs[ "choices" ][0][ "message" ][ "content" ]) 

print(f"时间:{time.time() - start}")

代码首先导入必要的库并初始化 LLM 模型。然后,它定义与模型对话的消息列表,包括指示模型根据特定架构以 JSON 格式输出的系统消息、用户对夏威夷和卡萨布兰卡信息的请求以及使用指定架构的助手响应。

Llama.cpp 在底层使用上下文无关语法来约束结构并为新城市生成有效的 JSON 输出。

在输出中我们得到以下生成的字符串:

{ 'activity_types' :  [ '购物' , '美食与美酒' , '文化' ] ,
 'attraction_list' :  [ { 'description' : '世界上最大的清真寺之一 ' 
                                     ',摩洛哥建筑的象征' ,
                       'name' : '哈桑二世清真寺' } , 
                     { 'description' : '一座历史悠久的城墙城市,拥有狭窄的 ' 
                                     '街道和传统商店' ,
                       'name' : '老麦地那' } , 
                     { 'description' : '一座历史悠久的广场,拥有美丽的 ' 
                                     '喷泉和周围的建筑' ,
                       'name' : '穆罕默德五世广场' } , 
                     { 'description' : '一座美丽的天主教堂,建于 ' '20世纪
                                     初' ,                       'name' : '卡萨布兰卡大教堂' } , { 'description' : '风景秀丽的海滨长廊,'                                      '可欣赏到城市和大海的美丽景色' ,                       'name' : 'Corniche' } ] , 'climate_type' : 'temperate' , 'description' : '一座拥有丰富历史和文化的繁华大城市' , 'location_type' : 'city' , 'most_notably_known_for' : '其历史建筑和文化'                            '意义' , 'name' : '卡萨布兰卡' , 'parents' : [ '摩洛哥' , '非洲' ] , 'tags' : [ 'city' , 'cultural' , 'historical' ,'昂贵的' ] }

                     








 
 

然后可以将其解析为我们的 Pydantic 类的一个实例。

方法 2:克服 Gemini API 的怪癖

Gemini API 是 Google 的托管 LLM 服务,其文档中声称 Gemini Flash 1.5 仅支持有限的 JSON 模式。不过,只需进行一些调整即可实现该功能。

以下是使其工作的一般说明:

schema = TouristLocation.model_json_schema() 
schema = replace_value_in_dict(schema.copy(), schema.copy()) 
del schema[ "$defs" ] 
delete_keys_recursive(schema, key_to_delete= "title" ) 
delete_keys_recursive(schema, key_to_delete= "location_long_lat" ) 
delete_keys_recursive(schema, key_to_delete= "default" ) 
delete_keys_recursive(schema, key_to_delete= "default" ) 
delete_keys_recursive(schema, key_to_delete= "minItems" ) 

print (schema) 

messages = [ 
    ContentDict( 
        role= "user" , 
        parts=[ 
            "您是一位能以 JSON 格式输出的得力助手。" 
            f"请遵循此模式{TouristLocation.model_json_schema()} "
         ], 
    ), 
    ContentDict(role= "user" , parts=[ "生成有关美国夏威夷的信息。" ]), 
    ContentDict(role= "model" , parts=[ f" {location.model_dump_json()} " ]), 
    ContentDict(role= "user" , parts=[ "生成有关卡萨布兰卡的信息" ]), 
] 

genai.configure(api_key=os.environ[ "GOOGLE_API_KEY" ]) 

# 将 `response_mime_type` 与 `response_schema` 结合使用需要 Gemini 1.5 Pro 模型
model = genai.GenerativeModel( 
    "gemini-1.5-flash" , 
    # 设置 `response_mime_type` 以输出 JSON 
    # 将架构对象传递给 `response_schema` 字段
    generation_config={ 
        "response_mime_type" : "application/json" , 
        "response_schema" : schema, 
    }, 
) 

response =模型.生成内容(消息)
打印(响应.文本)

以下是如何克服 Gemini 的局限性的方法:

  1. 用完整定义替换refGemini偶然发现了架构引用(ref: Gemini 偶然发现了架构引用(ref)。当您有嵌套对象定义时会用到它们。用架构中的完整定义替换它们。
def  replace_value_in_dict ( item, original_schema ): 
    # 来源:https://github.com/pydantic/pydantic/issues/889 
    if  isinstance (item, list ): 
        return [replace_value_in_dict(i, original_schema) for i in item] 
    elif  isinstance (item, dict ): 
        if  list (item.keys()) == [ "$ref" ]: 
            definitions = item[ "$ref" ][ 2 :].split( "/" ) 
            res = original_schema.copy() 
            for definition in definitions: 
                res = res[definition] 
            return res 
        else : 
            return { 
                key: replace_value_in_dict(i, original_schema) 
                for key, i in item.items() 
            } 
    else : 
        return item
  1. 删除不支持的键: Gemini 尚未处理“title”、“AnyOf”或“minItems”等键。请从您的架构中删除这些键。这会导致架构的可读性降低和限制性降低,但如果坚持使用 Gemini,我们别无选择。
def  delete_keys_recursive ( d, key_to_delete ): 
    if  isinstance (d, dict ): 
        # 如果存在则删除该键
        if key_to_delete in d: 
            del d[key_to_delete] 
        # 递归处理字典中的所有项目
        for k, v in d.items(): 
            delete_keys_recursive(v, key_to_delete) 
    elif  isinstance (d, list ): 
        # 递归处理列表中的所有项目
        for item in d: 
            delete_keys_recursive(item, key_to_delete)
  1. 枚举的一次性或少量提示: Gemini 有时会遇到枚举问题,它会输出所有可能的值,而不是单个选择。这些值在单个字符串中也由“ |”分隔,根据我们的架构,这些值是无效的。使用一次性提示,提供正确格式的示例,以引导它实现所需的行为。

通过应用这些转换并提供清晰的示例,您可以使用 Gemini API 成功生成结构化 JSON 输出。

结论

JSON 模式允许您直接从 LLM 获取结构化数据,从而使其更适合实际应用。虽然 Llama.cpp 等框架提供了简单的实现,但您可能会遇到 Gemini API 等云服务的问题。

希望本博客能让您更好地实际了解 JSON 模式的工作原理,以及即使在使用目前仅提供部分支持的 Gemini API 时如何使用它。

现在我能够让 Gemini 以 JSON 模式工作,我就可以完成我的 LLM 工作流程的实现,其中需要以特定方式构造数据。

本文的主要代码可关注雲闪世界。(Aws解决方案架构师vs开发人员&GCP解决方案架构师vs开发人员)

订阅频道(t.me/awsgoogvps_…) TG交流群(t.me/awsgoogvpsHost)