如何从大语言模型获取JSON输出:实用指南

如何从大语言模型获取JSON输出:实用指南

通过Llama.cpp或Gemini API强制输出JSON的教程

Photo by Etienne Girardet on Unsplash

大型语言模型(LLMs)在生成文本方面表现出色,但获得像 JSON 这样的结构化输出通常需要巧妙的提示,并希望 LLM 能够理解。值得庆幸的是,JSON 模式在 LLM 框架和服务中变得越来越普遍。这让你可以定义你想要的确切输出格式。

这篇文章将深入探讨使用 JSON 模式的受限生成。我们将使用一个复杂、嵌套且现实的 JSON 模式示例,引导 LLM 框架/API,如 Llama.cpp 或 Gemini API,生成结构化数据,特别是旅游地点信息。这建立在之前一篇关于使用 Guidance 进行受限生成的文章基础上,但更注重于更广泛采用的 JSON 模式。

虽然 JSON 模式的功能比 Guidance 限制更多,但其更广泛的支持使其更易于访问,尤其是在云端 LLM 提供商中。

在一个个人项目中,我发现虽然在 Llama.cpp 上使用 JSON 模式相对简单,但在 Gemini API 上实现却需要一些额外的步骤。本文将分享这些解决方案,帮助你有效利用 JSON 模式。

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

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

以下是一个简化版本:

{  
"name": "string",  
"location_long_lat": ["number", "number"],  
"climate_type": {"type": "string", "enum": ["tropical", "desert", "temperate", "continental", "polar"]},  
"activity_types": ["string"],  
"attraction_list": [  
{  
"name": "string",  
"description": "string"  
}  
],  
"tags": ["string"],  
"description": "string",  
"most_notably_known_for": "string",  
"location_type": {"type": "string", "enum": ["city", "country", "establishment", "landmark", "national park", "island", "region", "continent"]},  
"parents": ["string"]  
}

你可以手动编写这种类型的模式,也可以使用 Pydantic 库生成。以下是如何在一个简化的示例中做到这一点:

from typing import List  
from pydantic import 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=["beach", "sunny", "family-friendly"],  
    description="一个美丽的海滩,白色的沙子和清澈的蓝色海水。",  
)  
  
schema = location.model_json_schema()  
print(schema)

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

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

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': '旅游地点',  
 '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": "You are a helpful assistant that outputs in JSON."  
        f"Follow this schema {TouristLocation.model_json_schema()}",  
    },  
    {"role": "user", "content": "Generate information about Hawaii, US."},  
    {"role": "assistant", "content": f"{location.model_dump_json()}"},  
    {"role": "user", "content": "Generate information about Casablanca"},  
]  
response_format = {  
    "type": "json_object",  
    "schema": TouristLocation.model_json_schema(),  
}  

start = time.time()  

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

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

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

这段代码首先导入必要的库并初始化 LLM 模型。然后,它定义了一系列与模型进行对话的消息,包括一条系统消息,指示模型以 JSON 格式输出,遵循特定模式,用户请求关于夏威夷和卡萨布兰卡的信息,以及使用指定模式的助手响应。

Llama.cpp 在后台使用无上下文语法来限制结构并生成有效的 JSON 输出,以描述一个新的城市。

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

{'activity_types': ['shopping', 'food and wine', 'cultural'],  
 'attraction_list': [{'description': 'One of the largest mosques in the world '  
                                     'and a symbol of Moroccan architecture',  
                      'name': 'Hassan II Mosque'},  
                     {'description': 'A historic walled city with narrow '  
                                     'streets and traditional shops',  
                      'name': 'Old Medina'},  
                     {'description': 'A historic square with a beautiful '  
                                     'fountain and surrounding buildings',  
                      'name': 'Mohammed V Square'},  
                     {'description': 'A beautiful Catholic cathedral built in '  
                                     'the early 20th century',  
                      'name': 'Casablanca Cathedral'},  
                     {'description': 'A scenic waterfront promenade with '  
                                     'beautiful views of the city and the sea',  
                      'name': 'Corniche'}],  
 'climate_type': 'temperate',  
 'description': 'A large and bustling city with a rich history and culture',  
 'location_type': 'city',  
 'most_notably_known_for': 'Its historic architecture and cultural '  
                           'significance',  
 'name': 'Casablanca',  
 'parents': ['Morocco', 'Africa'],  
 'tags': ['city', 'cultural', 'historical', 'expensive']}

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

方法 2:克服 Gemini API 的奇特之处

Gemini API 是 Google 管理的 LLM 服务,其文档声称对 Gemini Flash 1.5 的 JSON 模式支持有限。然而,通过一些调整,可以使其正常工作。

以下是使其正常工作的常规说明:

```python
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 = model.generate_content(messages)  
print(response.text)

以下是克服 Gemini 限制的方法:

  1. 用完整定义替换 $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 有时在处理枚举时会遇到困难,会输出所有可能的值而不是单个选择。这些值也会以“|” 的形式在一个字符串中分隔,导致根据我们的架构是无效的。使用一轮提示,提供格式正确的示例,引导其朝期望的行为发展。

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

结论

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

希望这篇博客能让您更好地理解 JSON 模式的工作原理,以及如何即使在使用 Gemini 的 API(到目前为止仅有部分支持)时也能使用它。

现在我能够让 Gemini 在某种程度上与 JSON 模式配合工作,我可以完成我的 LLM 工作流的实现,在这种情况下必须以特定方式构建数据。

您可以在此处找到本文的主要代码:https://gist.github.com/CVxTz/8eace07d9bd2c5123a89bf790b5cc39e


参考文章:How to Get JSON Output from LLMs: A Practical Guide

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

数智笔记

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值