使用React Flow构建一个AI工作流工具
一、React Flow 核心概念(AI 工作流视角)
| 概念 | 说明 | AI 工作流中的意义 |
|---|---|---|
| Node(节点) | 图中的基本单元,代表一个操作 | 如:User Input、LLM Call、RAG Retriever、Code Executor |
| Edge(边) | 连接两个节点的有向线 | 表示数据流向(如 prompt → LLM → output) |
| Handle(句柄) | 节点上的连接点(source/target) | 控制哪些端口可连入/连出(如 LLM 节点只有 1 个输入、1 个输出) |
| ReactFlow Provider | 提供全局状态和方法 | 管理 nodes/edges、缩放、选择等 |
| Custom Node | 自定义节点组件 | 可嵌入表单、下拉菜单、实时预览等 |
关键思想:每个节点 = 一个 AI 技能(Skill/Tool),整个图 = 一个可执行的 Agent 工作流。
二、目标功能清单
我们要构建的 AI 工作流工具需支持:
✅ 可视化拖拽节点
✅ 右键菜单:添加新节点 / 删除选中节点
✅ 节点类型:Input、LLM、Output、RAG
✅ 导出为 JSON(用于保存或分享)
✅ “发送到后端”按钮:将 flow 发送给 API 执行
✅ 响应式布局 + 性能优化
三、开发环境准备
1. 前提
- Node.js ≥ 18
- npm / yarn / pnpm
2. 创建项目
npx create-react-app ai-workflow-builder cd ai-workflow-builder
3. 安装依赖
npm install @xyflow/react uuid axios # 或 yarn add @xyflow/react uuid axios
- @xyflow/react:React Flow 官方包(v12+)
- uuid:生成唯一节点 ID
- axios:用于调用后端 API
4. 清理默认文件
删除 src/App.css、src/logo.svg,保留 src/App.js 和 src/index.js
四、完整可执行代码实现
文件结构
src/ ├── components/ │ ├── nodes/ │ │ ├── InputNode.jsx │ │ ├── LLMNode.jsx │ │ └── OutputNode.jsx │ └── RightClickMenu.jsx ├── App.js └── index.js
Step 1:自定义节点组件(src/components/nodes/*.jsx)
InputNode.jsx
// src/components/nodes/InputNode.jsx
import { Handle, Position } from '@xyflow/react';
export default function InputNode({ data }) {
return (
<div className="border-2 border-blue-500 rounded-lg p-3 bg-blue-50 w-48 shadow">
<strong>📝 User Input</strong>
<Handle type="source" position={Position.Right} id="out" />
</div>
);
}LLMNode.jsx
// src/components/nodes/LLMNode.jsx
import { Handle, Position } from '@xyflow/react';
import { useState } from 'react';
export default function LLMNode({ data, id }) {
const [model, setModel] = useState(data?.model || 'gpt-4o');
const handleModelChange = (e) => {
if (data?.onModelChange) {
data.onModelChange(id, e.target.value);
}
};
return (
<div className="border-2 border-purple-500 rounded-lg p-3 bg-white w-48 shadow">
<strong>🧠 LLM</strong>
<div className="mt-2">
<select value={model} onChange={handleModelChange} className="w-full text-sm border rounded p-1">
<option value="gpt-4o">GPT-4o</option>
<option value="claude-3-5-sonnet">Claude 3.5</option>
<option value="qwen:7b">Qwen-7B (Ollama)</option>
</select>
</div>
<Handle type="target" position={Position.Left} id="in" />
<Handle type="source" position={Position.Right} id="out" />
</div>
);
}
OutputNode.jsx
// src/components/nodes/OutputNode.jsx
import { Handle, Position } from '@xyflow/react';
export default function OutputNode() {
return (
<div className="border-2 border-green-500 rounded-lg p-3 bg-green-50 w-48 shadow">
<em>✅ Final Output</em>
<Handle type="target" position={Position.Left} id="in" />
</div>
);
}
Step 2:右键菜单组件(src/components/RightClickMenu.jsx)
// src/components/RightClickMenu.jsx
import { useState, useEffect } from 'react';
export default function RightClickMenu({ x, y, onAddNode, onClose }) {
const [position, setPosition] = useState({ x, y });
useEffect(() => {
setPosition({ x, y });
}, [x, y]);
const handleClick = (type) => {
onAddNode(type, position.x, position.y);
onClose();
};
// 防止菜单超出视窗
const menuStyle = {
position: 'fixed',
top: Math.min(position.y, window.innerHeight - 120),
left: Math.min(position.x, window.innerWidth - 150),
backgroundColor: 'white',
border: '1px solid #ccc',
borderRadius: '4px',
boxShadow: '0 2px 10px rgba(0,0,0,0.2)',
zIndex: 1000,
padding: '4px 0',
};
return (
<div style={menuStyle} className="text-sm">
<button onClick={() => handleClick('input')} className="block w-full text-left px-4 py-2 hover:bg-gray-100">➕ Add Input</button>
<button onClick={() => handleClick('llm')} className="block w-full text-left px-4 py-2 hover:bg-gray-100">➕ Add LLM</button>
<button onClick={() => handleClick('output')} className="block w-full text-left px-4 py-2 hover:bg-gray-100">➕ Add Output</button>
<hr className="my-1" />
<button onClick={() => handleClick('delete')} className="block w-full text-left px-4 py-2 hover:bg-red-50 text-red-600">🗑️ Delete Selected</button>
</div>
);
}
Step 3:主应用逻辑(src/App.js)
// src/App.js
import React, { useState, useCallback, useRef } from 'react';
import ReactFlow, {
Controls,
MiniMap,
Background,
useNodesState,
useEdgesState,
addEdge,
ConnectionMode,
applyNodeChanges,
applyEdgeChanges,
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import { v4 as uuidv4 } from 'uuid';
import axios from 'axios';
// 导入自定义节点
import InputNode from './components/nodes/InputNode';
import LLMNode from './components/nodes/LLMNode';
import OutputNode from './components/nodes/OutputNode';
import RightClickMenu from './components/RightClickMenu';
const nodeTypes = {
input: InputNode,
llm: LLMNode,
output: OutputNode,
};
const initialNodes = [
{ id: '1', type: 'input', position: { x: 100, y: 100 }, data: {} },
{ id: '2', type: 'llm', position: { x: 300, y: 100 }, data: { model: 'gpt-4o' } },
{ id: '3', type: 'output', position: { x: 500, y: 100 }, data: {} },
];
const initialEdges = [
{ id: 'e1-2', source: '1', target: '2', sourceHandle: 'out', targetHandle: 'in' },
{ id: 'e2-3', source: '2', target: '3', sourceHandle: 'out', targetHandle: 'in' },
];
export default function App() {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const [menu, setMenu] = useState(null);
const reactFlowWrapper = useRef(null);
// 处理模型变更
const handleModelChange = useCallback((nodeId, model) => {
setNodes((nds) =>
nds.map((node) =>
node.id === nodeId ? { ...node, data: { ...node.data, model } } : node
)
);
}, [setNodes]);
// 添加节点
const addNode = useCallback((type, x, y) => {
if (type === 'delete') {
// 删除选中节点
const selectedIds = nodes.filter(n => n.selected).map(n => n.id);
setNodes(nds => nds.filter(n => !selectedIds.includes(n.id)));
setEdges(eds => eds.filter(e => !selectedIds.includes(e.source) && !selectedIds.includes(e.target)));
return;
}
const newNode = {
id: uuidv4(),
type,
position: { x: x - 80, y: y - 30 },
data: type === 'llm' ? { model: 'gpt-4o', onModelChange: handleModelChange } : {},
};
setNodes(nds => nds.concat(newNode));
}, [nodes, setNodes, setEdges, handleModelChange]);
// 右键事件
const onPaneClick = useCallback(() => setMenu(null), []);
const onPaneContextMenu = useCallback((event) => {
event.preventDefault();
setMenu({ x: event.clientX, y: event.clientY });
}, []);
// 连线
const onConnect = useCallback((params) => {
if (params.source !== params.target) {
setEdges(eds => addEdge(params, eds));
}
}, [setEdges]);
// 导出 JSON
const exportFlow = () => {
const flowData = { nodes, edges };
const dataStr = JSON.stringify(flowData, null, 2);
const blob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'ai-workflow.json';
a.click();
URL.revokeObjectURL(url);
};
// 发送到后端执行
const sendToBackend = async () => {
try {
const response = await axios.post('http://localhost:8000/execute', { nodes, edges });
alert('Execution started! Result: ' + JSON.stringify(response.data));
} catch (error) {
alert('Error: ' + error.message);
}
};
return (
<div style={{ width: '100vw', height: '100vh' }} ref={reactFlowWrapper}>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onPaneClick={onPaneClick}
onPaneContextMenu={onPaneContextMenu}
nodeTypes={nodeTypes}
connectionMode={ConnectionMode.LOOSE}
fitView
attributionPosition="top-right"
>
<Background />
<Controls />
<MiniMap />
{/* 顶部工具栏 */}
<div style={{
position: 'absolute',
top: 10,
left: 10,
display: 'flex',
gap: 10,
zIndex: 10,
}}>
<button onClick={exportFlow} className="bg-blue-500 text-white px-3 py-1 rounded text-sm">
📤 Export JSON
</button>
<button onClick={sendToBackend} className="bg-green-500 text-white px-3 py-1 rounded text-sm">
🚀 Send to Backend
</button>
</div>
</ReactFlow>
{/* 右键菜单 */}
{menu && (
<RightClickMenu
x={menu.x}
y={menu.y}
onAddNode={addNode}
onClose={() => setMenu(null)}
/>
)}
</div>
);
}
Step 4:简单后端模拟(可选,用于测试)
创建 backend/server.js(需 Node.js):
// backend/server.js
const express = require('express');
const cors = require('cors');
const app = express();
app.use(cors());
app.use(express.json());
app.post('/execute', (req, res) => {
console.log('Received flow:', req.body);
// 模拟执行:返回节点数量
res.json({
status: 'success',
message: `Executed workflow with ${req.body.nodes.length} nodes`,
result: "Hello from AI Workflow!"
});
});
app.listen(8000, () => console.log('Backend running on http://localhost:8000'));
安装依赖并启动:
cd backend npm init -y npm install express cors node server.js
五、工程构建与打包
1. 本地开发
npm start # 访问 http://localhost:3000
2. 生产构建
npm run build # 输出到 ./build 目录
3. 部署
- 将
build/目录部署到 Nginx、Vercel、Netlify 等 - 示例 Nginx 配置:
server {
listen 80;
server_name your-domain.com;
root /path/to/build;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
}六、功能演示说明
| 操作 | 效果 |
|---|---|
| 右键空白处 | 弹出菜单:可添加 Input/LLM/Output 节点 |
| 右键选中节点 → Delete | 删除所有选中节点及关联连线 |
| 拖拽节点 | 自由移动 |
| 点击“Export JSON” | 下载 ai-workflow.json |
| 点击“Send to Backend” | POST 到 http://localhost:8000/execute |
JSON 结构示例:
{
"nodes": [
{ "id": "1", "type": "input", "position": { "x": 100, "y": 100 }, "data": {} },
{ "id": "2", "type": "llm", "position": { "x": 300, "y": 100 }, "data": { "model": "qwen:7b" } }
],
"edges": [
{ "id": "e1-2", "source": "1", "target": "2", "sourceHandle": "out", "targetHandle": "in" }
]
}后端可根据此结构动态构建 LangChain 链或调用 MCP 服务。
七、后续扩展建议
- 保存到数据库:将 JSON 存入 PostgreSQL/MongoDB
- 版本管理:支持 flow 版本回溯
- 实时协作:集成 Yjs 实现多人协同编辑
- 执行结果展示:在节点上显示 LLM 输出
- 权限控制:企业级用户/团队管理
总结
你现在已经拥有一个:
- 基于 React Flow 的 AI 工作流构建器
- 支持 右键操作、JSON 导出、后端集成
- 可直接运行、打包、部署
以上就是使用React Flow构建一个AI工作流工具的详细内容,更多关于React Flow构建AI工作流的资料请关注脚本之家其它相关文章!
相关文章
React Native中NavigatorIOS组件的简单使用详解
这篇文章主要介绍了React Native中NavigatorIOS组件的简单使用详解,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧2018-01-01
react hooks d3实现企查查股权穿透图结构图效果详解
这篇文章主要为大家介绍了react hooks d3实现企查查股权穿透图结构图效果详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪2023-01-01
react-router-dom入门使用教程(前端路由原理)
这篇文章主要介绍了react-router-dom入门使用教程,主要包括react路由相关理解,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下2022-08-08


最新评论