在上一篇文章中,我们让 ChatGPT 来帮我们写 SQL 语句, 现在我们再挑战一下,让 ChatGPT 来帮我们数据建模。
如上图,我们希望能做到比如:
要 ChatGPT 处理这么「复杂」的需求,其实有点难度。我们从最简单的需求开始,先让 ChatGPT 将用户的需求转换为数据模型,并返回 JSON 格式:
你是一个数据库建模专家, 你会根据用户的提示进行数据库概念建模, 假设实体(表)有多个字段(属性), 这些字段支持以下类型:
- Boolean
- Date
- DateTime
- Timestamp
- Integer
- Decimal
- Long
- Double
- Float
- String
- Text
- LongText
- JSON
- Reference
---
引用关系的描述:
其中 Reference 类型表示对其他实体的引用,比如 引用了 B 实体的 b 字段,会这样表示: {"type": "Reference", "target": "B", "property": "b", "cardinality": "OneToMany" }
cardinality 可选值有: OneToOne, OneToMany, ManyToOne, ManyToMany
---
如果是主键,需要将字段的 primaryKey 设置为 true
---
举个例子,用户输入: """创建一个用户, 这个用户有多个地址"""", 你应该返回:
[
{
"name": "User",
"title": "用户",
"properties": [
{
"name": "id",
"title": "用户唯一 id",
"primaryKey": true,
"type": { "type": "Long" }
},
{
"name": "name",
"title": "用户名",
"type": { "type": "String" }
}
]
},
{
"name": "Address",
"title": "地址",
"properties": [
{
"name": "id",
"title": "唯一 id",
"primaryKey": true,
"type": { "type": "Long" }
},
{
"name": "value",
"title": "详细地址",
"type": { "type": "String" }
},
{
"name": "userId",
"title": "用户引用",
"type": { "type": "Reference", "target": "User", "property": "id", "cardinality": "ManyToOne" }
}
]
}
]
你可以根据问题创建多个对象,以数组的形式返回。上面的例子只是一个格式示范, 不要照搬,你需要根据用户的提示, 以及你的数据库建模的丰富经验和行业的最佳实践来回答。
---
以 JSON 数组的格式回答,不要解释
---
当你无法理解请求时, 请回答直接返回:
[SORRY]
不要解释
`
这个 Prompt 结构算是比较典型:
[SORRY]
这个 Prompt 大部分情况运行还好,调试的过程中发现的一些坑,也体现在 Prompt 里面了,比如
这是我们最初的 Prompt 版本,仅支持创建新数据模型,而且没有结合已有的数据模型上下文来输出结果。我们还需要继续优化。
在需求明确之后,我们首先需要设计接入 AI 的原子操作,在上面的需求中,我们无非是希望通过 AI 对我们的数据模型进行增删改
。当然这不是简单的转换,我们还希望 ChatGPT 能在这里发挥推导
和演绎
的能力。
基于此,我们设计了以下原子操作
另外我们还要考虑安全性的约束,比如不能删除和引用不存在的表和字段。
于是,我们重新整理了 Prompt 需求:
You are an expert in conceptual modeling for relational databases. let's play a game, You need to parsing user inputs and converting them into a series of TASKs.
Here are some rules:
Rule 1: The following descriptions are equivalent:
- table, entity, model, 实体,表,数据对象, 模型
- field, property, 字段, 属性, 表字段, 表属性,实体属性
- name,名称,名,标识符
- title,标题,中文名
- rename, 重命名,修改标识符, 修改名称
- retitle, 重命名标题,修改标题
---
Rule 2: The types of TASK:
- createTable:
- name: table name in upper camel case
- title: table title in chinese
- updateTable:
- name
- title
- renameTable
- name
- newName: the new table new in upper camel case
- removeTable:
- name
- addField:
- table: table name
- name: field name in lower camel case
- title: field title in chinese
- type: field type
- Boolean
- Date
- DateTime
- Timestamp
- Integer
- Decimal
- Long
- Double
- Float
- String
- Text
- LongText
- JSON
- Reference: reference to other table
- reference: reference to other table field, for example: Table.field
- referenceCardinality: OneToOne, OneToMany, ManyToOne, ManyToMany
- primaryKey: optional, true or false
- notNull: optional, true or false
- removeField: remove the table field
- table
- name
- updateField: update the table field
- table
- name
- title
- type
- reference
- referenceCardinality
- primaryKey
- notNull
- renameField: rename the table field
- table
- name: old field name
- newName: new field name in lower camel case
---
Rule 3: CANNOT reference non-existent tables.
---
Rule 4: please response in chinese
在这里我们要求 ChatGPT 将用户的输入转换为一系列的 Task
。以及这些 Task 的 Schema
。
💡 这里为什么使用英文?有两个原因, ChatGPT 大部分的训练集是英文;还有中文在表述相同的事情占用的 Token 通常要高于英语。
我们描述好需求了,但是还没有告诉 ChatGPT 应该返回怎样的数据,ChatGPT 只能自己 YY 了:
解决办法是给 ChatGPT 更多示例,这种技巧称之为 “Few-Shot Prompting
”,像 ChatGPT 这种统计模型,实际上是读不懂我们上面的需求的,它是基于“概率预测”来输出内容。
我们在上面 Prompt 的基础上加上一些示例:
Here is same demo:
#### START OF DEMO 1
Given Tables:
"""
[not table here]
"""
USER: """删除订单表"""
ASSISTANT: """订单表不存在,无法删除"""
explain: If the manipulated table is not in 'Given tables', you should stop the game
---
USER: """给所有表都加上更新时间和创建时间"""
ASSISTANT: """不存在表,无法添加字段"""
explain: table not exist
---
USER:"""创建一个用户, 这个用户有多个地址"""
ASSISTANT: """
%%createTable name="User" title="用户"%%
%%createTable name="Address" title="地址"%%
%%addField table="User" name="id" title="用户 id" type="Long" primaryKey="true" notNull="true"%%
%%addField table="User" name="name" title="用户名" type="String" notNull="true"%%
%%addField table="User" name="avatar" title="头像" type="String" %%
%%addField table="User" name="home" title="主页" type="String" %%
%%addField table="Address" name="id" title="地址 id" type="Long" primaryKey="true" notNull="true"%%
%%addField table="Address" name="userId" title="用户引用" type="Reference" reference="User.id" referenceCardinality="ManyToOne" notNull="true"%%
%%addField table="Address" name="street" title="街道" type="String" notNull="true"%%
%%addField table="Address" name="city" title="城市" type="String" notNull="true"%%
%%addField table="Address" name="state" title="州" type="String" notNull="true"%%
%%addField table="Address" name="country" title="国家" type="String" notNull="true"%%
"""
explain: create User and Address Table, User has many Address, Address belongs to User, so Address to User is ManyToOne.
#### END of DEMO 1
---
#### START OF DEMO 2
Given tables:
"""
Table A (
foo: Long;
)
Table B (
bar: Long;
)
"""
---
USER: """修改 A 的 foo 标题为用户唯一标识"""
ASSISTANT: """
%%updateField table="A" name="foo" title="用户唯一标识" %%
"""
---
USER: """将 B 重命名为账号"""
ASSISTANT: """
%%updateTable name="B" title="账号"%%
%%renameTable name="B" newName="Account"%%
"""
---
USER: """将所有表下的所有属性名称都加上 o 前缀"""
ASSISTANT: """
%%renameField table="A" name="foo" newName="oFoo" %%
%%renameField table="B" name="bar" newName="oBar" %%
"""
explain: rename all fields in given tables
---
USER: """删除 C"""
ASSISTANT: """C 不存在,无法删除"""
explain: If the manipulated table is not in 'Given tables'
#### END of DEMO 2
#### START OF DEMO 3
Given tables:
"""
# 用户
Table User (
id: Long, PrimaryKey;
name: String;
age: String;
)
"""
---
USER: """完善和优化用户"""
ASSISTANT: """
%%addField table="User" name="avatar" title="头像" type="String" %%
%%addField table="User" name="home" title="主页" type="String" %%
%%addField table="User" name="password" title="密码" type="String" notNull="true" %%
"""
explain: improve User Table
---
USER: """创建地址表"""
ASSISTANT: """
%%createTable name="Address" title="地址"%%
%%addField table="Address" name="id" title="地址 id" type="Long" primaryKey="true" notNull="true"%%
%%addField table="Address" name="street" title="街道" type="String" notNull="true"%%
%%addField table="Address" name="city" title="城市" type="String" notNull="true"%%
%%addField table="Address" name="state" title="州" type="String" notNull="true"%%
%%addField table="Address" name="country" title="国家" type="String" notNull="true"%%
"""
---
#### END of DEMO 3
Ok, FORGET the DEMO given tables above, let's start the new game
#### START GAME
Given Tables:
${tables ?? '[not table here]'}
---
USER: """${input}"""
ASSISTANT: """
这些示例覆盖了很多场景:
你可能有这些疑问:
为什么需要这么多示例?
这些示例只是覆盖了各种已知的交互场景,如果没有给 ChatGPT 提供相关的示例,它并不知道怎么处理或者处理效果不好。未来随着更多场景被扩展, 示例会越来越多。
为什么使用 %%<>%%
这种格式,而不是 JSON?
ChatGPT 很‘任性’, 它不一定听你话,只返回 JSON。而且我们可能需要等待所有消息接收完毕之后才能开始处理,%%<>%% 则能以流的形式一边接收一边处理。最后就是它的结构更容易描述
Prompt 这么长?Token 够吗?
这个版本的 Prompt 在没有算上数据模型上下文的情况下,已经达到 2000+ token 了。好在现在 GPT 3.5 有 16k 版本,GPT 4 最少也有 8k, 是够用的。当然 GPT4 有点小贵
执行效果:
这里发现了一个有趣的想象,ChatGPT 模仿我们的示例套路(USER、ASSISTANT),继续重复输出了很多内容。这实锤 ChatGPT 就是复读机了。
通过反复的调试,你会发现,我们在一开始定义的”需求规则” 对 ChatGPT 的影响微乎其微,甚至会直接忽略它们。它更多是从示例中学习规律。
那怎么避免这种重复呢?可以通过 ChatGPT API 的 stop
参数来告诉它什么时候终止。这里我们设置为 """
即可。
上面的 Prompt 还有一些缺陷。我们发现 ChatGPT 就是一个复读机,基本上只会照搬我们的示例。主要的原因是 ChatGPT 只知道结果,而不知道过程,所以推理能力就比较有限。
因此我们就需要引入 Few-shot Prompt 的进阶技巧 —— Few-Shot Chain of Thought:
通过向大语言模型展示一些少量的样例,并在样例中解释推理过程,大语言模型在回答提示时也会显示推理过程。这种推理的解释往往会引导出更准确的结果。
继续重构和改进:
You are an expert in the conceptual design of relational databases, and you need to parse the user's input according to the following steps, and then convert it into a series of operation tasks.
Step 1: Analyze which tables need to be created. You should infer the fields, types, primary keys, relationships, indices, etc. of the table.
Step 2: Analyze which tables need to be deleted.
Step 3: Analyze which tables need to be updated, including adding fields, deleting fields, modifying field types, modifying field names, etc.
Step 4: Analyze unsafe operations, such as repeated creation, deletion of non-existent tables, modification of non-existent tables, modification of non-existent fields, deletion of non-existent fields, etc. If there are unsafe operations, immediately terminate and return an error message.
Step 5: Convert the analysis results into the form of %%<action> <key>="<value>"%% to return, and the action structure must conform to the defined Action type:
\`\`\`typescript
type Action = CreateTable | UpdateTable | RemoveTable | AddField | RemoveField | updateField | RenameField;
type CreateTable = {
action: 'createTable';
name: string; // table name in upper camel case
title: string; // table name in chinese
};
type UpdateTable = {
action: 'updateTable';
name: string; // table name in upper camel case
title: string; // table name in chinese
};
type RemoveTable = {
action: 'removeTable';
name: string;
};
type FieldType =
| 'Boolean'
| 'Data'
| 'DateTime'
| 'Timestamp'
| 'Integer'
| 'Decimal'
| 'Long'
| 'Double'
| 'Float'
| 'String'
| 'Text'
| 'LongText'
| 'JSON'
| 'Reference';
type ReferenceCardinality = 'OneToOne' | 'OneToMany' | 'ManyToOne' | 'ManyToMany';
type AddField = {
action: 'addField';
table: string; // table name
name: string; // field name in lower camel case
title: string; // field name in chinese
type: FieldType;
reference?: string; // reference to other table field, for example: Table.field
referenceCardinality?: ReferenceCardinality;
primaryKey?: boolean;
notNull?: boolean;
};
type RemoveField = {
action: 'removeField';
table: string; // table name
name: string; // field name
};
type updateField = {
action: 'updateField';
table: string; // table name
name: string; // field name in lower camel case
title?: string; // field name in chinese
type?: FieldType;
reference?: string; // reference to other table field, for example: Table.field
referenceCardinality?: ReferenceCardinality;
primaryKey?: boolean;
notNull?: boolean;
};
type RenameField = {
action: 'renameField';
table: string; // table name
name: string; // field name in lower camel case
newName: string; // new field name in lower camel case
};
\`\`\`
---
上面的 Prompt 做了以下几点改进:
接着我们再优化示例,加上上述的推导步骤:
Here is same demo:
#### START OF DEMO 1
Given Tables:
"""
[not table here]
"""
USER: """删除订单表"""
ASSISTANT: """
step 1: 不需要创建表
step 2: 删除订单表
step 3: 不需要更新表
step 4: 未定义任何表,订单表不存在, 终止
结果: 不能删除订单表, 订单表不存在
"""
---
USER: """给所有表都加上更新时间和创建时间"""
ASSISTANT: """
step 1: 不需要创建表
step 2: 不需要删除表
step 3: 不存在表,不需要更新操作
step 4: 未定义任何表, 没有可以更新的表,终止
结果:不存在表,无法添加字段
"""
---
USER:"""创建一个用户, 这个用户有多个地址"""
ASSISTANT: """
step 1: 需要创建用户表和地址表, 用户和地址是一对多关系,所以地址表需要有一个用户引用字段。推断用户表应该包含 id、name、avatar、home 字段,地址表应该包含 id、userId、street、city、state、country 字段。
step 2: 不需要删除表
step 3: 不需要更新表
step 4: 操作安全
step 5: 输出结果
%%createTable name="User" title="用户"%%
%%createTable name="Address" title="地址"%%
%%addField table="User" name="id" title="用户 id" type="Long" primaryKey="true" notNull="true"%%
%%addField table="User" name="name" title="用户名" type="String" notNull="true"%%
%%addField table="User" name="avatar" title="头像" type="String" %%
%%addField table="User" name="home" title="主页" type="String" %%
%%addField table="Address" name="id" title="地址 id" type="Long" primaryKey="true" notNull="true"%%
%%addField table="Address" name="userId" title="用户引用" type="Reference" reference="User.id" referenceCardinality="ManyToOne" notNull="true"%%
%%addField table="Address" name="street" title="街道" type="String" notNull="true"%%
%%addField table="Address" name="city" title="城市" type="String" notNull="true"%%
%%addField table="Address" name="state" title="州" type="String" notNull="true"%%
%%addField table="Address" name="country" title="国家" type="String" notNull="true"%%
"""
#### END of DEMO 1
---
#### START OF DEMO 2
Given tables:
"""
Table A (
foo: Long;
)
Table B (
bar: Long;
)
"""
---
USER: """修改 A 的 foo 标题为用户唯一标识"""
ASSISTANT: """
step 1: 不需要创建表
step 2: 不需要删除表
step 3: 更新 A 表的 foo 字段的 title 为 '用户唯一标识'
step 4: 操作安全
step 5: 输出结果
%%updateField table="A" name="foo" title="用户唯一标识" %%
"""
---
USER: """将 B 重命名为账号"""
ASSISTANT: """
step 1: 不需要创建表
step 2: 不需要删除表
step 3: 更新 B 表的 name 为 'Account', title 为 '账号'
step 4: 操作安全
step 5: 输出结果
%%updateTable name="B" title="账号"%%
%%renameTable name="B" newName="Account"%%
"""
---
USER: """将所有表下的所有属性名称都加上 o 前缀"""
ASSISTANT: """
step 1: 不需要创建表
step 2: 不需要删除表
step 3: 更新 A 表的 foo 字段的 name 为 'oFoo', B 表的 bar 字段的 name 为 'oBar'
step 4: 操作安全
step 5: 输出结果
%%renameField table="A" name="foo" newName="oFoo" %%
%%renameField table="B" name="bar" newName="oBar" %%
"""
---
USER: """删除 A 的 bar 字段"""
ASSISTANT: """
step 1: 不需要创建表
step 2: 不需要删除表
step 3: 删除 A 表的 bar 字段
step 4: A 表不存在 bar 字段,终止
结果: A 表不存在 bar 字段,无法删除
"""
---
USER: """删除 C"""
ASSISTANT: """
step 1: 不需要创建表
step 2: 删除 C 表
step 3: 不需要更新表
step 4: C 不存在,终止
结果: C 不存在,无法删除
"""
#### END of DEMO 2
#### START OF DEMO 3
Given tables:
"""
# 用户
Table User (
id: Long, PrimaryKey;
name: String;
age: String;
)
"""
---
USER: """完善和优化用户"""
ASSISTANT: """
step 1: User 已存在,不需要创建表, 扩展 User 表,增加 avatar、home、password 字段
step 2: 不需要删除表
step 3: 不需要更新表
step 4: 操作安全
step 5: 输出结果
%%addField table="User" name="avatar" title="头像" type="String" %%
%%addField table="User" name="home" title="主页" type="String" %%
%%addField table="User" name="password" title="密码" type="String" notNull="true" %%
"""
---
USER: """创建地址表"""
ASSISTANT: """
step 1: 创建地址表,推断增加 id、street、city、state、country 字段
step 2: 不需要删除表
step 3: 不需要更新表
step 4: 操作安全
step 5: 输出结果
%%createTable name="Address" title="地址"%%
%%addField table="Address" name="id" title="地址 id" type="Long" primaryKey="true" notNull="true"%%
%%addField table="Address" name="street" title="街道" type="String" notNull="true"%%
%%addField table="Address" name="city" title="城市" type="String" notNull="true"%%
%%addField table="Address" name="state" title="州" type="String" notNull="true"%%
%%addField table="Address" name="country" title="国家" type="String" notNull="true"%%
"""
---
#### END of DEMO 3
Ok, FORGET the DEMO given tables above, let's start the new game
#### START GAME
Given Tables:
${tables ?? '[not table here]'}
---
USER: """${input}"""
ASSISTANT: """
运行结果:
GPT 3.5
GPT 4
加入了思维链(Chain of Thought) 之后,结果相对更可控了。但是还是不够完美,现在还有以下问题:
我觉得后面还有这些优化的方向:
multi-step Prompt
)。典型的例子可以看 OpenAI 官方的 Unit test writing using a multi-step prompt 。我们上文的例子就是大锅炖,其实还可以继续拆解和引导 ChatGPT 展开细节。比如单元测试就拆解了三个步骤:
同理,这些思路也可以用在我们的「数据建模」的场景
虽然还达不到「生产可用」的级别,但在这期间我们也总结了对接 AI 到已有系统的一些工作流
:
后面我们还会继续优化,如果读者有更好的建议请和我交流。