antv x6在实现AI流程编排中的探索

191 阅读10分钟

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流程编排等场景中提供可视化建模能力。

翻阅官方文档,对于初次使用者,对文档无从下手,下图根据文档内容和开发实践整理了大概框架,方便阅读理解。

image.png

  • 节点(Node):图中那些一个个的,或圆形、或方型、或图标等等图形所代表的元素,被称为“节点”,节点是图里面的最基础的元素;

  • 连线(Edge):连接两个节点的元素,被称为“连线”,连线是 AntV X6 中非常重要的一部分,AntV X6 内置了很多实用的连线功能,也提供了优雅的扩展机制 ,这是相比于其他流程图框架占据绝对优势的地方;

  • 事件(Events):通过AntV X6内置事件系统,我们可以监听图内发生的任何事件。

总的来说,X6 是基于 SVG 的渲染引擎,可以使用不同的 SVG 元素渲染节点和边,非常适合节点内容比较简单的场景。面对复杂的节点, SVG 中有一个特殊的 foreignObject 元素,在该元素中可以内嵌任何 XHTML 元素,可以借助该元素来渲染 HTML、React/Vue/Angular 组件到需要位置,给项目开发带来非常大的便利。

在选择渲染方式时推荐:

  • 如果节点内容比较简单,而且需求比较固定,使用 SVG 节点
  • 其他场景,都推荐使用当前项目所使用的框架来渲染节点

3、项目规划

3.1 需求梳理

先看效果图

image.png

上图所示内容中,是一个最简版的流程图编排过程,概括一下需求内容为:

1、左侧区域为节点区域,支持拖拽到中间内容区,如果拖拽到页面其他区域无效;

2、拖拽到画布的每个节点有独特的样式,并且有特定的锚点,只能从锚点出发连接到下个节点;

3、画布中的节点支持点击,支持配置当前节点的相关信息;

3.2 功能拆分

3.2.1 系统架构图

image.png

1. 用户界面层

  • Canvas.vue - 主画布组件,是整个系统的核心界面

  • Shapes.vue - 节点面板,提供各种可拖拽的节点类型

  • 配置面板 - 包括对话配置和分支配置,用于设置节点属性

2. 核心引擎层

  • AntV X6图形引擎 - 提供强大的图形渲染和交互能力

  • 拖拽插件(DnD) - 实现节点的拖拽功能

  • 节点注册系统 - 管理不同类型的节点组件

3. 节点类型系统

系统支持四种核心节点类型:

  • 对话节点 - 处理用户对话逻辑

  • 分支节点 - 实现条件分支判断

  • 结束节点 - 流程结束点

  • 初始节点 - 流程开始点

4. 配置管理层

  • 画布配置 - 管理整个画布的设置

  • 节点配置 - 管理各个节点的属性

  • 连接桩配置 - 管理节点间的连接规则

3.2.2 数据流

image.png

用户拖拽->dnd处理->x6引擎->配置面板->更新数据->保存数据

3.2.3 事件处理架构

image.png

事件捕获阶段

  • 用户交互:用户通过界面进行各种操作(点击、拖拽、连线等)

  • 事件捕获:系统捕获用户的交互事件

事件分类处理

系统根据事件类型进行分流处理:

  • 节点点击事件:打开配置面板,允许用户编辑节点属性
  • 拖拽开始事件:创建拖拽节点,准备拖拽操作
  • 拖拽结束事件:在画布上创建实际的节点
  • 连线创建事件:验证连线规则,建立节点间的连接关系
  • 连线删除事件:清理连线数据,移除节点间的连接

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中内置了最基础的例如:圆形、菱形、方形等图形节点。同时也可以支持图片、自定义节点。

image.png

在需求中展示了三种节点,如果在其他需求中,可能会有更多节点。框架提供的基础节点样式很明显不能支持当前需求,或者在很多项目中都需要定制节点的样式,所以在这里用到了自定义节点的功能。

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 配置来完成。

如果想通过手动操作来创建连线,需要有两个条件:

  1. 需要从具有 magnet: true 属性的元素上才能手动拖拽出连线;
  2. 需要在全局 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固定位置。对于动态的,需要借助addPortremovePort进行手动添加和删除。

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和特效应对各种开发场景。掌握了它的基础语法,那么再复杂的需求也只是在基础上进行拓展。

6、参考资料