AI 调教师:绘制 ER 图

在上一篇文章中,我们让 ChatGPT 来帮我们写 SQL 语句, 现在我们再挑战一下,让 ChatGPT 来帮我们数据建模。

如上图,我们希望能做到比如:

  • 创建数据模型,让 ChatGPT 帮我们推断表应该包含的字段、字段类型、主键、索引、表之间的关联关系等等
  • 优化数据模型。对现有的数据模型进行扩展和优化
  • 对数据模型及其字段进行增删改。
  • ...

要 ChatGPT 处理这么「复杂」的需求,其实有点难度。我们从最简单的需求开始,先让 ChatGPT 将用户的需求转换为数据模型,并返回 JSON 格式:

markdown 复制代码
你是一个数据库建模专家, 你会根据用户的提示进行数据库概念建模, 假设实体(表)有多个字段(属性), 这些字段支持以下类型:

- 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 结构算是比较典型:

  • 角色定义。数据库建模专家
  • 任务。将用户需求转换为概念模型
  • 规则。字段的类型,引用关系的描述,主键
  • 输出规则。输出 JSON,如果失败就返回 [SORRY]
  • 示例。

这个 Prompt 大部分情况运行还好,调试的过程中发现的一些坑,也体现在 Prompt 里面了,比如

  • 我们想让它返回 JSON 格式,但是它可能会夹带一些解释,导致没办法直接 JSON.parse
  • 它可能会直接照搬我们给它的示例

这是我们最初的 Prompt 版本,仅支持创建新数据模型,而且没有结合已有的数据模型上下文来输出结果。我们还需要继续优化。

设计原子操作

在需求明确之后,我们首先需要设计接入 AI 的原子操作,在上面的需求中,我们无非是希望通过 AI 对我们的数据模型进行增删改。当然这不是简单的转换,我们还希望 ChatGPT 能在这里发挥推导演绎的能力。

基于此,我们设计了以下原子操作

  • 表操作
    • 新增表
    • 修改表
    • 重命名表
    • 删除表
  • 字段操作
    • 创建字段
    • 修改字段
    • 重命名字段
    • 删除字段

另外我们还要考虑安全性的约束,比如不能删除和引用不存在的表和字段。

于是,我们重新整理了 Prompt 需求:

markdown 复制代码
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 通常要高于英语。

Few-shot Prompting

我们描述好需求了,但是还没有告诉 ChatGPT 应该返回怎样的数据,ChatGPT 只能自己 YY 了:

解决办法是给 ChatGPT 更多示例,这种技巧称之为 "Few-Shot Prompting",像 ChatGPT 这种统计模型,实际上是读不懂我们上面的需求的,它是基于"概率预测"来输出内容。

我们在上面 Prompt 的基础上加上一些示例:

markdown 复制代码
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

通过向大语言模型展示一些少量的样例,并在样例中解释推理过程,大语言模型在回答提示时也会显示推理过程。这种推理的解释往往会引导出更准确的结果。

继续重构和改进:

markdown 复制代码
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 做了以下几点改进:

  • 我们拆解了整个推导的过程:
    • 第一步:识别需要创建的表格,以及对应的字段、字段类型、主键、关联关系等等
    • 第二步:识别需要删除的表格
    • 第三步:识别需要更新的字段
    • 第四步:识别不安全的操作,如果存在不安全的操作就终止生成
    • 第五步:将上面的结果转换为 DSL。
  • 我们使用 Typescript 来描述 DSL 的 Schema。我在上一篇文章**AI 调教师:聊聊 TypeChat 以及 ChatGPT 形式化输出 大概介绍过 ChatGPT 的形式化输出能力。ChatGPT 可能很好地识别 Typescript

接着我们再优化示例,加上上述的推导步骤:

markdown 复制代码
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) 之后,结果相对更可控了。但是还是不够完美,现在还有以下问题:

  • Token 占用过大。尽管我们可以选择支持更大 Token 的模型,然而我们还要考虑未来为更多场景的加入示例、对话历史需要预留的空间,还有就是成本问题。
  • 不支持多轮对话或者用户纠正机制。这个需要从交互上进行优化,比如提供执行确定,重新生成等等
  • 回答效果还有待优化。并没有发挥出 ChatGPT 强大的联想能力,其实这是一件挺矛盾的事情,示例不够,GPT 输出的结果可能不符合要求,多了有可能抑制它的「创造性」?

我觉得后面还有这些优化的方向:

  1. 转换为多步骤 Prompt(multi-step Prompt)。典型的例子可以看 OpenAI 官方的 Unit test writing using a multi-step prompt 。我们上文的例子就是大锅炖,其实还可以继续拆解和引导 ChatGPT 展开细节。比如单元测试就拆解了三个步骤:

    1. Explain(解释): 给一个 Python 函数,让 GPT 解释它做了什么,为什么这么做.
    2. Plan(计划): 让 GPT 列举针对这个函数单元测试的计划。如果这个计划太短,我们就请 GPT 进一步阐述更多单元测试的思路
    3. Execute(执行): 最后,让 GPT 将上述计划的用例转换为单元测试

    同理,这些思路也可以用在我们的「数据建模」的场景

  2. 模型微调(Fine-tune)。上面的 Prompt 已经列举了较多的示例,已经占据了很多 Token 空间,如果要达到生产可用的级别,我们还需要积累更多的样本,微调出专用的模型是更好的选择。

  3. 使用更高级的模型。比如 GPT 4 的效果会比 3.5 好上很多

虽然还达不到「生产可用」的级别,但在这期间我们也总结了对接 AI 到已有系统的一些工作流

  • 确定需求。
  • 系统原子操作的拆解。我们的主要工作就是让 AI 将用户的自然语言需求转换为这些原子操作。
  • 设计 DSL。设计 AI 输出的 DSL 形式,需要兼顾 AI 和已有系统的处理能力
  • 推理过程步骤设计。让 AI 模型知道结果是如何得到的,可以提升输出结果的准确性。
  • 提供案例。案例越多,输出的结果就越可控
  • 调优。这个调试的过程很难一步到位,就是测试、重建、测试、重建的过程。而且我们很难对它进行单元测试诸如此类的。

后面我们还会继续优化,如果读者有更好的建议请和我交流。

相关推荐
小小吱4 分钟前
HTML动画
前端·html
OptimaAI7 分钟前
【 LLM论文日更|检索增强:大型语言模型是强大的零样本检索器 】
人工智能·深度学习·语言模型·自然语言处理·nlp
谢眠12 分钟前
机器学习day4-朴素贝叶斯分类和决策树
人工智能·机器学习
HelpHelp同学16 分钟前
教育机构内部知识库:教学资源的集中管理与优化
人工智能·知识库软件·搭建知识库·知识管理工具
深度学习lover23 分钟前
<项目代码>YOLOv8 番茄识别<目标检测>
人工智能·python·yolo·目标检测·计算机视觉·番茄识别
IT古董23 分钟前
【机器学习】机器学习中用到的高等数学知识-1.线性代数 (Linear Algebra)
人工智能·python·线性代数·机器学习
糊涂涂是个小盆友26 分钟前
前端 - 使用uniapp+vue搭建前端项目(app端)
前端·vue.js·uni-app
飞腾开发者31 分钟前
飞腾平台Arm NN软件栈安装使用指南
linux·运维·人工智能·机器学习·计算机视觉
浮华似水1 小时前
Javascirpt时区——脱坑指南
前端
王二端茶倒水1 小时前
大龄程序员兼职跑外卖第五周之亲身感悟
前端·后端·程序员