业内做AI应用的人,很多都卡在同一个地方:逻辑写死在代码里,业务方每次提个新需求都要改一轮后端,联调、发版、回归,折腾一圈下来,半天就没了。我自己带团队做AI Agent落地时,也踩过这个坑。后来决定换个思路,把流程编排从代码里抽出来,做成一个可视化平台,让业务人员能自己拖拽节点、配置参数、串联大模型调用,效果比预想的好很多。这篇就用一个实际项目的视角,拆解一下AI可视化编排平台从零到一的开发步骤,涉及架构选型、画布设计、执行引擎、Agent集成和稳定性治理,适合打算自建这类平台的团队参考。
1. 项目定位:搞清楚要做的是"工作流画布",不是"大模型封装"
很多团队一开始容易跑偏,以为可视化编排平台就是把大模型API包一层、画个界面让用户填Prompt。真做下去才发现,大模型调用只是其中一个环节,平台的核心竞争力在于编排能力——让各种异构节点(大模型、代码、API、数据库、人工审批)能灵活组合、按条件分支、循环执行、异常重试。所以第一步不是写代码,而是把产品边界想清楚。
1.1 先做减法:第一版只做三类节点
我见过不少项目死在一开始想做太多。可视化编排涉及的能力范围太宽,流程引擎、规则引擎、数据集成、权限体系、监控告警,哪个都能做几个月。第一版我的建议是死磕三类节点:
- 大模型节点:调用LLM,支持自定义Prompt模板、多轮上下文、结构化输出。
- 代码节点:运行Python/JS脚本,做数据清洗、格式转换、逻辑判断。
- 服务节点:通过HTTP调用外部API,或调用内部RPC服务。
这三类节点能覆盖80%以上的AI应用场景。比如一个“智能客服工单分类”流程,就是大模型节点分类 + 代码节点映射部门 + 服务节点创建工单。先跑通这个闭环,再去加数据库节点、消息队列节点、人工审批节点都不迟。
提示:第一版不要做嵌套子流程!子流程意味着要处理父流程和子流程之间的变量传递、上下文隔离、并发调度,复杂度直接翻倍。等主流程稳定了再上。
1.2 和现成工具的边界:为什么不直接用n8n或Dify
市面上现成的开源方案不少,n8n、Dify、Coze、LangFlow都能拖拽编排。那为什么还要自研?说句实话,自研的性价比体现在三个点:
- 深度定制:现成工具是通用产品,节点类型、交互方式、权限模型都是别人定死的。企业内部往往有统一登录、统一权限、统一工单系统,接现成工具反而要写一堆适配层。
- 与大模型深度集成:自研平台能直接复用团队已有的模型网关、Prompt管理、知识库检索能力,而不是被工具的封装限制住。
- 数据不出内网:很多场景要求流程执行的日志、中间数据全部留存内网,现成SaaS直接排除,开源自部署又要自己维护一套陌生系统。
但反过来,如果团队就两三个人、需求也不复杂,我不建议自研,直接用现成工具更快。自研的前提是你对编排能力有明确且长期的需求。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 整体架构:画布、编排、执行三层分离,别混在一起
架构设计的核心原则一句话:画布只管编辑,编排只管解析,执行只管跑任务。三层职责必须解耦,否则后期改任何一层都会牵一发动全身。
2.1 三层架构的具体职责
| 层级 | 核心职责 | 关键组件 |
|---|---|---|
| 画布层 | 节点拖拽、连线、属性配置、校验 | React Flow / AntV X6 |
| 编排层 | 将画布JSON解析为DAG,做拓扑排序和校验 | 自研解析器 |
| 执行层 | 按DAG顺序执行节点,管理状态和重试 | Celery / 自研Executor |
画布层保存的是“图数据结构”——每个节点有id、类型、坐标、配置,每条边有source、target、sourceHandle、targetHandle。这份JSON是整个平台的“源代码”,编排层拿到它之后做两件事:一是校验图是否合法(有没有环、有没有孤立节点、端口类型是否匹配),二是生成可执行的任务计划(先跑哪个、后跑哪个、哪些可以并行)。
执行层不关心图结构,它只消费编排层产出的任务计划。任务计划是一棵扁平的节点执行列表,每个节点带上了依赖信息和运行参数。执行层按依赖关系调度,把结果写回上下文,供下游节点读取。
2.2 状态存储:运行态和定义态必须分开
踩过一个很深的坑:一开始把节点的运行状态(正在运行、成功、失败)直接存在画布JSON里,结果并发调试时数据全乱了。后来彻底分离:
- 定义态:节点的配置、连线、坐标,存PostgreSQL或MongoDB,不可变。
- 运行态:每次执行的实例结果、日志、变量值,存Redis和ES,按执行ID隔离。
这样做的好处是:同一份流程定义可以被多个执行实例复用,互相不干扰。比如流程“日报生成”定义了10个节点,每天9点跑一次,每次运行都是一份独立的运行态数据。修改流程定义也只影响新触发的执行,不影响正在跑的实例。
2.3 产品视角的架构设计:给业务方“所见即所得”幻觉
架构不只是技术层面的,还要考虑产品心智。我的做法是让画布上的节点颜色直接反映运行状态——运行中黄色、成功绿色、失败红色。业务方盯着画布就能看到流程走到哪一步、卡在哪一步,不需要去翻日志。这个功能看起来简单,但要实现得漂亮,前端要轮询运行态接口,将状态映射到节点边框。这块工作量不算小,但值得,因为它是平台“可视化”三个字的关键体验。
3. 画布编辑器:选型、数据结构与交互细节
画布是整个平台的颜值担当,也是开发工作量最重的部分。前端技术栈我推荐React Flow或AntV X6。两个都试过,简单说说差异:React Flow生态好、类型定义清晰、自定义节点方便;X6有更多开箱即用的交互(比如框选、小鱼干、对齐线),国内文档友好。我最终选了React Flow,因为团队React技术栈和它配合最顺。
3.1 画布的数据结构:图JSON就是一切
画布数据模型建议用flat结构,不要嵌套:
json复制{
"id": "flow_001",
"name": "智能客服工单分类",
"nodes": [
{
"id": "node_input",
"type": "inputNode",
"position": { "x": 100, "y": 200 },
"data": {
"label": "用户输入",
"schema": { "type": "object", "properties": { "message": { "type": "string" } } }
}
},
{
"id": "node_llm_classify",
"type": "llmNode",
"position": { "x": 350, "y": 200 },
"data": {
"model": "gpt-4o-mini",
"promptTemplate": "请将以下用户消息分类为:售后、售前、其他。\n用户消息:{{input.message}}",
"outputSchema": { "type": "object", "properties": { "category": { "type": "string" } } }
}
}
],
"edges": [
{
"id": "edge_001",
"source": "node_input",
"target": "node_llm_classify",
"sourceHandle": "output",
"targetHandle": "input"
}
]
}
每个节点是独立对象,节点间的数据流通过边(edge)表达。这个结构可以完整保存画布状态,后端拿到后只需要遍历nodes和edges就能构建出DAG。前端所有操作(拖拽、删除、连线、改参数)都是对这个JSON的增删改查,用zustand或redux做状态管理都行。
3.2 节点属性配置的工程化设计
节点配置是用户交互最频繁的区域。每个节点类型都需要独立属性面板,比如LLM节点要配模型、Prompt、温度、输出格式;代码节点要配运行语言、代码内容、依赖包。属性面板建议做成schema驱动的——节点数据类型定义好属性schema,面板根据schema自动渲染表单。
typescript复制interface NodeSchema {
type: string;
label: string;
fields: FieldSchema[];
}
interface FieldSchema {
name: string;
label: string;
type: 'input' | 'textarea' | 'select' | 'codeEditor' | 'variable';
options?: { label: string; value: string }[];
required?: boolean;
}
采用schema驱动后,新增节点类型只需要后端加一个类型注册、前端加一个schema定义,属性面板不用动代码。这对快速迭代非常重要。
3.3 连线和校验:别让用户把图画错
可视化编排最容易出的问题是用户连错线。比如把文本输出连到了需要JSON的节点,或者连成了环。平台必须帮用户把错误拦在前面:
- 端口类型校验:每个节点的输入输出端口都有类型定义(string、object、number、any)。连线时只允许同类型端口连接,类型不匹配的边直接禁止创建。
- 循环检测:每次添加边时,前端就做一次DFS检测,若有环则报错并回滚操作。别等保存到后端再报错,那样体验很差。
- 孤立节点提示:保存前检查是否有节点没有任何出入边,给出警告让用户确认。
这里分享一个小技巧:端口类型建议允许“any”类型兼容一切,不然业务方会被类型系统搞得抓狂,连个线都要纠结选类型。平台要做的是兜底校验,而不是给用户制造障碍。
3.4 自动布局:应付“画布一团乱”的终极方案
业务方永远不按规矩画图,拖得七零八落。平台需要提供“自动布局”能力,一键把DAG整理成有序的层级图。我用的是dagre库,喂给它节点和边,它返回每个节点的新坐标:
typescript复制import dagre from 'dagre';
function autoLayout(nodes, edges) {
const g = new dagre.graphlib.Graph();
g.setGraph({ rankdir: 'LR', nodesep: 80, ranksep: 150 });
g.setDefaultEdgeLabel(() => ({}));
nodes.forEach(node => g.setNode(node.id, { width: 220, height: 80 }));
edges.forEach(edge => g.setEdge(edge.source, edge.target));
dagre.layout(g);
return nodes.map(node => {
const pos = g.node(node.id);
return { ...node, position: { x: pos.x - 110, y: pos.y - 40 } };
});
}
这个功能看着不起眼,但是业务方反馈里满意度最高的功能之一。
4. 执行引擎:DAG调度、上下文传递与重试机制
画布只是皮,执行引擎才是骨架。执行引擎负责把DAG图变成真正跑起来的任务流。它的核心问题有三个:调度顺序、数据传递、异常处理。
4.1 拓扑排序与并行调度
执行引擎拿到编排层产出的DAG后,先做拓扑排序。拓扑排序保证每个节点都在它的依赖节点之后执行。排序之后,引擎需要一个“就绪队列”——初始状态下,所有入度为0的节点进入就绪队列,引擎并发消费就绪队列中的节点,节点执行完毕后更新其下游节点入度,入度变为0的节点继续进入就绪队列,直到所有节点执行完毕。
python复制from collections import deque
def schedule(dag):
indegree = {node: 0 for node in dag.nodes}
children = {node: [] for node in dag.nodes}
for edge in dag.edges:
children[edge.source].append(edge.target)
indegree[edge.target] += 1
ready = deque([node for node in dag.nodes if indegree[node] == 0])
while ready:
node = ready.popleft()
execute(node)
for child in children[node]:
indegree[child] -= 1
if indegree[child] == 0:
ready.append(child)
注意,真实的执行引擎不能像上面这样按顺序串行处理ready队列,因为无依赖的节点应该被并发执行。建议用线程池或异步协程池,并发度可以配置,一般默认10个并发。
4.2 上下文:节点之间的数据到底怎么传
这个是最容易被低估的部分。节点之间传递数据,直接的做法是把上游节点的全部输出都传给下游。但实际项目里,一个节点可能有多个上游,下游只需要其中某几个字段。硬传全部数据有两个问题:一是上下文越来越大,浪费内存;二是命名冲突——两个上游都输出result,下游引用时到底取谁的?
我的方案是引入“变量命名空间”:
- 每个节点有唯一id(如node_llm_classify),它的输出挂在命名空间下:
nodes.node_llm_classify.output。 - 每个节点可以声明它的“输出映射”,把输出重新命名为语义化变量,比如
nodes.node_llm_classify.output.category可以直接映射为${business.category}。 - 下游节点引用变量时,只允许引用显式声明的变量,未声明的变量在参数配置时根本选不到。
变量引用在配置面板里做成“插入变量”按钮,用户点击后弹出变量树,选择想要的变量。这种方式比让用户手写表达式靠谱得多,极大减少拼写错误。
4.3 重试、超时与优雅退出
大模型调用的最大特点是不确定性。可能遇到限流、超时、返回格式异常。执行引擎必须内置三种保护机制:
- 超时控制:每个节点可以单独配置超时时间,LLM节点默认120秒,HTTP节点默认30秒。超时直接判定失败并走重试逻辑。
- 自动重试:可配置重试次数和退避策略。注意不是所有失败都值得重试——超时和5xx可以重试,4xx大概率重试也没用。我的实现里,HTTP节点默认只对408、429、5xx重试,重试间隔用指数退避加抖动。
- 失败策略:节点失败后,整个流程默认终止,但也可以配置为“忽略失败继续执行”。比如某个节点只是“增强信息”作用,失败不影响主流程,就设为忽略失败。
注意:重试要小心接口幂等性。如果节点是“创建工单”这类非幂等操作,重试可能导致重复创建。建议平台提供“幂等开关”,由流程设计者根据业务语义决定失败后是否重试。
4.4 批量执行的场景处理
真实业务里,编排流程总是配合批处理使用。比如“批量总结客户反馈”,可能一次性传入1000条数据,每条都跑一遍完整流程。如果简单用一个for循环串行执行,1000条可能要跑几个小时。
我的方案是“批次模式”:流程定义保持不变,执行引擎启动N个worker并发消费输入批次。每个输入项生成独立的执行实例,实例之间完全隔离,共享一份流程定义。数据库表设计上,增加一个batch_id字段,可以按批次查询所有执行结果。并发度根据下游依赖系统的承受能力配置,一般建议10-20。
5. Agent节点与LLM融合:让平台不只是"流程玩具"
可视化编排平台如果只支持“单次调用大模型”,那和普通低代码平台区别不大。真正的差异化在于支持Agent式交互——让大模型能决策调用什么工具、什么时候调用、如何根据中间结果调整计划。这块是平台进阶的关键。
5.1 运行时的判别式控制流
平台本身是过程式的,而Agent是决策式的。两者结合需要“判别式控制流”机制。我的做法是增加两类特殊节点:
- 条件分支节点:根据上游变量,决定走哪条分支线。类似编程的if-else。配置面比较简单——选一个变量、选一个运算符(等于、包含、大于等)、填一个值。
- 循环节点:对数组变量逐项执行同一段子流程。例如“对每个客户执行满意度分析”。
有了这两个节点,业务方就能拼出“带逻辑”的流程:先跑大模型节点做分类,再根据分类结果走不同分支,分支里又可以循环处理明细数据。
5.2 让大模型调用工具的“Agent节点”
在节点类型里扩展“Agent节点”:它接受一个目标任务和一组工具列表(工具就是平台上的HTTP服务节点或内部函数),运行时大模型自主决策用哪个工具、传什么参数。
具体实现逻辑:
- Agent节点接收任务描述和可选上下文。
- 平台将可用工具列表(名称、描述、参数Schema)拼进系统Prompt。
- Agent模型返回工具调用指令(function_call)。
- 平台执行工具调用,把结果拼回对话上下文,再次询问Agent。
- 重复上述过程,直到Agent给出最终答案或达到最大轮次。
技术实现上,可以使用LangChain的AgentExecutor或直接调OpenAI的function calling。但注意,可控性比智能性更重要——必须设置最大工具调用轮次(默认5轮),并限制Agent只能访问流程内声明的工具,不能让Agent随便调外部API。
实操心法:Agent节点强大但不可控,建议只开放给高级用户。普通业务场景优先用条件分支+固定调用的方式,跑通了再考虑转成Agent节点。毕竟流程要能解释、能复盘、能排查,全黑盒的Agent决策在生产环境很难维护。
5.3 人机协同:关键的审批节点
AI全自动跑流程不现实,很多场景需要人参与审批。平台需要内置“人工审批节点”,流程执行到该节点时暂停,等待指定人员在待办列表里审批通过/拒绝。审批通过后,流程继续执行;拒绝则流程终止或走拒绝分支。
这个功能实现不复杂:执行引擎检测到审批节点时,将节点状态标记为“等待审批”,将执行实例挂起,生成一条审批待办。审批接口被调用后,恢复实例执行。但要注意“挂起恢复”的实现——不要用线程sleep方案,而要把执行状态持久化,通过状态机转换恢复。这部分建议和执行引擎的持久化一起考量。
6. 平台稳定性与可观测性:上线之前必须想清楚的事
编排平台是“生产工具”,不是“Demo玩具”。业务方一旦依赖它跑核心流程,稳定性问题就是最高优先级。这块我踩过的坑最多,单独说说。
6.1 全链路日志与执行回溯
流程跑挂了,如果没有日志,排查问题会让人崩溃。平台上线第一天就要做“执行快照”:每次执行实例,把每个节点的输入、输出、耗时、状态变更和错误信息全部记下来。存储建议分两类:
- 结构化数据:节点输入输出、状态流转,存ES/MongoDB,字段化方便检索。
- 文本日志:运行时的完整日志流,存Loki或S3文件,按执行ID关联。
前端做一个“执行详情”页面:左侧显示DAG图和节点状态,右侧显示当前节点的完整输入输出和日志。业务方报障时,直接把执行ID发过来,技术团队按图索骥就能定位问题。
6.2 告警机制:不要等用户发现故障
平台自身要监控三件事:流程执行失败率、节点执行耗时、并发积压数。我的实现很简单:定时任务每隔1分钟统计最近10分钟的失败率,超过阈值(比如5%)就发到企业微信/钉钉机器人。耗时监控解决的是“流程没挂但变慢了”的问题——如果某个节点P95耗时翻倍,说明模型或上游服务出现了性能退化。
最容易被忽略的是“静默失败”——节点显示成功,但输出内容异常(比如模型输出了空字符串)。这个监控方式只能靠“输出Schema校验”:节点输出前用JSON Schema校验,不匹配就判定为失败。这个校验逻辑和画布属性配置里定义的输出Schema是同一套。
6.3 配置即代码:版本管理与灰度发布
可视化编排平台的流程配置是“活代码”——业务方改一个节点参数就可能改变整个流程行为。所以流程定义必须纳入版本管理:每次保存生成新版本,支持回滚;发布时可以进行“运行环境”隔离(测试环境/生产环境)。发布流程可以做成:测试环境编辑调试,点击发布后生产环境生成新版本,并且支持灰度——先让10%的流量跑新版本,观察无误后全量切换。
这块我建议不要自己造轮子,可以直接复用Git的分支/标签模型:每次发布创建一个tag,线上运行的是某个tag的流程快照。审计时能看到某次执行使用的是哪个版本的流程定义。
6.4 性能瓶颈和扩缩容
执行引擎的并发和实例数量需要监控,因为LLM调用是慢操作,一个节点可能要几十秒。如果100个实例同时执行,每个实例3个LLM节点,那实时就是300个并发LLM请求。模型API的限流必须提前考虑。我的建议:
- 在平台层做“模型并发池”,每个模型的key配置最大并发数,超出的请求排队等待。
- 执行引擎的worker数量可以根据队列积压情况动态调整。K8s部署的话,用HPA根据队列深度扩缩容Pod。
- 数据库和Redis的性能不会成为瓶颈(因为耗时大头在外部API调用),但日志写入量会很大,注意ES的写入性能和磁盘容量。
7. 开发排期与团队配置的具体建议
按我的经验,一个小规模(3-4人)团队大概可以按这个节奏推进:
| 阶段 | 时间 | 里程碑 |
|---|---|---|
| 需求梳理与原型 | 1-2周 | 明确节点类型、交互流程、边界范围 |
| 画布编辑器MVP | 3-4周 | 支持节点拖拽、连线、属性配置、保存 |
| 执行引擎MVP | 2-3周 | 支持DAG调度、串并行执行、变量传递 |
| 大模型与Agent节点 | 2-3周 | 各类节点可用,跑通真实业务场景 |
| 平台稳定性 | 2周 | 日志、告警、版本管理、审批节点 |
团队配置上,画布前端是最重的,建议2个前端;编排与执行引擎1-2个后端;Agent与大模型集成1个后端。总人力大概3-5人,2-3个月可以做出一版能用、能接真实业务流的平台。
这个排期相对保守,前提是画布编辑器基于React Flow这类成熟库做二次开发,执行引擎不自研复杂的工作流引擎(比如不引入BPMN规范),一切以“够用”为前提。
最后再说一个个人体会比较深的事:这类平台最容易被忽略的,不是技术,而是流程设计者的使用习惯。如果节点配置项太复杂,业务方宁愿继续找研发写死代码。所以开发过程中,我建议让业务方的代表种子用户提前介入测试,根据他们的反馈反复调整配置面板和交互逻辑。画布编辑器做得顺不顺手、变量引用好不好用、错误提示能不能看懂,比底层架构的优雅程度更能决定平台落地成不成功。
另外,控制流节点(条件分支、循环)务必在第一批就做进去。没有控制流,平台只能做“线性流水线”,业务场景覆盖太窄,价值会被严重低估。这是我在后续迭代中深刻的体会。
