周一至周五 09:00 - 18:00(UTC+08:00) 
©2026 NST LABS TECH LTD. 保留所有权利。Ivy LinCommunity & Content Lead
如何在 2026 年为 AI 代理创建一个开源可视化工作流构建器
TL;DR
- AI代理的可视化工作流构建器是一个连接节点的画布加上一个按依赖顺序运行它们的引擎。 画布是一个用户界面问题(拖动、连接、定位);引擎是一个图形问题(有向无环图的拓扑排序,或DAG)。
- React Flow(发布为
@xyflow/react包,当前版本12.11.3,MIT许可证)是画布部分的标准开源库。 它渲染节点和边,处理拖动和缩放,并提供钩子用于读取和修改图的状态。
- 执行部分——决定何时运行,按什么顺序,使用什么输入——是画布库所不提供的。 必须将其编写为一个小的独立引擎:对图进行排序,运行每个节点的处理程序,并将每个节点的输出传递给从其连接的节点。
- 需要实时网络数据的节点(“fetch”或“crawl”节点)需要其背后有一个真实的获取层, 而不仅仅是一个
fetch()调用——JavaScript渲染的页面和反机器人保护会在工作流的其余部分之前静默打破手动制作的HTTP请求。
- 本文的执行引擎实际上已经运行过, 而不仅仅是描述:一个四节点图(触发→爬取→提示→输出)在此环境中进行了拓扑排序并执行,下面展示了真实捕获的输出。
- 一个手动构建的工作流构建器是一个真实的、可构建的周末项目,有两个验证过的开源组件(React Flow加上拓扑排序),但是它不包含生产工具所需的任何操作功能(身份验证、重试、节点级日志记录、版本控制)——本文明确指出了这一限制,而不是轻描淡写。
介绍:什么是“构建自己的代理工作流构建器”
AI代理工作流构建器在结构上是两个不同的系统,统一使用一个用户界面:一个画布,用户在其中排列和连接节点,一个引擎读取该图形并实际执行它。对“开放代理构建器”风格工具的搜索兴趣随着无代码和低代码AI平台的兴起而增长,但大多数出现的内容要么是一个没有可见代码的托管产品,要么是一个短暂的演示,绘制了一个漂亮的画布但从未实际运行任何东西。本文构建了真实的东西:一个使用开源React Flow库的画布,以及一个与实时数据获取节点连接的小型依赖顺序执行引擎,且该执行引擎实际上已经运行,并捕获了其输出如下。
故意排除两个范围。首先,这不是任何具体商业或开源工作流产品的操作指南——它使用中立库教授基础模式,因此结果可以由你自行扩展。其次,这里的节点中的“AI”指的是节点的处理程序调用语言模型或工具;图形引擎本身对节点内部执行的内容没有任何意见,这恰恰使其对于非AI自动化也具有可重用性。
安装:React Flow和一个最小项目
React Flow以@xyflow/react在npm上发布。在撰写本文时,发布的版本为12.11.3(已直接确认与npm注册表一致,并在验证本文时在新项目中通过普通的npm install干净安装),该包根据xyflow GitHub仓库的规定为MIT许可证。
搭建一个最小的Vite + React项目,然后添加库:
npm create vite@latest agent-builder -- --template react
cd agent-builder
npm install @xyflow/react
React Flow还提供一个样式表,必须导入一次,否则画布将完全不渲染布局:
import '@xyflow/react/dist/style.css';
配置:画布框架
React Flow画布需要三样东西:一个节点数组,一个边数组,以及<ReactFlow>组件本身。官方文档的最小示例是这个结构:
import { ReactFlow, Background, Controls } from '@xyflow/react';
import '@xyflow/react/dist/style.css';
const initialNodes = [
{ id: 'n1', position: { x: 0, y: 0 }, data: { label: 'Node 1' }, type: 'input' },
{ id: 'n2', position: { x: 100, y: 100 }, data: { label: 'Node 2' }, type: 'output' },
];
const initialEdges = [
{ id: 'n1-n2', source: 'n1', target: 'n2', type: 'smoothstep', label: 'connects with' },
];
export default function App() {
return (
<div style={{ height: '100%', width: '100%' }}>
<ReactFlow nodes={initialNodes} edges={initialEdges}>
<Background />
<Controls />
</ReactFlow>
</div>
);
}
该版本是静态的——一旦渲染,数组就不会改变。一个真正的构建器需要画布对拖放和新连接作出反应,这就是useNodesState和useEdgesState的作用。每个钩子返回当前数组、一个 setter 和一个变更处理程序,直接连接到<ReactFlow>的onNodesChange / onEdgesChange属性:
import { ReactFlow, useNodesState, useEdgesState } from '@xyflow/react';
export default function App() {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
return (
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
/>
);
}
React Flow 文档本身指出,这些钩子旨在为受控流的原型设计,且更大的构建器应在节点和边的状态需要在画布组件外共享时转向专用的状态存储(特别提到 Zustand)——真正的执行引擎会这样做,因为它必须读取画布所显示的相同图形。
基本实现:代理操作的节点类型
默认的节点类型(input、default、output)仅渲染一个标签。一个代理工作流需要包含配置信息的节点类型——用于获取步骤的 URL、用于模型步骤的提示模板——并暴露其他节点可以附加的连接点。React Flow 的自定义节点模式是注册在nodeTypes映射中的普通 React 组件:
const nodeTypes = {
crawlNode: CrawlNode,
promptNode: PromptNode,
};
<ReactFlow nodeTypes={nodeTypes} nodes={nodes} edges={edges} />
import { Handle, Position } from '@xyflow/react';
export function CrawlNode({ data }) {
return (
<div className="workflow-node">
<div className="workflow-node__title">获取页面</div>
<div className="workflow-node__field">{data.url}</div>
<Handle type="target" position={Position.Left} />
<Handle type="source" position={Position.Right} />
</div>
);
}
同时具有target句柄(左侧)和source句柄(右侧)的节点可以位于链的中间——它接收来自上游节点的连接并将自己的输出发送到下游。触发节点仅需要一个source句柄;终端输出节点仅需要一个target句柄。
高级模式:实际运行图形的执行引擎
画布只生成两个数组——节点和边。将其转变为一个运行的工作流意味着回答一个 UI 库无法回答的问题:节点以什么顺序运行?答案是拓扑排序——在访问每个节点之前,必须先运行每个输入到该节点的节点。这是图论,而不是 React,因此它被作为一段普通脚本为本文编写和执行,而不是作为一张图表留下。
测试的图:一个触发节点,一个抓取节点,用于获取页面,一个提示节点,用于总结抓取节点返回的内容,以及一个输出节点。
function topoSort(nodeList, edgeList) {
const indegree = new Map(nodeList.map((n) => [n.id, 0]));
const adjacency = new Map(nodeList.map((n) => [n.id, []]));
for (const edge of edgeList) {
adjacency.get(edge.source).push(edge.target);
indegree.set(edge.target, indegree.get(edge.target) + 1);
}
const queue = nodeList.filter((n) => indegree.get(n.id) === 0).map((n) => n.id);
const order = [];
while (queue.length) {
const id = queue.shift();
order.push(id);
for (const next of adjacency.get(id)) {
indegree.set(next, indegree.get(next) - 1);
if (indegree.get(next) === 0) queue.push(next);
}
}
if (order.length !== nodeList.length) {
throw new Error('图存在循环——工作流必须是 DAG');
}
return order;
}
循环检查比看起来更重要:画布 UI 会乐于允许某人拖动创建一个循环的边缘(节点 A 输入节点 B 输入节点 A),如果没有这个检查,执行引擎要么会挂起,要么会无声地丢弃节点,而不是告诉构建工作流的人出错了。
手中有执行顺序,运行工作流是一个循环,它调用每个节点的处理程序,并沿着指向它的边缘将输出线程传递到输入:
async function run(nodes, edges) {
const order = topoSort(nodes, edges);
const nodesById = new Map(nodes.map((n) => [n.id, n]));
const outputs = new Map();
const log = [];
for (const id of order) {
const node = nodesById.get(id);
```javascript
const incoming = edges.filter((e) => e.target === id).map((e) => outputs.get(e.source));
let result;
switch (node.type) {
case 'trigger': result = '运行开始'; break;
case 'crawl': result = await runCrawlNode(node); break;
case 'prompt': result = runPromptNode(node, incoming[0]); break;
case 'output': result = incoming[0]; break;
default: throw new Error(`未知节点类型: ${node.type}`);
}
outputs.set(id, result);
log.push({ node: id, type: node.type, output: result });
}
return { order, log };
}
runCrawlNode 是一个手写的 fetch() 调用,它在任意 URL 上会遇到问题:很多真实页面是客户端渲染的,阻止没有浏览器指纹的请求,或者需要代理才能可靠访问。为了验证这篇文章,该处理程序调用了 Nstproxy Crawl 的 POST /api/v1/crawl/scrape 端点(在 docs.nstproxy.com/docs/crawl 中有文档)——它返回渲染的 Markdown,而不是原始的 HTML——所以爬虫节点的工作被简化为解包响应,而不是重新实现一个浏览器:
async function runCrawlNode(node) {
const response = await fetch('https://api.nstproxy.com/api/v1/crawl/scrape', {
method: 'POST',
headers: { 'x-api-key': process.env.NSTPROXY_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({ url: node.data.url, formats: ['markdown'], onlyMainContent: true }),
});
const envelope = await response.json();
if (envelope.err) throw new Error(envelope.msg || '爬虫请求失败');
const inner = envelope.data;
if (!inner.success) throw new Error(inner.status || '爬虫未完成');
return inner.data.markdown;
}
此环境没有对任意域的出站网络访问权限,也没有有效的 Nstproxy API 密钥,因此下面的验证运行替代了一个本地 HTTP 固件,它返回 Nstproxy Crawl 自身文档所指定的确切嵌套响应信封(外部 code/err/msg/data,内部 data.success/status,页面有效负载在 data.data.markdown)以替代真实端点——这里披露而不是呈现为实时 API 调用。解包逻辑本身(检查 err,然后 success,再读取 data.data.markdown)与真实端点完全相同;仅在此测试中目标发生了变化。
示例:运行四节点图
为这次运行而使用的图:start(触发器)→ fetch(爬虫节点,目标是 Nstproxy Crawl 产品页面)→ prompt(总结爬虫节点返回的内容)→ output。在本环境中使用 Node.js (v22.22.2) 执行,fetch 指向一个本地固件服务器,替代真实的 Nstproxy Crawl API,如上所述。
{
"order": ["start", "fetch", "prompt", "output"],
"log": [
{ "node": "start", "type": "trigger", "output": "运行开始" },
{
"node": "fetch",
"type": "crawl",
"output": "# Nstproxy Crawl\n\nNstproxy Crawl 将 URL 转换为干净的 Markdown、JSON 或截图,只需一次 API 调用,内置 JavaScript 渲染和代理支持访问。"
},
{
"node": "prompt",
"type": "prompt",
"output": "用一句话总结此页面:\n\n# Nstproxy Crawl\n\nNstproxy Crawl 将 URL 转换为干净的 Markdown、JSON 或截图,只需一次 API 调用,内置 JavaScript 渲染和代理支持访问。\n\n[模拟模型输出] Nstproxy Crawl — 从抓取页面生成的一行总结。"
},
{
"node": "output",
"type": "output",
"output": "用一句话总结此页面:\n\n# Nstproxy Crawl\n\nNstproxy Crawl 将 URL 转换为干净的 Markdown、JSON 或截图,只需一次 API 调用,内置 JavaScript 渲染和代理支持访问。\n\n[模拟模型输出] Nstproxy Crawl — 从抓取页面生成的一行总结。"
}
]
}
order 数组确认拓扑排序将每个节点放置在其依赖项之后,而 log 数组显示每个节点的输出流入下一个——爬虫节点的 Markdown 成为提示节点的输入,而提示节点的(模拟)摘要成为最终输出。提示节点的模型调用是确定性的占位符,而不是实时 LLM API 调用,以与上述爬虫替换相同的方式披露;在 runPromptNode 内部用真实模型调用替代模拟行意味着调用任意提供者的 SDK。
快速查看
```
每个工作流中的“fetch”节点最终都会涉及到需要JavaScript渲染的页面或阻止普通HTTP请求——Nstproxy Crawl通过一个API调用处理这一层,而不是自己制作浏览器。
诚实的限制
一个周末构建的画布加上一个拓扑排序引擎证明了核心模式有效,但它缺少生产工作流工具所需的一切围绕该核心的内容。没有持久性层——本文中的图形在一次运行中存在于内存中,且会话之间没有任何内容被保存。没有重试或部分失败处理——如果爬虫节点的请求失败,整个运行会抛出而不是重试或路由到错误分支。没有显示在画布UI上的每个节点执行日志,没有方法在运行中暂停和检查状态,没有身份验证或多用户访问控制,也没有保存工作流的版本控制。对于安全地运行任意用户提供代码的节点处理程序,也没有保护——真实产品中的任何“自定义代码”节点类型都需要一个沙箱执行环境,而本文中的普通switch语句并不提供。这些都不属于React Flow解决的问题;它们是将此模式转变为其他人可以依赖的工具的实际工程工作。如果爬虫节点的数据抓取层是外包而不是手动构建的,则在提交工作流的抓取步骤之前,值得检查托管API的定价与预期调用量的对比。
故障排除
画布没有样式或节点重叠的渲染。 这几乎总是意味着跳过了@xyflow/react/dist/style.css的导入——React Flow使用绝对坐标定位节点,这取决于它自己的基本样式表。
带有循环的工作流挂起或默默丢弃节点。 这是上面topoSort函数抛出时的循环情况——画布UI没有内置方法来防止有人连接创建循环的边缘,因此引擎必须明确检查,而不是假设用户构建的每个图都是有效的DAG。
自定义节点不接受连接。 检查节点组件是否包含具有正确type(source或target)的Handle,并且它实际被渲染——一个省略<Handle>的自定义节点组件在视觉上渲染良好,但永远无法连接到另一个节点。
某些URL的“fetch”节点工作,而其他URL则失败。 这通常是JavaScript渲染或机器人检测问题,而不是节点代码中的错误——普通的fetch()调用只看到服务器返回的原始HTML,而不是浏览器在运行页面的脚本后会渲染的内容,这就是为什么本文中的爬虫节点调用一个渲染感知的API而不是直接抓取URL。
结论
构建AI代理工作流构建器清晰地分为两个经过验证的部分:用于画布的React Flow(@xyflow/react,版本12.11.3,MIT许可)和一个小型手写的拓扑排序引擎用于执行——两者之间没有相互依赖,这就是它们可以独立开发和测试的原因,如本文所示。结果是一个真实的、可运行的模式,而不是一个图表,已被一个实际的四节点图从头到尾执行验证。它不是一个完成的产品——持久化、重试、沙箱化以及访问控制仍然是建立在该核心之上的真正工程工作。关于所用数据抓取层的更多背景信息,请参见Nstproxy Crawl发布帖子。
常见问题
问:我需要特别使用React Flow,还是可以使用其他库?
React Flow(@xyflow/react)是本文使用和验证的库,但它并不是唯一的选择——Svelte Flow(同一团队的Svelte等价物)覆盖非React项目,任何可以公开节点位置、边缘和注册自定义组件的画布库都可以填补相同的角色。这里描述的执行引擎与哪个画布库渲染完全独立,因为它只消耗普通的节点和边缘数组。
问:执行引擎需要在浏览器中运行吗?
不——对于大多数真实的工作流程而言,它不应该。画布在浏览器中运行,因此人可以编辑图形,但执行工作流程(尤其是带有 API 密钥或长时间运行的步骤)应在服务器上进行。本文所示的引擎是纯 JavaScript,没有浏览器依赖性,因此可以在 Node.js 中运行,正如在此处测试的那样,或在任何支持 fetch 的后端运行时内部运行。
在运行之前拒绝它。本文中的 topoSort 函数在排序顺序不包括每个节点时会抛出错误,这正是当存在循环时发生的——在画布 UI 中捕获该错误并告知相关节点,而不是让引擎挂起。
可以——节点处理器只是一个函数;它可以根据需要进行尽可能多的调用,然后返回其输出。引擎施加的唯一约束是,节点的处理器仅接收指向它的边的节点的输出,因此任何需要来自两个上游源的数据的节点需要两个传入边。
问:用真实的语言模型调用替换模拟提示节点的最快方法是什么?
用对正在使用的模型提供者的 SDK 的调用替换 runPromptNode 的主体,将传入节点的输出作为上下文,并将模型的响应作为字符串返回——引擎的其余部分(排序、输出传递、日志记录)不需要改变,因为它只关心节点处理器返回一个值。
问:为什么爬虫节点调用 API,而不是直接使用 fetch()?
普通的 fetch() 调用只接收服务器在任何客户端 JavaScript 运行之前发送的 HTML,许多页面在之后渲染其实际内容——渲染感知的 API 如同浏览器一样执行页面,然后在返回 Markdown 或 HTML 之前,这正是本文示例中的爬虫节点所依赖的。
立即访问住宅、数据中心、IPv6 与 ISP 高质量代理池。