AI生成Json结构化数据的几种方案

AI生成Json结构化数据的几种方案

在AI功能开发中,我们通常希望AI生成的数据是结构化的,不仅仅是简单的一层字段,也可以包含嵌套关系。

由于AI的注意力机制,AI可以在一次生成中根据上下文补全层级关系。比如我们想让AI生成一个虚拟人物,并附加一些相关属性,如下:

json复制代码{
    "name": "John",
    "age": 30,
    "city": "New York",
    "pets": [
        {
            "name": "Buddy",
            "species": "dog"
        },
        {
            "name": "Simba",
            "species": "cat"
        }
    ]
}

本文将探讨使用AI生成这种结构化数据的几种方案,以及每种方案的优缺点。

早期版本

OpenAI早期并未提供Json结构化输出的选项,并且现在还有很多大模型不支持Json输出,我们只能通过Prompt来让大模型输出Json,例如:

Prompt:生成一个虚拟人物,返回为Json数据,包含name, age, city, pets,其中pets数组包含name和species

ChatGPT:好的,我将为您生成一个虚拟人物的JSON数据。请注意,由于这是一个虚拟人物,所以所有的信息都是随机生成的。 { "name": "Emma Smith", "age": 28, "city": "New York", "pets": [ { "name": "Charlie", "species": "Dog" }, { "name": "Luna", "species": "Cat" } ] } 这是一个名为Emma Smith的28岁的虚拟人物,居住在纽约。她有两只宠物,一只名叫Charlie的狗和一只名叫Luna的猫。她还有三个朋友,他们分别是James Johnson(30岁)、Olivia Davis(26岁)和William Thompson(29岁)。请注意,这只是一个虚拟人物示例,所有的信息都是随机生成的,并不代表真实的人物。

生成的结果还可以,但是提取出Json字符串还是比较麻烦的,甚至有些时候Json会包含一些错误的引号、逗号、括号,这种方式稳定性很差,非常不推荐。

Json模式

Json模式可以让AI只返回合法的Json字符串,并节省了输出Token。

目前OpenAI等大模型已经支持Json结构化输出,只需要在请求中加上response_format": {"type": "json_object"},然后在提示词中说明要生成的Json结构,例如:

Prompt:生成一个虚拟人物,返回为Json数据,包含name, age, city, pets,其中pets数组包含name和species

ChatGPT: { "name": "Emma Smith", "age": 28, "city": "New York", "pets": [ { "name": "Charlie", "species": "Dog" }, { "name": "Luna", "species": "Cat" } ] }

这种模式保证了结果百分百可用,后续的方案都会基于Json模式展开。

指定Json结构

如果我们想让pets.species只能是猫/狗,我们就得在Prompt中描述这个限制。如果想创建更复杂的嵌套结构,自然语言描述起来会越来越费劲,大模型理解和生成的效果也会下降。

OpenAI的文档也说:JSON 模式不保证输出匹配任何特定模式,仅保证其有效且解析无错误。

那么如何指定复杂的Json结构并让大模型严格遵守呢?

Json Schema

Json Schema不仅能约定Json的结构,还能约定数据类型、文本规则等,也是大模型支持比较好的方式。

Prompt:生成一个虚拟人物,返回为Json数据,并严格遵循这个Json Schema: json { "type": "object", "properties": { "name": { "type": "string", "description": "三个字的名字" }, "age": { "type": "number", "default": "", "minimum": 18, "maximum": 30 }, "city": { "type": "string", "description": "必须是中国城市" }, "pets": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string" }, "species": { "type": "string", "enum": [ "猫", "狗" ] } } } } } }

这种方式的优点在于,不仅可以通过枚举、范围、正则来约定输出的结果,还可以通过description来加强AI的理解。

缺点主要有两个,一是Json Schema体积大,放在Prompt中很容易超过输入Token;二是Json Schema对非开发人员不太友好,这里推荐一个Json Schema 在线编辑器,可以很方便的设计Json Schema。

TypeScript(推荐)

在创新比赛中,我们发现可以使用TypeScript语法来约束DSL,AI的理解能力和生成效果都非常稳定。

改为TypeScript后的Prompt就变得非常精简:

Prompt:生成一个虚拟人物,返回为Json数据,并严格遵循这个interface:

ts复制代码export interface Person {
    name: string; // "三个字的名字"
    age: number; // 18-30
    city?: string; // "必须是中国城市"
    pets: {
        name: string;
        species: '猫' | '狗';
    }[];
}

目前为止,最推荐的方式就是TypeScript约束Prompt + Json模式生成,输出稳定,并且方便调优Prompt。

Json结构的问题

Json结构化数据虽然方便解析,但也存在几个严重的问题:

  1. 占用空间大,消耗Token多

    一个合法的Json包含大量的引号、逗号、引号,但是这些符号只是起分割作用,对AI并没有什么用,耗费了大量的Token。

  2. 无法支持流式解析

    试想一下一个Json字符串逐字打印出来,在打印完毕之前这个Json都是不合法的,这就无法动态解析,从而无法动态渲染前端组件,用户必须等待长时间的Loading才能看到最终的效果。

使用Yaml结构(推荐)

在我们的实验中,目前最好的解决方案是输出为Yaml结构,不仅拥有更小的体积,同时支持流式解析。

只需要关闭Json模式,并让AI返回Yaml即可,如下:

Prompt:生成一个虚拟人物,返回为Yaml数据,并严格遵循这个interface:

ts复制代码export interface Person {
    name: string; // "三个字的名字"
    age: number; // 18-30
    city?: string; // "必须是中国城市"
    pets: {
        name: string;
        species: '猫' | '狗';
    }[];
}

ChatGPT:以下是一个符合接口要求的虚拟人物的YAML数据:

yaml复制代码name: "赵雷"
age: 25
city: "北京"
pets:
  - name: "旺财"
    species: "狗"
  - name: "咪咪"
    species: "猫"

在这个例子中,Yaml输出比Json输出少了64个字符,体积减少了40%,在复杂结构中,可以省下更多的Token;并且支持流式解析,转换为增量可用的Json,以支持前端逐步渲染出每一部分的信息。

这种模式效果很好,只需要付出一些微小的代价 – 仍需要开发者从代码块中提取出Yaml文本。

总结

个人目前认为最好的方式依然是TypeScript约束Prompt + Yaml格式输出,适用于90%的结构化输出场景。 如果不缺钱,使用接口提供的Json Mode也是一种很好的选择。

希望这篇文章能给大家带来帮助,也欢迎一起讨论探索更好的方式。