1、前言
如今AI应用已深度渗透至工业生产、金融分析、医疗诊断、教育创新等各个领域。从智能客服到自动化决策系统,这些看似“智能”的终端应用背后,往往隐藏着复杂的技术链路——数据清洗、模型训练、推理服务、流程监控等环节需要精密协同,而这一切的底层支撑,离不开开发人员对“智能中枢”的可视化构建能力。
本文将聚焦AntV X6在AI编排流程图场景中的创新实践,从技术实现、功能亮点到行业应用案例,深度解析这一工具如何成为AI开发者的“智能画布”,助力开发者快速实现Ai流程编排功能。
2、技术分析
在我们的规划设计中,主要的需求点是前端通过节点(开始节点、会话节点、挂断节点)的编排,实现一套流程图,通过各个场景和顺序的排列,把数据提交给后端和大模型,训练模型接收用户提问的能力和做出应答。
在实现编排、模型训练、智能体搭建或者其他AI场景中,各家厂商都有自己的一套流程和技术架构。通过调研发现,常见的框架中,以阿里开源框架antv系列(G6、X6)、滴滴开源的logicflow、经典框架bpmn.js、扣子平台原生js+svg 等。为了后续扩展方便,还有其他的图表功能接入进来,最终选择的是antv系列。本文主要是对x6在前端业务中的使用和问题进行展开。如果喜欢其他框架的同学,可以进行尝试。
官方定义:AntV X6 是基于 HTML 和 SVG 的图编辑引擎,支持流程图、ER图、网络拓扑图等多种图形绘制,并能在AI流程编排等场景中提供可视化建模能力。
翻阅官方文档,对于初次使用者,对文档无从下手,下图根据文档内容和开发实践整理了大概框架,方便阅读理解。
-
节点(Node):图中那些一个个的,或圆形、或方型、或图标等等图形所代表的元素,被称为“节点”,节点是图里面的最基础的元素;
-
连线(Edge):连接两个节点的元素,被称为“连线”,连线是 AntV X6 中非常重要的一部分,AntV X6 内置了很多实用的连线功能,也提供了优雅的扩展机制 ,这是相比于其他流程图框架占据绝对优势的地方;
-
事件(Events):通过AntV X6内置事件系统,我们可以监听图内发生的任何事件。
总的来说,X6 是基于 SVG 的渲染引擎,可以使用不同的 SVG 元素渲染节点和边,非常适合节点内容比较简单的场景。面对复杂的节点, SVG 中有一个特殊的 foreignObject 元素,在该元素中可以内嵌任何 XHTML 元素,可以借助该元素来渲染 HTML、React/Vue/Angular 组件到需要位置,给项目开发带来非常大的便利。
在选择渲染方式时推荐:
- 如果节点内容比较简单,而且需求比较固定,使用
SVG节点 - 其他场景,都推荐使用当前项目所使用的框架来渲染节点
3、项目规划
3.1 需求梳理
先看效果图
上图所示内容中,是一个最简版的流程图编排过程,概括一下需求内容为:
1、左侧区域为节点区域,支持拖拽到中间内容区,如果拖拽到页面其他区域无效;
2、拖拽到画布的每个节点有独特的样式,并且有特定的锚点,只能从锚点出发连接到下个节点;
3、画布中的节点支持点击,支持配置当前节点的相关信息;
3.2 功能拆分
3.2.1 系统架构图
1. 用户界面层
-
Canvas.vue - 主画布组件,是整个系统的核心界面
-
Shapes.vue - 节点面板,提供各种可拖拽的节点类型
-
配置面板 - 包括对话配置和分支配置,用于设置节点属性
2. 核心引擎层
-
AntV X6图形引擎 - 提供强大的图形渲染和交互能力
-
拖拽插件(DnD) - 实现节点的拖拽功能
-
节点注册系统 - 管理不同类型的节点组件
3. 节点类型系统
系统支持四种核心节点类型:
-
对话节点 - 处理用户对话逻辑
-
分支节点 - 实现条件分支判断
-
结束节点 - 流程结束点
-
初始节点 - 流程开始点
4. 配置管理层
-
画布配置 - 管理整个画布的设置
-
节点配置 - 管理各个节点的属性
-
连接桩配置 - 管理节点间的连接规则
3.2.2 数据流
用户拖拽->dnd处理->x6引擎->配置面板->更新数据->保存数据
3.2.3 事件处理架构
事件捕获阶段
-
用户交互:用户通过界面进行各种操作(点击、拖拽、连线等)
-
事件捕获:系统捕获用户的交互事件
事件分类处理
系统根据事件类型进行分流处理:
- 节点点击事件:打开配置面板,允许用户编辑节点属性
- 拖拽开始事件:创建拖拽节点,准备拖拽操作
- 拖拽结束事件:在画布上创建实际的节点
- 连线创建事件:验证连线规则,建立节点间的连接关系
- 连线删除事件:清理连线数据,移除节点间的连接
4、项目实施
4.1 布局
采用画布撑满屏幕,节点区域固定在左侧。
#antv-container为画布容器,在初始化画布的时候需要配置一个容器。
<div id="antv-shapes" class="antv-shapes robotCanvasNoSelect">
<Shapes @startDrag="startDrag"></Shapes>
</div>
<div id="antv-container"></div>
4.2 节点的定义
在x6中内置了最基础的例如:圆形、菱形、方形等图形节点。同时也可以支持图片、自定义节点。
在需求中展示了三种节点,如果在其他需求中,可能会有更多节点。框架提供的基础节点样式很明显不能支持当前需求,或者在很多项目中都需要定制节点的样式,所以在这里用到了自定义节点的功能。
x6中提供了多种扩展包支持自定义节点,满足使用不同框架的前端开发者进行适配。以下示例中均采用vue2的写法,同时使用了@antv/x6-vue-shape来使用vue组件渲染节点。
@antv/x6-vue-shape提供了register方法注册节点,自定义节点必须通过节点注册才能使用。
import { register } from '@antv/x6-vue-shape'
import CustomNode from './components/CustomNode.vue'
register({
shape: 'custom-vue-node',
width: 100,
height: 100,
component: CustomNode,
})
上述代码中,register函数接收一个对象参数,这个对象的属性如下:
- shape:自定义节点的名字
- width: 节点的宽
- height: 节点的高
- component: vue组件的名字(和在vue中引入组件一样)
如果一个需求中存在很多个自定义节点,那么就得调用很多次register方法。在实际开发中,可以约定shapes目录,里边专门存放自定义节点的.vue文件,通过构建工具提供的遍历文件的方法,实现自动注册。
const registerNodes = () => {
// 使用vite构建工具
// const shapes = import.meta.glob('./components/shapes/*.vue', { eager: true });
// 使用webpack构建工具
const nodes = require.context('./', true, /\.vue$/);
nodes.keys().forEach(item => {
let src = nodes(item);
if (!src.name) {
src.name = item.split('/').pop().replace(/\.vue$/, '')
}
register({
shape: src.name,
component: src.default,
})
})
}
4.3 拖拽
官方库:通过拖拽交互往画布中添加节点,如流程图编辑场景,从流程图组件库中拖拽组件到画布中。antv提供了一个独立的插件包 @antv/x6-plugin-dnd 来使用这个功能。
如果你的需求很简单。只是把节点拖拽到画布上正常回显,那么可以直接使用。如果需求和上述类似,节点列表和拖拽到画布上的样式不一样,那么还需要自定义节点样式。
官方给出示例如下,本质是拦截放置事件,返回一个新的节点。
const dnd = new Dnd({
getDropNode(node) {
const { width, height } = node.size()
// 返回一个新的节点作为实际放置到画布上的节点
return node.clone().size(width * 3, height * 3)
},
})
在这里把官方的示例做一下转换,灵活运用getDropNode方法。
getDropNode: (node) => {
const data = node.getData();
const n = graph?.createNode({
shape: data.nodeCode,
...compName2Config[data.compName],
data: { ...data, nodeDeleteClick: that.nodeDeleteClick },
});
return n;
}
使用createNode方法创建一个新节点。同时,通过data属性自定义节点数据,把外界输入传入到节点内部。
在节点内部通过inject方法获取节点,调用节点方法获取自定义数据。
export default defineComponent({
name: 'ProgressNode',
inject: ['getNode'],
data() {
return {
percentage: 80,
}
},
mounted() {
const node = (this as any).getNode() as Node
node.on('change:data', ({ current }) => {
const { progress } = current
this.percentage = progress
})
},
})
4.4 连线
官方定义:连线交互规则都是通过 connecting 配置来完成。
如果想通过手动操作来创建连线,需要有两个条件:
- 需要从具有
magnet: true属性的元素上才能手动拖拽出连线; - 需要在全局
connecting配置中自定义createEdge方法。
本次需求中连线规则:
- 防回连: 通过
checkReversePath函数防止形成环路 - 连接验证: 在
validateConnection中验证连接的有效性 - 路由算法: 使用 Manhattan 路由算法,支持智能正交路由
connecting: {
snap: true,
allowMulti: 'withPort',
allowBlank: false,
allowLoop: false,
allowNode: false,
highlight: true,
router: {
name: 'manhattan',
args: {
startDirections: ['right'],
endDirections: ['left'],
padding: { horizontal: 30 }
}
},
connector: {
name: 'rounded',
args: { radius: 8 }
},
validateConnection(){}
}
validateConnection在移动边的时候判断连接是否有效,如果返回 false,当鼠标放开的时候,不会连接到当前元素。在这个函数中,可以对连线做特殊处理满足业务需求。
4.5 连接桩(点)
在官方定义中,连接点和连接桩是不同的概念,但它们发挥的实际功能差不多。如果对连接没有特殊需求,可以简化概念,把它们当成同一种类处理。
对于连接桩的配置,可以查看文档。在需求中,每一个连接桩代表一个条件分支,并且是动态的。对于静态的,通过设置position固定位置。对于动态的,需要借助addPort和removePort进行手动添加和删除。
async dynamicPorts(edgeList, portsId, node, type) {
// 比较现有连接桩与配置的连接桩
const res = this.compare(edgeList, portsId, node);
if (res === 'text') {
// 仅文案变化,更新文案
this.handleTextChange(edgeList, portsId, node);
} else if (res) {
// 无变化,直接保存
await this.saveNode('nodeMoved', {});
} else {
// 连接桩发生变化,重新绘制
this.addNewPorts(edgeList, node, type, keepInfo);
}
}
4.6 事件系统
-
节点事件
点击事件:
node:click- 打开节点配置面板鼠标进入:
node:mouseenter- 高亮显示连接桩鼠标离开:
node:mouseleave- 隐藏连接桩移动事件:
node:moved- 节点位置变化 -
连线事件
连线创建:
edge:connected- 连线建立连线移除:
edge:removed- 连线删除连线高亮:
edge:mouseenter/leave- 连线高亮效果 -
画布事件
缩放事件:
scale- 画布缩放平移事件:
panning- 画布平移
this.graphInstance.on('scale', this.canvasScale);
this.graphInstance.on('node:mouseenter', this.nodeMouseEnter);
this.graphInstance.on('node:mouseleave', this.nodeMouseLeave);
this.graphInstance.on('node:click', this.nodeClick);
this.graphInstance.on('edge:mouseenter', this.edgeMouseEnter);
this.graphInstance.on('edge:mouseleave', this.edgeMouseLeave);
this.graphInstance.on('node:moved', this.nodeMoved);
this.graphInstance.on('node:added', this.nodeAdded);
this.graphInstance.on('node:removed', this.nodeRemoved);
this.graphInstance.on('edge:removed', this.edgeRemoved);
this.graphInstance.on('edge:connected', this.edgeConnected);
5、附加功能
antv x6除了提供基本的画布能力,还提供了丰富的插件扩展画布功能,比如快捷键、框选、撤销重做等,使用这些功能都需要安装官方独立的npm包。
其中,在插件文档中没有提供自动布局的概念和方法。但是通过官方提供的demo中,用到了dagre库。相关阅读:dagre布局算法
在使用antv x6过程中,框架提供的能力远不如此,它是一个功能很强大的库,可以开箱即用、定制化能力也很丰富,这里介绍的也只是一部分能力。官方也列举了很多demo和特效应对各种开发场景。掌握了它的基础语法,那么再复杂的需求也只是在基础上进行拓展。