AntV-X6构建AI智能体工作流编辑器:完整实现技术解析 - 我的博客
我的博客

AntV-X6构建AI智能体工作流编辑器:完整实现技术解析

基于Vue3 + TypeScript + AntV X6构建的AI智能体工作流编辑器,涵盖DAG循环检测、拓扑排序、执行器实现、性能优化等核心技术

| 3.1k 字 | 阅读时间 8 分钟

一、项目架构设计

1.1 整体架构概览

本工作流编辑器基于 Vue 3 + TypeScript + Pinia 技术栈,核心图形渲染依赖 AntV X6 图引擎。整体采用 分层架构,从下到上依次为:

┌─────────────────────────────────────────────────────────────┐
                    视图层 (View Layer)                      │
  WorkflowEditor | Toolbar | InspectorPanel | DryRunPanel
├─────────────────────────────────────────────────────────────┤
                    状态管理层 (Store Layer)                  │
  graphStore | workflowStore | dryRunStore | uiStore
├─────────────────────────────────────────────────────────────┤
                    业务逻辑层 (Service Layer)                │
  graphOperations | eventService | uiService
├─────────────────────────────────────────────────────────────┤
                    核心引擎层 (Core Layer)                   │
  dag-validator | topology-sort | workflow-executor
├─────────────────────────────────────────────────────────────┤
                    基础类型层 (Type Layer)                   │
  graph.ts | workflow.ts | inspector.ts
└─────────────────────────────────────────────────────────────┘

1.2 核心组件职责

组件/模块职责描述关键文件
WorkflowEditor主编辑器容器,管理画布和面板状态WorkflowEditor.vue
graphStore图形状态管理,节点增删改查stores/graph/index.ts
dag-validatorDAG 循环检测与工作流验证utils/dag-validator.ts
topology-sort拓扑排序与执行顺序计算utils/topology-sort.ts
workflow-executor工作流执行引擎executor/workflow-executor.ts
dryRunStore试运行状态管理stores/dryRunStore.ts

1.3 状态管理设计

项目采用 Pinia 进行状态管理,核心 Store 包括:

  • graphStore:管理 X6 图形实例、节点选择状态、历史记录
  • workflowStore:管理工作流执行状态
  • dryRunStore:管理试运行功能的状态和历史记录
  • uiStore:管理 UI 面板的显示/隐藏状态

二、DAG(有向无环图)校验机制

2.1 循环检测算法

项目采用 深度优先搜索 (DFS) 算法检测图中的循环,核心实现位于 dag-validator.ts

export function detectCycle(graph: Graph): { hasCycle: boolean; cycle?: string[] } {
  const adjacencyList: Record<string, string[]> = {};
  nodes.forEach((node: GraphNode) => {
    adjacencyList[node.id] = [];
  });

  edges.forEach((edge: GraphEdge) => {
    const source = edge.getSourceCellId();
    const target = edge.getTargetCellId();
    if (source && target) {
      adjacencyList[source].push(target);
    }
  });

  const visited = new Set<string>();
  const recStack = new Set<string>();
  const path: string[] = [];

  const dfs = (nodeId: string): string[] | null => {
    visited.add(nodeId);
    recStack.add(nodeId);
    path.push(nodeId);

    for (const neighbor of adjacencyList[nodeId]) {
      if (!visited.has(neighbor)) {
        const result = dfs(neighbor);
        if (result !== null) return result;
      } else if (recStack.has(neighbor)) {
        const cycleStartIndex = path.indexOf(neighbor);
        const cyclePath = path.slice(cycleStartIndex);
        return [...cyclePath, neighbor];
      }
    }

    path.pop();
    recStack.delete(nodeId);
    return null;
  };
  // ...
}

算法要点

  • 使用 visited 集合记录已访问节点
  • 使用 recStack 集合记录当前递归路径
  • 当发现回边(指向递归栈中节点的边)时,提取循环路径

2.2 工作流完整性验证

validateWorkflow 函数执行多项验证规则:

验证项错误类型触发条件
循环检测cycle图中存在环
空工作流empty无节点
缺少开始节点missing_start无 INPUT 类型节点
多个开始节点multiple_starts多个 INPUT 类型节点
缺少结束节点missing_end无 OUTPUT 类型节点
孤立节点isolated_node节点无任何连接

2.3 连接验证规则

connection.ts 中定义了连接的约束规则:

export function validateConnection(
  sourceCell, targetCell, sourceMagnet, targetMagnet, graph
) {
  // 1. 基础验证
  if (!sourceMagnet) return { valid: false, reason: '源连接点不存在' };
  if (!targetMagnet) return { valid: false, reason: '目标连接点不存在' };
  if (sourceCell === targetCell) return { valid: false, reason: '不能连接到自身' };

  // 2. 节点类型限制
  if (sourceType === 'OUTPUT') return { valid: false, reason: '输出节点不能作为源节点' };
  if (targetType === 'INPUT') return { valid: false, reason: '开始节点不能作为目标节点' };

  // 3. 端口方向限制
  if (!isOutputPort(sourcePortGroup)) return { valid: false, reason: '输入桩不能发起连线' };
  if (!isInputPort(targetPortGroup)) return { valid: false, reason: '输出桩不能接收连线' };

  // 4. 循环节点内部连接限制
  const loopValidation = validateLoopNodeConnection(sourceCell, targetCell);
  if (!loopValidation.valid) return { valid: false, reason: loopValidation.reason };

  // 5. 重复连接检测
  const existingConnection = edges.find(edge => 
    edge.getSourceCellId() === sourceCellData.id && 
    edge.getTargetCellId() === targetCellData.id
  );
  if (existingConnection) return { valid: false, reason: '这两节点之间已存在连线' };

  return { valid: true, reason: '' };
}

三、执行器实现

3.1 执行器架构设计

WorkflowExecutor 采用 策略模式,支持不同节点类型的定制化执行:

export interface NodeExecutor<T = unknown> {
  execute(node: GraphNode, context: ExecutionContext): Promise<T>;
  canRetry?(error: unknown): boolean;
}

export class WorkflowExecutor {
  private graph: Graph;
  private nodeExecutors: Record<string, NodeExecutor>;
  private workflowState: WorkflowState;
  private nodeStates: Record<string, ExecutionState>;
  
  // 核心执行逻辑
  public async execute(): Promise<ExecutionResult> {
    const executionOrder = getExecutionOrder(this.graph);
    
    for (const nodeId of executionOrder) {
      const node = this.graph.getNodes().find(n => n.id === nodeId);
      if (!node) continue;

      await this.waitIfPaused();
      if (this.abortController.signal.aborted) { /* 处理取消 */ }

      this.workflowState.currentNodeId = nodeId;
      const result = await this.executeNodeWithRetry(node);

      if (!result.success) {
        return { success: false, error: result.error, workflowState: this.workflowState };
      }
      this.updateProgress();
    }
    // ...
  }
}

3.2 重试机制

执行器内置 指数退避重试 策略:

private async executeNodeWithRetry(node: GraphNode): Promise<{ success: boolean; error?: unknown }> {
  for (let attempt = 0; attempt <= nodeState.maxRetries; attempt++) {
    // 检查取消信号
    if (this.abortController && this.abortController.signal.aborted) {
      return { success: false, error: new Error('Execution aborted') };
    }

    try {
      this.updateNodeState(node.id, { status: 'running', startTime: Date.now(), retryCount: attempt });
      const result = await executor.execute(node, context);
      this.outputs[node.id] = result;
      this.updateNodeState(node.id, { status: 'completed', endTime: Date.now(), output: result });
      return { success: true };
    } catch (error) {
      const canRetry = executor.canRetry ? executor.canRetry(error) : true;
      
      if (attempt >= nodeState.maxRetries || !canRetry) {
        this.updateNodeState(node.id, { status: 'error', error: error.message, endTime: Date.now() });
        return { success: false, error };
      }

      // 指数退避延迟
      await this.delay(this.options.retryDelay * Math.pow(2, attempt));
    }
  }
}

3.3 执行控制能力

执行器支持完整的生命周期控制:

操作方法功能说明
启动execute()开始工作流执行
取消abort()立即终止执行
暂停pause()暂停执行流程
恢复resume()恢复暂停的执行

四、性能优化策略

4.1 拓扑排序缓存

topology-sort.ts 实现了智能缓存机制:

const TOPOLOGY_CACHE_MAX_AGE = 5000; // 5秒过期
const TOPOLOGY_CACHE_SIZE = 100; // 最多缓存100个条目
const topologyCache = new Map<string, CacheEntry>();

export function topologySort(graph: TopoGraph): string[] {
  const cacheKey = generateCacheKey(nodes, edges);
  const cachedResult = getCachedResult(cacheKey);
  
  if (cachedResult) {
    return cachedResult;
  }

  // 执行拓扑排序...
  const result = /* 排序结果 */;
  setCacheResult(cacheKey, result);
  return result;
}

缓存策略

  • 时间过期:缓存条目 5 秒后自动失效
  • LRU 淘汰:超过最大容量时删除最旧条目
  • 键生成:基于节点 ID 和边信息的哈希值

4.2 事件节流与防抖

events.ts 中应用了性能优化:

const THROTTLE_DELAY = 50;
const DEBOUNCE_DELAY = 100;

// 节流:节点悬停事件
graphRef.value.on(
  'node:mouseover',
  throttle(({ node }: { node: Node }) => {
    handleNodeMouseOver(node);
  }, THROTTLE_DELAY)
);

// 防抖:节点位置变化事件
const positionHandler = debounce(
  ({ node, options }) => {
    // 处理节点位置变化
  },
  DEBOUNCE_DELAY
);

4.3 渲染优化

通过 Z-index 管理 优化图层渲染:

graphRef.value.on('node:change:zIndex', ({ node, current }) => {
  const nodeData = node.getData();
  if (nodeData?.type === 'LOOP') {
    const children = node.getChildren?.();
    if (children) {
      children.forEach((child) => {
        child.setZIndex(current + 1);
      });
    }
  }
});

五、关键技术难点与解决方案

坑点一:循环节点内部连接限制

问题描述: 循环节点(LOOP)内部的子节点需要与外部节点隔离,不能从外部直接连接到循环内部,也不能从循环内部连接到外部。

解决方案: 在 connection.ts 中实现 validateLoopNodeConnection 函数:

export function validateLoopNodeConnection(sourceCell, targetCell): LoopValidationResult {
  const sourceLoopResult = isLoopChildNode(sourceNode);
  const targetLoopResult = isLoopChildNode(targetNode);

  // 循环内部节点不能连接到外部
  if (sourceLoopResult.isLoopChild && !targetLoopResult.isLoopChild) {
    return { valid: false, reason: '循环节点内部的子节点只能连接到同一循环节点内的其他子节点' };
  }

  // 外部节点不能连接到循环内部
  if (!sourceLoopResult.isLoopChild && targetLoopResult.isLoopChild) {
    return { valid: false, reason: '不能从循环节点外部连接到循环节点内部' };
  }

  // 不同循环节点的内部不能互相连接
  if (sourceLoopResult.isLoopChild && targetLoopResult.isLoopChild) {
    if (sourceLoopResult.loopNode?.id !== targetLoopResult.loopNode?.id) {
      return { valid: false, reason: '循环节点内部的子节点只能连接到同一循环节点内' };
    }
  }

  return { valid: true, reason: '' };
}

坑点二:节点嵌入与父子关系管理

问题描述: 当用户拖拽节点进入/离开循环节点时,需要正确维护父子关系,并处理节点移动时的边界检测。

解决方案: 在 init.ts 中配置 embedding 选项:

embedding: {
  enabled: true,
  findParent(this: Graph, { node }): Cell[] {
    const bbox = node.getBBox();
    return this.getNodes().filter((n) => {
      const data = n.getData();
      if (data && data.type === 'LOOP') {
        if (data.collapsed === true) return false;
        const targetBBox = n.getBBox();
        return bbox.isIntersectWithRect(targetBBox);
      }
      return false;
    });
  },
  validate(this: Graph, { child, parent }): boolean {
    const forbiddenComponents = ['LOOP', 'INPUT'];
    if (forbiddenComponents.includes(child.getData()?.type || '')) {
      return false;
    }
    // 已有连接的节点不能拖入循环(需按Ctrl)
    if (!child.parent) {
      const hasNoConnectedEdges = this.getConnectedEdges(child).length === 0;
      return hasNoConnectedEdges && ctrlPressed.value;
    }
    return false;
  },
}

坑点三:实时状态更新的性能问题

问题描述: 工作流执行时,每个节点状态变化都会触发 UI 更新,大量节点执行会导致性能问题。

解决方案: 在 dryRunStore.ts 中使用 批量更新 策略:

const executeWithRealtimeUpdate = async () => {
  for (let i = 0; i < executionOrder.length; i++) {
    const nodeId = executionOrder[i];
    
    // 暂停检测(让出主线程)
    await new Promise((resolve) => setTimeout(resolve, 0));

    // 批量更新状态
    executionStates[nodeId] = { ...executionStates[nodeId], status: 'running', startTime: Date.now() };
    logs.push({ timestamp: Date.now(), level: 'info', message: `开始执行节点: ${nodeId}` });

    // 单次状态更新
    dryRunState.value.currentRecord = {
      ...dryRunState.value.currentRecord!,
      executionStates,
      logs,
      progress: Math.round((i / executionOrder.length) * 100),
    };

    // 执行节点...
  }
};

坑点四:拓扑排序的正确性与缓存一致性

问题描述: 拓扑排序需要正确处理多种边界情况(自环、孤立节点、空图),同时缓存机制需要保证一致性。

解决方案

  1. 完善的循环检测
// 处理自环
if (source === target) {
  return { hasCycle: true, cycle: [source, target] };
}

// 处理空图
if (nodes.length === 0) {
  return { hasCycle: false };
}
  1. 智能缓存管理
function cleanupCache(): void {
  const now = Date.now();
  const keysToDelete: string[] = [];

  // 清理过期条目
  for (const [key, entry] of topologyCache.entries()) {
    if (now - entry.timestamp > TOPOLOGY_CACHE_MAX_AGE) {
      keysToDelete.push(key);
    }
  }

  // LRU 淘汰
  if (topologyCache.size > TOPOLOGY_CACHE_SIZE) {
    const entries = Array.from(topologyCache.entries());
    entries.sort((a, b) => a[1].timestamp - b[1].timestamp);
    const excess = topologyCache.size - TOPOLOGY_CACHE_SIZE;
    for (let i = 0; i < excess; i++) {
      topologyCache.delete(entries[i][0]);
    }
  }
}

坑点五:连接端口的交互体验

问题描述: 用户在创建连线时,需要清晰的视觉反馈来区分输入端口和输出端口。

解决方案

  1. 端口样式区分
// 输出端口(右侧/底部)- 绿色
// 输入端口(左侧/顶部)- 蓝色
export const OUTPUT_PORT_GROUPS = ['right', 'bottom'];
export const INPUT_PORT_GROUPS = ['left', 'top'];
  1. 悬停交互增强
export function handleNodeMouseOver(node: Node): void {
  const ports = node.getPorts();
  ports.forEach((port) => {
    if (port.group === 'right' || port.group === 'bottom') {
      // 放大输出端口
      node.setPortProp(port.id, 'attrs/circle/r', portInteractionStyles.nodeHover.r);
    }
  });
}

六、总结与展望

6.1 技术亮点

  1. 完整的 DAG 验证体系:循环检测、完整性验证、连接约束
  2. 灵活的执行器架构:策略模式 + 重试机制 + 生命周期控制
  3. 智能性能优化:缓存、节流、防抖、批量更新
  4. 完善的测试覆盖:单元测试覆盖核心算法

6.2 未来优化方向

  1. 并行执行支持:支持无依赖节点的并行执行
  2. 分布式执行:支持跨机器的分布式工作流执行
  3. 可视化调试器:实时查看执行流程和状态变化
  4. 协作编辑:支持多人实时协作编辑工作流
  5. 工作流模板市场:提供可复用的工作流模板

项目地址https://github.com/wangjincai05/antvX6

预览地址https://workflow.wantasy.asia/

核心文件清单

  • src/utils/dag-validator.ts - DAG 校验
  • src/utils/topology-sort.ts - 拓扑排序
  • src/executor/workflow-executor.ts - 执行器
  • src/stores/graph/index.ts - 图形状态管理
  • src/components/workflow/WorkflowEditor.vue - 主编辑器

本文基于真实项目代码分析,涵盖了工作流编辑器从架构设计到性能优化的完整实现路径,希望能为开发者提供有价值的参考。

评论

相关文章