Before
背景介绍
利于大语言模型来生成数据管理系统的开发代码。
数据管理系统,需要大量的CRUD页面,各模块页面结构、功能高度相似,只是字段不同。
每个模块页面功能基本可分为检索、列表、详情三部分。
在开发中,一部分工作就是把字段从PRD拷贝到代码中,且有相当一部分框架性代码是重复的。这两部分代码均可以通过大语言模型进行生成。
期望具备功能:
- 输入基础数据后,一键生成所有代码至项目中,项目启动后能看到正确展示的页面。
- 支持单个文件生成、覆盖项目中文件。
- 支持模型选择
实现方式
基于上述背景知识,这个应用对于最终生成的内容是有特定预期的,期望当输入相同时,最终的输出始终是稳定一致的。
它不需要模型有很高的创造力,而是需要严格遵循指令。
数据准备
对PRD提供的表格进行整理,得到检索、列表、详情三份表格。
每个表格包括展示字段、字段名称两个基本列,以及数据类型、是否可编辑、备注等其他列(每个字段不同的功能可以通过增加列配置不同值来实现)。
准备好基础数据之后,还需要模块名称。此外,因为希望能够直接将生成的代码放入项目中,还需要添加目录。
剩下的就是写prompt让大语言模型来生成代码了,当然还需要选择大语言模型,并且准备好key。
应用的基础信息输入部分如下图所示:
prompt编写
接下来的工作就是逐一生成代码并写入文件,这部分主要就是依靠大语言模型,所以需要编写prompt。
需要写的prompt数量理论上就是最终生成的新文件数量,也是需要调用大语言模型的次数。
每个prompt的整体框架其实是类似的,都是让大模型基于给出的表格信息、变量信息以及模版代码来生成代码。可以将通用的部分提取出来,便于复用,也有利于后期统一优化prompt。
可以尝试使用像Let's think step by step这样的提示,来优化生成的代码(与其说是优化,不如说是降低错误率,因为需要生成的代码内容是确定的)。
当然还有很多写prompt的注意事项和技巧,我们在实践中一点点挖掘。
下面举两个比较代表性的例子来展开说明。
生成枚举文件
这个文件中需要生成三个枚举值,分别是通过检索表格、列表表格和详情表格生成的。
查看prompt详情
根据下面表格、模版代码、变量生成一段ts代码,所有显示声明的ts类型都需要保留,模板代码中的常量内容不做任何变更,要求如下:
导出三个enum,分别为:
enum E_{转下划线大写(name)}_SEARCH_PARAMS,enum中的每个枚举成员为检索字段表格中的[转为大写(检索字段)]。
enum中的每个枚举成员的值为检索字段表格中的[转为大写(检索字段)]。
enum E_{转下划线大写(name)}_LIST_COLUMNS,enum中的每个枚举成员为列表字段表格中的[转为大写(列表字段)]。
enum中的每个枚举成员的值为列表字段表格中的[转为大写(列表字段)]。
enum E_{转下划线大写(name)}_DOC_ITEMS,enum中的每个枚举成员为详情字段表格中的[转为大写(详情字段)]。
enum中的每个枚举成员的值为详情字段表格中的[转为大写(详情字段)]。
在每个字段上方一行加/** 注释 */格式的注释,内容为该字段在下表中对应的字段名称。
模板代码:
export const enum E_{转下划线大写(name)}_SEARCH_PARAMS {
/** 字段名称 */
XX_XX = '[转为大写(检索字段)]';
}
export const enum E_{转下划线大写(name)}_LIST_COLUMNS {
/** 字段名称 */
XX_XX = '[转为大写(列表字段)]';
}
export const enum E_{转下划线大写(name)}_DOC_ITEMS {
/** 字段名称 */
XX_XX = '[转为大写(详情字段)]';
}
不生成任何import语句。
请逐步思考,给出完整的正确的代码。
name=ModuleName
列表字段表格:
| 列表字段 | 字段名称 | 数据类型 | 备注 |
|----------------|------|--------|-----|
| module_name_id | 模块ID | string | KEY |
检索字段表格:
| 检索字段 | 字段名称 | 检索类型 | 备注 |
|----------------|------|--------|-----|
| module_name_id | 模块ID | string | KEY |
详情字段表格:
| 详情字段 | 字段名称 | 可编辑 | 所占列数 | 备注 |
|----------------|------|-----|------|-----|
| module_name_id | 模块ID | 0 | 1 | KEY |
可以使用chatGPT或者其他大语言模型工具测试一下,输出内容基本能稳定如下(只截取生成内容中的代码部分):
export const enum E_MODULE_NAME_SEARCH_PARAMS {
/** 模块ID */
MODULE_NAME_ID = 'MODULE_NAME_ID'
}
export const enum E_MODULE_NAME_LIST_COLUMNS {
/** 模块ID */
MODULE_NAME_ID = 'MODULE_NAME_ID'
}
export const enum E_MODULE_NAME_DOC_ITEMS {
/** 模块ID */
MODULE_NAME_ID = 'MODULE_NAME_ID'
}
使用真实数据进行多次测试,结果也是比较稳定的。
调整过程
当使用真实数据时,这段prompt是比较长的,在openAI的Tokenizer页面能测出来,对于GPT来说这段文本一共有2000+的token数量。
对于那些8k、16k的模型,这个数量还是比较能够接受的,但是4k的模型就可能会有问题了(token不足无法生成完整的代码结果)。
所以最开始的方案是将enum文件进行拆分生成,拆成检索、列表以及详情三块,每一块各自写prompt调用大语言模型得到代码,最后再进行拼接。
这样每个prompt只需要有一个表格的数据内容,token数量就会大大减少。
当然最后还是选择了上述方案,首先目前模型支持的token数量越来越大,其次按现在以token数量计费的模式,上述方案在总体token数量上来说还是更少的,也就意味着更便宜。
生成线上列表
这个文件大部分都是固定的代码,只需要将模块名称替换进去。但其中存在两个数组变量,其值是需要根据检索表格和列表表格进行生成。 因为这两部分代码处于中间位置,基本上无法拆分,所以需要在一个prompt中生成完整的代码。
查看prompt详情
根据下面表格、模版代码、变量生成一段ts代码,所有显示声明的ts类型都需要保留,模板代码中的常量内容不做任何变更,要求如下:
导出一个component,名称为{name}PreviewList。
component中的setup函数中的内容补全规则如下formItemConfig数组值来自于检索字段表格中的[E_转大写下划线{name}_SEARCH_PARAMS].[大写(检索字段)],以及4个来自E_BASE_SEARCH_PARAMS的基本字段。
columns数组值来自于列表字段表格中的[E_转大写下划线{name}LIST_COLUMNS].[大写(列表字段)],以及5个来自E_BASE_TABLE_COLUMN的基本字段。
常量定义完成,最后return一个组件,名称为Base{name}ListPage。
模板代码:
export default defineComponent({
name: '{name}PreviewList',
setup() {
const formItemConfig = [
E{转下划线大写(name)}SEARCH_PARAMS.{大写(检索字段表格中的检索字段名)},
E_BASE_SEARCH_PARAMS.CREATED_BY,
E_BASE_SEARCH_PARAMS.CREATED_TIME_RANGE,
E_BASE_SEARCH_PARAMS.UPDATED_BY,
E_BASE_SEARCH_PARAMS.UPDATED_TIME_RANGE,
];
const columns = [
E{转下划线大写(name)}_LIST_COLUMNS.{大写(列表字段)},
E_BASE_TABLE_COLUMN.CREATED_TS,
E_BASE_TABLE_COLUMN.CREATED_BY,
E_BASE_TABLE_COLUMN.UPDATED_TS,
E_BASE_TABLE_COLUMN.UPDATED_BY,
E_BASE_TABLE_COLUMN.DATA_STATUS,
];
return () => {
return (
<Base{name}ListPage
formItemConfig={formItemConfig}
columns={columns}
></Base{name}ListPage>
);
};
},
});
保留模板中的import语句。
请逐步思考,给出完整的正确的代码。
name=TranslationalMedicine
列表字段表格:
| 列表字段 | 字段名称 | 数据类型 | 备注 |
|----------------|------|--------|-----|
| module_name_id | 模块ID | string | KEY |
检索字段表格:
| 检索字段 | 字段名称 | 检索类型 | 备注 |
|----------------|------|--------|-----|
| module_name_id | 模块ID | string | KEY |
使用以上prompt进行生成,得到的结果还是比较稳定的,但是使用真实数据时产生了很多问题。
- 根据表格生成的数据有字段遗漏
- 默认写在数组中的几个值在生成的代码中被遗漏
- 个别字段的enum没有根据规则生成,而是使用了默认写在数组中的值的enum,如下:
const formItemConfig = [
E_MODULE_NAME_SEARCH_PARAMS.MODULE_NAME_ID,
// 字段不全
E_BASE_SEARCH_PARAMS.PUB_DT, // 应该是E_MODULE_NAME_SEARCH_PARAMS.PUB_DT
// 几个默认值遗漏
]
多次测试的过程中,均有上述一个或多个问题出现。
调整过程
经过调试,发现主要是受检索表格中其他列数据的影响(真实的检索表格为了根据字段类型生成不同的检索框,添加了很多字段进行控制)。
虽然在生成线上列表的prompt中没有使用这些列,但生成的代码还是受到了这部分数据的影响。
最后将多余的列数据从prompt中删除,问题1、3基本不会再复现,但是问题2还是重复出现。
使用gpt-4-turbo模型、claude系列模型能解决问题二,gpt-3-turbo模型情况,问题二基本是稳定复现。
其它调整
import语句
生成代码是,大语言模型通常会自动添加import语句,然而这些语句显然不是我们期望的。
因为我们希望生成的文件可以直接运行,所以就需要将所有的import语句都写在模版代码中(如果有import新生成文件的语句,则需要写入变量),
并且在prompt中注明"保留模板中的import语句。"。
这样的prompt效果非常稳定,但是import语句也会占相当一部分token。
最初的设想是先生成代码,然后在项目中手动导入import语句。你可以在prompt中注明"不生成任何import语句。",同样效果也比较稳定。
但是如果你的模板代码中有一部分import语句,你希望大语言模型保留这部分语句,同时不会自己生成其它的import语句,
那么我还没有找到一款效果稳定的prompt。
此外,因为一部分import语句导入的新生成的文件,所以文件名以及路径也可能是需要用变量替换的,
这个时候会存在大小写的问题,在prompt中尽量显示地将大小写声明出来,避免错误。或者在设计时就将文件夹和文件名的格式进行统一。
表格列数据
当写入prompt中的表格有很多列,即使你写明只使用某几列,也无法避免多余的列数据对结果产生影响。
所以所有prompt均使用统一的表格数据是不合理的,都应该根据使用到的列进行数据修剪。降低token数量,同时提高生成代码的稳定性。
特殊错误
调整到最后还剩下一些难以解释的错误,这些错误内容就需要在prompt中写入一些有针对性的特殊说明来进行处理。
实现总结
整体的实现上除了一些前后端代码的编写,整体工程的实现,大部分工作就是调整prompt。
调prompt其实是一个非常纠结的过程,因为大语言模型自身的特性,会发生很多意想不到的情况,针对这些情况调整prompt可能又会导致新的问题出现。
而当你找到一个效果非常好的prompt,又是很有成就感的一件事情。
总的来说就是需要不断尝试、总结,摸索出大语言模型喜欢的prompt模式。
实现效果
使用大语言模型来帮助你生成代码是一件非常简单的事情。但如果你需要进行工程化,让其能够持续地为你提供稳定准确的代码生成服务,就需要花费一些功夫。
version 0.9
- 生成的内容可能存在代码以外的文本
- 某些遍历列表行生成的内容存在错漏
- 有拼写错误(patsnap --> patsna)
- 自动增加ts显示类型,且不正确(受上下文影响)
- 大小写错误
- 针对某些字段进行特殊处理,会添加无用的filter语句
解决方案:
- 通过正则匹配代码部分内容写入文件(不同模型的表现不同,也可以尝试通过prompt指令使输出内容只包含最终生成代码)
- 基本可以通过筛选输入表格列,至只保留需要列数据来解决
- 对这种词在prompt中进行显示提示(prompt增加:PATSNAP为项目专有名词,不要更改)
- 目前为止只出现一次
- 在示例中通过"XX_XX"形式表明是大写
- ?? 对于一些无法理解的错误,需要添加特殊的说明,来对生成结果进行修正。
version 1.0
对生成的内容进行完整性校验:
代码的完整性、针对表格生成内容的完整性(是否有字段遗漏)、拼写错误、大小写错误
支持批量重新生成。
针对不同难易程度的任务调用不同的大语言模型,来降低生成时间和成本。
根据不同模型对不同任务的效果,对不同任务选择不同的模型。
version 2.0
实现列表和表单的自动生成
总结
在有了这个idea之后的实践过程中,一开始觉得很容易,在chatGPT上面写段prompt生成代码,大部分都能一次出不错的结果,其余的经过一些调整,一两次重试就能得到理想的代码。
然后搭了项目将这个idea做工程化的实现,添加各种各样的辅助功能,到完成了一键生成所有代码,困难的部分就出现了。
一键生成这个按钮点起来非常happy,但是当进入项目去看生成的一个个文件,点一个红一个的时候,真的很无语,只能挨个去调整优化prompt。
其实回过头来看大部分都是prompt不够准确或者有多余信息导致的问题,但是排查起来还是蛮费劲的。
对一些不知道为啥会出现的怪问题,就得为它们量身定制prompt,还好这样的问题并不多。
最后剩下一些偶现问题,就得通过工程方式进行处理,加上校验和重试机制,来保证代码的准确率。
最后的最后一键生成、启动项目、查看页面效果一气呵成还是蛮有成就感的。
In general, if you find that a model fails at a task and a more capable model is available, it's often worth trying again with the more capable model.
确实没有必要对着一个模型死磕,有时候可能你的prompt已经比较完善了,但是模型本身的能力不足,你可能需要优化几十次才能与这个模型相匹配,
但换一个更强大的模型,可能直接就成功了。
现状是模型越来越多,能力越来越强大,所以在考虑成本的基础上,选择更加强大的模型。
使用英文prompt
各个模型比较
下面是针对gpt、claude以及gemini几个模型各自完成任务时,在价格、消耗token数量、时长以及生成结果三个方面进行对比的结果。每个模型各自进行了10次测试。记录如下图:
效果比较 加个比较
遇到的问题
import语句引入 表格没有使用的字段会影响输出
提示工程
Prompt设计的基本原则,是Prompt应当和大模型的高质量训练数据分布尽可能一致
https://zhuanlan.zhihu.com/p/660369244 Effective Prompt: 编写高质量Prompt的14个有效方法 https://www.jiqizhixin.com/articles/2024-03-18-6 大模型能自己优化Prompt了,曾经那么火的提示工程要死了吗?
避免使用语法结构过于复杂、语义模糊不清、逻辑混乱的语言。
避免任何歧义、语病、拼写/标点错误的存在。
正面描述任务的具体要求,尽量避免使用否定句。
用分割符,“```”、“"""”、“<tag>” 之类的分隔符将不同部分的内容进行分隔
指定角色
重复重要的需求(头尾处)
存在的问题
未来展望
使用项目代码进行训练??
优化prompt,减少token数量,降低成本
当大语言模型的能力越来越强大,更多的问题可以通过prompt完全解决,而不再需要工程上针对各个问题单独进行解决。
根据界面UI图直接生成代码。
使用工具
Reference
1. Effective Prompt: 编写高质量Prompt的14个有效方法
2. Prompt engineering