项目体验地址:dbfu.github.io/easy-builde...
项目仓库地址:github.com/dbfu/easy-b...
功能还比较简陋,后面慢慢完善,大家可以先体验一下AI助手功能。
目前插件是和低代码项目放在一起,后面我会给单独拆出来,让想用这个插件的朋友也能在自己项目里快速使用。目前物料使用的是官方的基于antd的物料,我只改了几个组件,让组件对外暴露了方法和值。
前言
上一篇分享了一个不用写代码也能使用低代码的插件,但是还是有一些上手难度,比如要理解事件、方法等概念。现在AI大模型可以帮助我们处理很多事情,那AI和低代码结合能碰撞出什么样的火花呢,下面和大家分享一下我的实践。
建议先看一下上篇文章
思考
前面使用插件后配置流程虽然简化了很多,但是还不是不够简单,如果能借助AI分析用户的输入,自动实现前面所有的动作就好了。
下面来分析一下点击按钮,打开弹框
这个动作,其实只需要给它转换为下面这样的数据结构,我们就能解析并对接我们前面开发的事件流插件。
json
{
"componentName": "Button",
"event": "onClick",
"action": {
"type": "ComponentMethod",
"componentName": "Modal",
"method": "open"
}
}
componentName
: 触发事件的组件
event
: 事件
action
: 执行的动作
可能有人会有疑问,这里只知道组件名称,不知道是哪个组件,怎么给组件绑定事件呢,我这里的处理是,如果画布中只出现一个当前类型的组件时,就取这个组件,如果画布中没有当前类型的组件,自动生成一个插入到画布中,如果画布中有多个当前类型的组件,会让用户选择一个。
也就是说当用户输入了点击按钮,打开弹框
,会自动在画布中添加一个按钮和弹框,并且给按钮的点击事件绑定打开弹框的动作。
demo演示
点击按钮,打开弹框
点击按钮,打开弹框,一秒后关闭。
实现原理
分析用户输入
靠我们自己写代码去解析用户输入,基本不可能,因为用户可以随便输入,没有固定的格式,这时候我们需要借助 AI 大模型,帮我们分析用户的输入,然后转换为上面的数据结构。
分析用户输入这里我验证了两套方案,一个是langchain+ zod,还有一个是微软出的typechat,他们都可以实现把用户输入以json格式输出。
初始化后端框架
因为要对外暴露接口,所以要用到后端框架,我这里后端框架采用midway。
创建一个midway项目
sh
npm init midway@latest -y
推荐选择koa-v3
模版,项目名称自己输入。
langchain + zod
在service里封装一个方法,调用 langchain 库,根据 zod 定义的模型,解析用户输入,最终返回和模型一致的数据结构。用的大模型是gpt-3.5-turbo
,因为某些网站可以免费获取到3.5的密钥。
ts
async formatInputToEventFlows(input: string) {
const parser = StructuredOutputParser.fromZodSchema(schema);
const chain = RunnableSequence.from([
PromptTemplate.fromTemplate(
fs
.readFileSync(this.koaApp.getAppDir() + '/template.txt', 'utf-8')
.toString()
),
new OpenAI({
temperature: 0.5,
modelName: 'gpt-3.5-turbo',
configuration: {
// openapi 代理地址
baseURL: process.env.OPENAI_ENDPOINT,
// api key
apiKey: process.env.OPENAI_API_KEY,
},
}),
parser,
]);
const response = await chain.invoke({
input,
format_instructions: parser.getFormatInstructions(),
});
return response;
}
看一下 schema 怎么定义的
ts
import { z } from "zod";
export const showMessageAction = z
.object({
name: z.literal("showMessage"),
type: z.enum(["success", "error"]),
content: z.string().describe("消息内容"),
})
.describe("显示消息");
export const openPageAction = z
.object({
name: z.literal("openPage"),
url: z.string().describe("打开页面的url"),
})
.describe("打开页面");
export const componentAction = z
.object({
name: z.literal("ComponentMethod"),
component: z.union([
z.literal("Button").describe("按钮组件"),
z.literal("Modal").describe("弹窗组件"),
z.literal("Input").describe("输入框组件"),
]),
method: z.string().describe("组件方法"),
})
.describe("调用组件方法");
export const schema = z.object({
componentName: z.string().nullish().describe("触发事件的组件,可以为空"),
eventName: z.union([
z.literal("success").describe("成功事件"),
z.literal("error").describe("失败事件"),
z.string().describe(`当前组件对应的事件。
点击(onClick)
确认按钮点击事件(onOK), 取消按钮点击事件(onCancel),弹出事件(onShow)
获取焦点事件(onFocus), 失去焦点事件(onBlur)`),
]),
action: z
.union([showMessageAction, openPageAction, componentAction])
.describe("执行的动作"),
children: z.lazy(() =>
schema.nullish().describe("后续事件,没有后续可以为空")
),
});
大模型之所以能根据用户输入解析到对应的字段上,完全靠 describe 方法里的字段描述。
看一下Prompt定义,format_instructions和input是变量,发送给openai的时候,会被替换成zod的模型描述和用户输入。
txt
你是一个低代码平台
希望你能根据用户输入,分析用户的行为。
{format_instructions}
{input}
要求children字段数据格式和JSON Schema保持一致
typechat
使用typechat解析用户输入
ts
async formatInputToEventFlows(input: string) {
const model = createLanguageModel({
OPENAI_MODEL: 'gpt-3.5-turbo',
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
OPENAI_ENDPOINT: process.env.OPENAI_ENDPOINT,
});
const schema = fs.readFileSync(
path.join(this.koaApp.getAppDir(), 'src/service/schema.ts'),
'utf8'
);
const translator = createJsonTranslator<EventFlow>(
model,
schema,
'EventFlow'
);
const response = await translator.translate(input);
if (response.success) {
return response.data;
} else {
throw new Error('error');
}
}
看一下schema定义,typechat支持使用ts定义,在字段上加注释就行了。
ts
export type SuccessEvent = 'success';
export type ErrorEvent = 'error';
export type ButtonEvent = 'onClick';
export type ModalEvent = 'onOk' | 'onCancel' | 'onShow';
export type EventFlow = {
// 当前触发的事件的组件,可以为空
componentName?: 'Button' | 'Modal' | null;
// 事件
event: SuccessEvent | ErrorEvent | ButtonEvent | ModalEvent;
// 动作
action: {
onSuccess?: EventFlow['action'];
onError?: EventFlow['action'];
} & (
| {
name: 'ComponentMethod';
component: 'Modal';
method: 'open' | 'close';
}
| {
name: 'showMessage';
type: 'success' | 'error';
content: string;
}
| {
name: 'openPage';
url: string;
}
| {
// 执行定时器
name: 'setTimeout';
// 毫秒
timer: number;
// 多少毫秒后执行的动作
onSuccess?: EventFlow['action'];
}
);
};
我开始使用的是langchain+zod方案,这种方式定义模型的方式比较复杂并且返回值还不稳定,后面把方案换成了typechat,定义模型也比较简单,不需要太多的注释,并且还准确率也高,返回的结果比较稳定。
接口测试
输入:点击按钮,打开弹框
输出:
json
{
"componentName": "Button",
"event": "onClick",
"action": {
"name": "ComponentMethod",
"component": "Modal",
"method": "open"
}
}
输入:点击按钮,打开弹框。成功后显示提示,提示内容为 hello。显示成功后关闭弹框。
输出:
json
{
"componentName": "Button",
"event": "onClick",
"action": {
"name": "ComponentMethod",
"component": "Modal",
"method": "open",
"onSuccess": {
"name": "showMessage",
"type": "success",
"content": "hello",
"onSuccess": {
"name": "ComponentMethod",
"component": "Modal",
"method": "close"
}
}
}
}
输入:点击按钮,打开弹框。一秒后,关闭弹框。
输出:
json
{
"componentName": "Button",
"event": "onClick",
"action": {
"name": "ComponentMethod",
"component": "Modal",
"method": "open",
"onSuccess": {
"name": "setTimeout",
"timer": 1000,
"onSuccess": {
"name": "ComponentMethod",
"component": "Modal",
"method": "close"
}
}
}
}
使用typechat方案测试了很多遍,输出还是挺稳定的。
前端解析
前端采用对话的方式,用户输入完需求后,向后端发送请求,从后端拿到通过大模型格式化后的数据结构,在前端代码中再去解析数据结构,生成组件和绑定事件。
对话框实现
对话内容分为用户和AI,消息类型定义
ts
// 用户消息类型
interface UserMessage {
id: string,
role: 'user',
content: string,
status: 'success',
}
// AI消息类型
export interface AIMessage {
id: string,
role: 'ai',
content: AIContent,
status: 'loading' | 'success' | 'error'
}
export interface AIContent {
componentName?: string,
event: string,
action: {
onSuccess?: AIContent['action'],
onError?: AIContent['action'],
} & {
name: string,
[k: string]: any,
},
}
布局使用的是flex布局
关于输入框回车事件有个需要注意的地方,中文输入法输入英文单词按回车键,也会触发回车事件,这种情况可以用keyCode来判断,英文下的回车keyCode是13,中文输入法下的keyCode是229,可以用这个判断。
解析后端返回的结构
先解析触发事件的组件。如果当前组件类型画布中没有,那么自动给用户生成一个。如果有多个,需要用户选择一个。如果只有一个,就选择当前这个。
再解析事件。判断组件是否有当前事件,如果没有就终止
解析动作。根据不同的动作生成事件流数据结构
解析后续事件。如果 有onSuccess或onError不为空,表示还有后续事件,递归解析。
解析完成后,生成的事件流。
以点击按钮,打开弹框
为例,看一下自动生成事件流数据结构。
json
{
"onClick": {
"type": "flow",
"value": {
"id": "root",
"label": "开始",
"type": "start",
"children": [
{
"type": "action",
"id": "deadd458-1357-4d64-9810-6169458cdb51",
"label": "组件方法",
"key": "action",
"config": {
"type": "ComponentMethod",
"config": {
"componentId": "db1a11a4-47a7-4c88-b86c-025aca7fe168",
"method": "open"
}
}
}
]
}
}
}
最后调用node的setPropValue方法,把生成的事件流绑定到当前组件的事件属性上。
未来规划
上面通过AI大模型实现了用户输入需求,自动生成组件和组件事件绑定。虽然例子比较简单,但是我觉得这一块还是有很大的前景的,我也在慢慢完善。
关于这一块我的规划是:
-
继续完善组件事件绑定动作这一方式,支持更复杂的用户输入,比如自动生成条件,和组件属性绑定变量。
举个例子,当用户输入点击按钮,检验输入框的内容是否为邮箱格式,如果是邮箱格式,发送邮件。如果不是提示错误消息,消息内容为邮箱格式不正确,这里解析用户输入的内容,需要加上条件,当然还有更复杂的场景,我慢慢完善吧。
-
还有一个方向,用户直接输入功能需求,使用大模型直接生成低代码的schema,这样的方式对比上面更简单,比如输入我要使用阿里的LowCodeEngine低代码平台开发员工管理系统,帮我生成一个员工管理页面低代码Schema。,AI会自动生成一个增删改查的Schema,拿过来简单解析一下就能对接到低代码平台了。
json
{
"title": "员工管理系统",
"components": [
{
"type": "Table",
"name": "employeeTable",
"dataSource": {
"type": "API",
"api": "/api/employees"
},
"columns": [
{
"title": "姓名",
"field": "name",
"type": "Text"
},
{
"title": "职位",
"field": "position",
"type": "Text"
},
{
"title": "部门",
"field": "department",
"type": "Text"
},
{
"title": "电子邮件",
"field": "email",
"type": "Email"
},
{
"title": "电话号码",
"field": "phone",
"type": "Text"
}
],
"actions": [
{
"type": "Button",
"title": "新增",
"action": {
"type": "Dialog",
"title": "新增员工",
"form": {
"type": "Form",
"fields": [
{
"name": "name",
"label": "姓名",
"type": "TextInput"
},
{
"name": "position",
"label": "职位",
"type": "TextInput"
},
{
"name": "department",
"label": "部门",
"type": "TextInput"
},
{
"name": "email",
"label": "电子邮件",
"type": "EmailInput"
},
{
"name": "phone",
"label": "电话号码",
"type": "TextInput"
}
],
"submit": {
"type": "API",
"api": "/api/employees/create"
}
}
}
},
{
"type": "Button",
"title": "编辑",
"action": {
"type": "Dialog",
"title": "编辑员工信息",
"dataSource": {
"type": "API",
"api": "/api/employees/{id}"
},
"form": {
"type": "Form",
"fields": [
{
"name": "name",
"label": "姓名",
"type": "TextInput"
},
{
"name": "position",
"label": "职位",
"type": "TextInput"
},
{
"name": "department",
"label": "部门",
"type": "TextInput"
},
{
"name": "email",
"label": "电子邮件",
"type": "EmailInput"
},
{
"name": "phone",
"label": "电话号码",
"type": "TextInput"
}
],
"submit": {
"type": "API",
"api": "/api/employees/update"
}
}
}
}
]
}
]
}
- 还有一个方向,如果用户想开发一个系统,直接输入我想开发XXX系统,利用AI大模型给出表模型,然后我们解析模型之间的关系自动生成接口和页面,这样就可以轻松完成一个系统。比如用户输入我要开发一套员工管理系统,帮我生成一个员工管理系统的所有表结构,以json格式输出。
json
{
"tables": [
{
"name": "Employee",
"fields": [
{
"name": "id",
"type": "int",
"primaryKey": true,
"autoIncrement": true
},
{
"name": "name",
"type": "varchar",
"length": 255
},
{
"name": "email",
"type": "varchar",
"length": 255,
"unique": true
},
{
"name": "phone",
"type": "varchar",
"length": 255,
"nullable": true
},
{
"name": "positionId",
"type": "int",
"reference": {
"table": "Position",
"field": "id"
}
}
]
},
{
"name": "Department",
"fields": [
{
"name": "id",
"type": "int",
"primaryKey": true,
"autoIncrement": true
},
{
"name": "name",
"type": "varchar",
"length": 255
}
]
},
{
"name": "EmployeeDepartment",
"fields": [
{
"name": "employeeId",
"type": "int",
"reference": {
"table": "Employee",
"field": "id"
}
},
{
"name": "departmentId",
"type": "int",
"reference": {
"table": "Department",
"field": "id"
}
}
],
"primaryKey": [
"employeeId",
"departmentId"
]
},
{
"name": "Position",
"fields": [
{
"name": "id",
"type": "int",
"primaryKey": true,
"autoIncrement": true
},
{
"name": "title",
"type": "varchar",
"length": 255
}
]
}
]
}
上面是AI回复的结果,我们拿到这个json数据自动生成接口和页面也不是不行。
这几个就是我未来的规划,打算慢慢完善他们。
最后
项目体验地址:dbfu.github.io/easy-builde...
项目仓库地址:github.com/dbfu/easy-b...
功能目前还比较简陋,后面会慢慢完善,比如添加更多的物料、对接后端接口、让AI支持更复杂的用户输入等。
目前插件是和低代码项目放在一起,后面我会给单独拆出来,让想用这个插件的朋友也能在自己项目里快速使用。
本文正在参加阿里低代码引擎征文活动