从零开始设计一个智能体编排系统 - 画布实现篇
·
导读:有了 DSL 作为"设计图纸",接下来我们要建造"施工场地"了。本文带你深入了解如何实现一个流畅的可视化画布,让智能体编排像搭积木一样简单。

一、画布:智能体的"工作台"
1.1 画布的核心功能
想象一下 Photoshop 的画布、Figma 的设计板,或者思维导图工具的编辑区——我们的智能体画布也是类似的 concept。
┌─────────────────────────────────────────────────────┐
│ 画布核心功能 │
├─────────────────────────────────────────────────────┤
│ │
│ 🎯 节点展示 → 显示所有节点及其状态 │
│ 🖱️ 拖拽添加 → 从面板拖入新节点 │
│ 🔗 连线编辑 → 连接节点构建流程 │
│ 🔍 缩放平移 → 查看大型工作流 │
│ ✏️ 节点操作 → 移动/复制/删除节点 │
│ 💾 自动保存 → 实时同步到 DSL │
│ │
└─────────────────────────────────────────────────────┘
1.2 画布界面布局
┌──────────────────────────────────────────────────────────┐
│ 顶部工具栏 │
│ ← 返回 │ 智能体名称 │ 导入 导出 保存 运行 │
├──────────┬───────────────────────────────────────────────┤
│ │ │
│ 节点 │ │
│ 面板 │ 画布区域 │
│ │ │
│ [开始] │ [节点] ──► [节点] ──► [节点] │
│ [AI] │ │ │
│ [检索] │ ▼ │
│ [条件] │ [节点] │
│ [工具] │ │
│ ... │ │
│ │ │
└──────────┴───────────────────────────────────────────────┘
二、技术选型:为什么选择 VueFlow?
2.1 流程图引擎对比
┌────────────────────────────────────────────────────┐
│ 主流流程图引擎对比 │
├────────────────────────────────────────────────────┤
│ │
│ 引擎 │ 优点 │ 缺点 │
│ ─────────────│────────────────│─────────────────│
│ VueFlow │ Vue3 原生 │ 生态相对较小 │
│ React Flow │ 功能强大 │ 仅支持 React │
│ GoJS │ 功能最全 │ 商业授权 │
│ JointJS │ 开源免费 │ 学习曲线陡 │
│ 自研 │ 完全可控 │ 成本高 │
│ │
└────────────────────────────────────────────────────┘
2.2 VueFlow 的核心能力
┌─────────────────────────────────────────┐
│ VueFlow 能做什么 │
├─────────────────────────────────────────┤
│ │
│ ✅ 节点渲染 → 自定义节点组件 │
│ ✅ 边渲染 → 多种连线样式 │
│ ✅ 拖拽支持 → 从外部拖入节点 │
│ ✅ 连线交互 → 拖拽连线、自动吸附 │
│ ✅ 视图控制 → 缩放、平移、适应视图 │
│ ✅ 选择/多选 → 框选、Ctrl+ 点击 │
│ ✅ 快捷键 → 复制/粘贴/删除 │
│ ✅ 迷你地图 → 快速导航 │
│ ✅ 背景网格 → 对齐辅助 │
│ │
└─────────────────────────────────────────┘
2.3 VueFlow 的基本用法
<template>
<VueFlow
v-model:nodes="nodes"
v-model:edges="edges"
:node-types="nodeTypes"
@connect="handleConnect"
@node-drag-stop="handleNodeMove"
>
<!-- 自定义节点模板 -->
<template #node-beginNode="slotProps">
<BeginNode v-bind="slotProps" />
</template>
</VueFlow>
</template>
三、节点面板:智能体的"工具箱"
3.1 节点分类设计
好的分类让用户快速找到需要的节点:
┌─────────────────────────────────────────┐
│ 节点面板 │
├─────────────────────────────────────────┤
│ │
│ 📌 基础 │
│ ├─ 开始 │
│ ├─ 结束 │
│ └─ 消息输出 │
│ │
│ 🤖 AI 能力 │
│ ├─ AI 对话 │
│ ├─ 知识检索 │
│ └─ 问题分类 │
│ │
│ 🔀 流程控制 │
│ ├─ 条件分支 │
│ ├─ 迭代循环 │
│ └─ 并行处理 │
│ │
│ 🛠️ 工具扩展 │
│ ├─ API 调用 │
│ ├─ 代码执行 │
│ └─ 数据处理 │
│ │
└─────────────────────────────────────────┘
3.2 拖拽添加节点
用户操作流程:
1. 鼠标悬停节点 → 显示预览
↓
2. 按下鼠标 → 开始拖拽
↓
3. 拖到画布上方 → 显示放置位置
↓
4. 松开鼠标 → 创建节点
实现要点:
┌─────────────────────────────────────────┐
│ 拖拽实现要点 │
├─────────────────────────────────────────┤
│ │
│ 1. 设置 draggable="true" │
│ 2. 监听 dragstart 事件 │
│ 3. 携带节点类型信息 │
│ 4. 画布监听 dragover 和 drop │
│ 5. 计算放置位置 │
│ 6. 创建节点并添加到画布 │
│ │
└─────────────────────────────────────────┘
3.3 节点预览
拖拽时显示半透明预览,帮助用户定位:
┌─────────────────────────────────────────┐
│ │
│ [节点面板] │
│ ┌─────┐ │
│ │ AI │ ──► 拖拽 │
│ └─────┘ │
│ ╲ │
│ ╲ 半透明预览 │
│ ╲ ┌─────┐ │
│ ╲ │ AI │ │
│ ╲└─────┘ │
│ ↓ │
│ 放置位置 │
│ │
└─────────────────────────────────────────┘
四、连线:构建智能体的"神经网络"
4.1 连线交互流程
1. 鼠标悬停节点输出点 → 高亮显示
↓
2. 点击输出点 → 开始连线
↓
3. 拖动鼠标 → 显示跟随的连线
↓
4. 悬停目标节点输入点 → 吸附高亮
↓
5. 松开鼠标 → 创建连接
4.2 连线校验
不是所有节点都能随意连接,需要规则校验:
┌─────────────────────────────────────────┐
│ 连线规则 │
├─────────────────────────────────────────┤
│ │
│ ✅ 允许的连接 │
│ • 开始 → 任意节点 │
│ • AI → 消息输出 │
│ • 条件 → 多个分支 │
│ │
│ ❌ 禁止的连接 │
│ • 循环依赖 │
│ • 类型不匹配 │
│ • 超过最大连接数 │
│ │
└─────────────────────────────────────────┘
校验逻辑示意:
function canConnect(source, target) {
// 1. 不能自己连自己
if (source === target) return false
// 2. 不能产生循环
if (wouldCreateCycle(source, target)) return false
// 3. 检查节点类型兼容性
if (!isCompatible(source.type, target.type)) return false
// 4. 检查连接数限制
if (exceedsMaxConnections(source)) return false
return true
}
4.3 连线样式
不同状态的连线用不同样式表示:
┌─────────────────────────────────────────┐
│ 连线状态样式 │
├─────────────────────────────────────────┤
│ │
│ 正常状态 ───── 黑色实线 │
│ 悬停状态 ───── 蓝色加粗 │
│ 选中状态 ───── 蓝色虚线 │
│ 无效状态 ─ ─ ─ 灰色虚线 │
│ 错误状态 ═════ 红色双线 │
│ │
└─────────────────────────────────────────┘
4.4 自定义边组件
<template>
<g class="custom-edge">
<!-- 边路径 -->
<path :d="path" class="edge-path" />
<!-- 边上的文字 -->
<textPath :href="`#${id}`" class="edge-label">
{{ label }}
</textPath>
<!-- 删除按钮(悬停显示) -->
<circle
:cx="midX"
:cy="midY"
class="delete-btn"
@click="onDelete"
/>
</g>
</template>
五、画布交互:让编辑更流畅
5.1 视图控制
缩放和平移:
┌─────────────────────────────────────────┐
│ 视图控制方式 │
├─────────────────────────────────────────┤
│ │
│ 🔍 缩放 │
│ • 鼠标滚轮 │
│ • Ctrl/Cmd + +/- │
│ • 工具栏按钮 │
│ • 双指捏合(触控板) │
│ │
│ ✋ 平移 │
│ • 拖拽空白区域 │
│ • 方向键 │
│ • 迷你地图拖拽 │
│ │
│ 📐 适应视图 │
│ • 一键显示所有节点 │
│ • 重置缩放级别 │
│ │
└─────────────────────────────────────────┘
5.2 节点选择
单选:点击节点 → 选中(蓝色边框)
多选:
- Ctrl/Cmd + 点击 → 添加/取消选择
- 框选 → 选择区域内的所有节点
┌─────────────────────────────────────────┐
│ │
│ ┌─────┐ ┌─────┐ │
│ │ A ✓ │ │ B ✓ │ │
│ └─────┘ └─────┘ │
│ ╲ ╱ │
│ ╲ 框选 ╱ │
│ ╲ ╱ │
│ └───┘ │
│ │
│ ┌─────┐ ┌─────┐ │
│ │ C ✓ │ │ D │ │
│ └─────┘ └─────┘ │
│ │
└─────────────────────────────────────────┘
5.3 节点操作
移动节点:
拖拽节点 → 实时显示位置 → 松开 → 更新坐标
复制节点:
1. 选中节点
↓
2. Ctrl/Cmd + C(或点击复制按钮)
↓
3. Ctrl/Cmd + V(或点击画布)
↓
4. 生成新节点(ID 不同,配置相同)
删除节点:
1. 选中节点
↓
2. Delete/Backspace(或点击删除按钮)
↓
3. 确认对话框
↓
4. 删除节点及相关连线
5.4 快捷键设计
┌─────────────────────────────────────────┐
│ 快捷键列表 │
├─────────────────────────────────────────┤
│ │
│ Ctrl/Cmd + C → 复制选中节点 │
│ Ctrl/Cmd + V → 粘贴节点 │
│ Delete/Backspace → 删除选中节点 │
│ Ctrl/Cmd + Z → 撤销 │
│ Ctrl/Cmd + Y → 重做 │
│ Ctrl/Cmd + A → 全选 │
│ Ctrl/Cmd + 0 → 适应视图 │
│ Ctrl/Cmd + + → 放大 │
│ Ctrl/Cmd + - → 缩小 │
│ │
└─────────────────────────────────────────┘
六、节点渲染:从数据到 UI
6.1 节点组件结构
每个节点都是一个独立的 Vue 组件:
┌─────────────────────────────────────────┐
│ 节点组件结构 │
├─────────────────────────────────────────┤
│ │
│ ┌─────────────────────────┐ │
│ │ 节点头部 │ │
│ │ [图标] 节点名称 [×] │ │
│ ├─────────────────────────┤ │
│ │ │ │
│ │ 节点内容 │ │
│ │ (预览/摘要信息) │ │
│ │ │ │
│ ├─────────────────────────┤ │
│ │ ● 输入点 输出点 ● │ │
│ └─────────────────────────┘ │
│ │
└─────────────────────────────────────────┘
6.2 节点状态
┌─────────────────────────────────────────┐
│ 节点状态 │
├─────────────────────────────────────────┤
│ │
│ 默认状态 → 正常显示 │
│ 悬停状态 → 轻微高亮 │
│ 选中状态 → 蓝色边框 + 阴影 │
│ 运行状态 → 显示执行进度 │
│ 成功状态 → 绿色标记 │
│ 错误状态 → 红色标记 + 错误信息 │
│ │
└─────────────────────────────────────────┘
6.3 节点颜色系统
不同颜色的节点代表不同类型:
┌─────────────────────────────────────────┐
│ 节点颜色规范 │
├─────────────────────────────────────────┤
│ │
│ 🟢 绿色系 → 开始/结束节点 │
│ 🔵 蓝色系 → AI/工具节点 │
│ 🟣 紫色系 → 检索/知识节点 │
│ 🟠 橙色系 → 条件/分支节点 │
│ 🟡 黄色系 → 消息/输出节点 │
│ 🔴 红色系 → 错误/异常节点 │
│ 🟤 棕色系 → 循环/迭代节点 │
│ │
└─────────────────────────────────────────┘
七、配置表单:节点的"控制面板"
7.1 表单触发方式
┌─────────────────────────────────────────┐
│ 打开表单的方式 │
├─────────────────────────────────────────┤
│ │
│ 1. 双击节点 → 打开配置抽屉 │
│ 2. 点击节点 → 显示快捷菜单 → 配置 │
│ 3. 右键菜单 → 编辑配置 │
│ 4. 属性面板 → 始终显示在右侧 │
│ │
└─────────────────────────────────────────┘
7.2 表单布局
抽屉式(推荐):
┌──────────────────────────────────────────┐
│ 画布区域 │ 配置抽屉 │
│ │ ┌────────┐ │
│ [节点] ──► [节点] │ │ 表单 │ │
│ │ │ 内容 │ │
│ │ │ │ │
│ │ │ │ │
│ │ │ │ │
│ │ └────────┘ │
└──────────────────────────────────────────┘
弹窗式:
┌──────────────────────────────────────────┐
│ 画布区域(背景变暗) │
│ │
│ ┌────────────────────────────┐ │
│ │ 节点配置 │ │
│ ├────────────────────────────┤ │
│ │ │ │
│ │ 表单内容 │ │
│ │ │ │
│ │ │ │
│ │ [取消] [确定] │ │
│ └────────────────────────────┘ │
│ │
└──────────────────────────────────────────┘
7.3 表单内容组织
┌─────────────────────────────────────────┐
│ AI 节点配置表单 │
├─────────────────────────────────────────┤
│ │
│ 📋 基础设置 │
│ ├─ 模型选择 │
│ ├─ 温度参数 │
│ └─ 最大 Token 数 │
│ │
│ 💬 提示词配置 │
│ ├─ 系统提示词 │
│ ├─ 用户提示词 │
│ └─ 变量插入 │
│ │
│ 🛠️ 工具配置 │
│ ├─ 可用工具列表 │
│ └─ 工具参数 │
│ │
│ ⚙️ 高级设置 │
│ ├─ 错误处理 │
│ ├─ 重试策略 │
│ └─ 输出格式 │
│ │
└─────────────────────────────────────────┘
八、实时同步:画布与 DSL 的双向绑定
8.1 数据流设计
┌─────────────────────────────────────────┐
│ 数据流向 │
├─────────────────────────────────────────┤
│ │
│ 画布操作 │
│ ↓ │
│ [事件触发] │
│ ↓ │
│ Store 更新 │
│ ↓ │
│ DSL 自动同步 │
│ ↓ │
│ [防抖处理] │
│ ↓ │
│ 保存到后端 │
│ │
└─────────────────────────────────────────┘
8.2 关键同步点
┌─────────────────────────────────────────┐
│ 需要同步的操作 │
├─────────────────────────────────────────┤
│ │
│ ✅ 添加节点 → 更新 nodes 和 components│
│ ✅ 删除节点 → 清理相关数据 │
│ ✅ 移动节点 → 更新 position │
│ ✅ 连接节点 → 更新 edges │
│ ✅ 断开连线 → 删除 edge │
│ ✅ 配置节点 → 更新 component.params│
│ │
└─────────────────────────────────────────┘
8.3 防抖优化
频繁操作时,避免每次都保存:
用户操作 → 操作 1 → 操作 2 → 操作 3 → ... → 停止
↓ ↓ ↓ ↓
(不保存) (不保存) (不保存) 保存
↑
延迟 500ms
九、性能优化
9.1 大规模画布优化
问题:当节点超过 100 个时,渲染变慢。
解决方案:
┌─────────────────────────────────────────┐
│ 性能优化策略 │
├─────────────────────────────────────────┤
│ │
│ 1️⃣ 虚拟渲染 → 只渲染可视区域 │
│ 2️⃣ 懒加载 → 节点内容按需渲染 │
│ 3️⃣ 缓存 → 复用已渲染节点 │
│ 4️⃣ Web Worker → 复杂计算异步处理 │
│ 5️⃣ 降级显示 → 缩放时显示简化版 │
│ │
└─────────────────────────────────────────┘
9.2 渲染优化技巧
使用 memo 缓存组件:
<template>
<VueFlow>
<template #node-LLMNode="slotProps">
<LLMNode
v-memo="[slotProps.id, slotProps.data]"
v-bind="slotProps"
/>
</template>
</VueFlow>
</template>
避免不必要的响应式:
// 使用 markRaw 避免深度响应式
const nodeTypes = {
BeginNode: markRaw(BeginNode),
LLMNode: markRaw(LLMNode)
}
十、总结
10.1 画布实现要点回顾
┌────────────────────────────────────────────────────┐
│ 画布实现八大要点 │
├────────────────────────────────────────────────────┤
│ │
│ 1️⃣ 引擎选择 → VueFlow 提供强大基础能力 │
│ 2️⃣ 节点分类 → 清晰分类帮助用户快速找到节点 │
│ 3️⃣ 拖拽体验 → 预览+吸附让放置更精准 │
│ 4️⃣ 连线校验 → 防止无效连接保证流程正确 │
│ 5️⃣ 视图控制 → 缩放平移适应不同规模工作流 │
│ 6️⃣ 快捷操作 → 复制粘贴删除提升编辑效率 │
│ 7️⃣ 双向同步 → 画布与 DSL 实时保持一致 │
│ 8️⃣ 性能优化 → 虚拟渲染支撑大规模节点 │
│ │
└────────────────────────────────────────────────────┘
10.2 用户体验关键点
- ✅ 所见即所得:拖拽即生效,无需额外确认
- ✅ 即时反馈:悬停、选中、错误都有明确提示
- ✅ 容错设计:误操作可以撤销,删除需要确认
- ✅ 辅助功能:对齐网格、迷你地图、快捷键
系列文章导航:
- ✅ 架构篇 - 整体设计和技术选型
- ✅ DSL 设计篇 - 数据结构与序列化
- ✅ 画布实现篇 - VueFlow 集成与交互(本文)
- 📝 节点系统篇 - 18 种节点的实现细节
- 📝 表单系统篇 - 动态表单与变量引用
- 📝 状态管理篇 - Pinia Store 设计
- 📝 高级特性篇 - 迭代/循环/嵌套
- 📝 实战篇 - 从零构建一个完整智能体
作者注:本文基于 agent-flow 项目的实际代码分析编写,力求还原真实的架构设计过程。欢迎在评论区讨论或提问!
下一篇:从零开始设计一个智能体编排系统 - 节点系统篇(敬请期待)
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)