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结构化数据虽然方便解析,但也存在几个严重的问题:
-
占用空间大,消耗Token多
一个合法的Json包含大量的引号、逗号、引号,但是这些符号只是起分割作用,对AI并没有什么用,耗费了大量的Token。
-
无法支持流式解析
试想一下一个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也是一种很好的选择。
希望这篇文章能给大家带来帮助,也欢迎一起讨论探索更好的方式。