使用React?Flow構(gòu)建一個(gè)AI工作流工具
一、React Flow 核心概念(AI 工作流視角)
| 概念 | 說明 | AI 工作流中的意義 |
|---|---|---|
| Node(節(jié)點(diǎn)) | 圖中的基本單元,代表一個(gè)操作 | 如:User Input、LLM Call、RAG Retriever、Code Executor |
| Edge(邊) | 連接兩個(gè)節(jié)點(diǎn)的有向線 | 表示數(shù)據(jù)流向(如 prompt → LLM → output) |
| Handle(句柄) | 節(jié)點(diǎn)上的連接點(diǎn)(source/target) | 控制哪些端口可連入/連出(如 LLM 節(jié)點(diǎn)只有 1 個(gè)輸入、1 個(gè)輸出) |
| ReactFlow Provider | 提供全局狀態(tài)和方法 | 管理 nodes/edges、縮放、選擇等 |
| Custom Node | 自定義節(jié)點(diǎn)組件 | 可嵌入表單、下拉菜單、實(shí)時(shí)預(yù)覽等 |
關(guān)鍵思想:每個(gè)節(jié)點(diǎn) = 一個(gè) AI 技能(Skill/Tool),整個(gè)圖 = 一個(gè)可執(zhí)行的 Agent 工作流。
二、目標(biāo)功能清單
我們要構(gòu)建的 AI 工作流工具需支持:
? 可視化拖拽節(jié)點(diǎn)
? 右鍵菜單:添加新節(jié)點(diǎn) / 刪除選中節(jié)點(diǎn)
? 節(jié)點(diǎn)類型:Input、LLM、Output、RAG
? 導(dǎo)出為 JSON(用于保存或分享)
? “發(fā)送到后端”按鈕:將 flow 發(fā)送給 API 執(zhí)行
? 響應(yīng)式布局 + 性能優(yōu)化
三、開發(fā)環(huán)境準(zhǔn)備
1. 前提
- Node.js ≥ 18
- npm / yarn / pnpm
2. 創(chuàng)建項(xiàng)目
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:生成唯一節(jié)點(diǎn) ID
- axios:用于調(diào)用后端 API
4. 清理默認(rèn)文件
刪除 src/App.css、src/logo.svg,保留 src/App.js 和 src/index.js
四、完整可執(zhí)行代碼實(shí)現(xiàn)
文件結(jié)構(gòu)
src/ ├── components/ │ ├── nodes/ │ │ ├── InputNode.jsx │ │ ├── LLMNode.jsx │ │ └── OutputNode.jsx │ └── RightClickMenu.jsx ├── App.js └── index.js
Step 1:自定義節(jié)點(diǎn)組件(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:主應(yīng)用邏輯(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';
// 導(dǎo)入自定義節(jié)點(diǎn)
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]);
// 添加節(jié)點(diǎn)
const addNode = useCallback((type, x, y) => {
if (type === 'delete') {
// 刪除選中節(jié)點(diǎn)
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]);
// 導(dǎo)出 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);
};
// 發(fā)送到后端執(zhí)行
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:簡(jiǎn)單后端模擬(可選,用于測(cè)試)
創(chuàng)建 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);
// 模擬執(zhí)行:返回節(jié)點(diǎn)數(shù)量
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'));
安裝依賴并啟動(dòng):
cd backend npm init -y npm install express cors node server.js
五、工程構(gòu)建與打包
1. 本地開發(fā)
npm start # 訪問 http://localhost:3000
2. 生產(chǎn)構(gòu)建
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 節(jié)點(diǎn) |
| 右鍵選中節(jié)點(diǎn) → Delete | 刪除所有選中節(jié)點(diǎn)及關(guān)聯(lián)連線 |
| 拖拽節(jié)點(diǎn) | 自由移動(dòng) |
| 點(diǎn)擊“Export JSON” | 下載 ai-workflow.json |
| 點(diǎn)擊“Send to Backend” | POST 到 http://localhost:8000/execute |
JSON 結(jié)構(gòu)示例:
{
"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" }
]
}后端可根據(jù)此結(jié)構(gòu)動(dòng)態(tài)構(gòu)建 LangChain 鏈或調(diào)用 MCP 服務(wù)。
七、后續(xù)擴(kuò)展建議
- 保存到數(shù)據(jù)庫:將 JSON 存入 PostgreSQL/MongoDB
- 版本管理:支持 flow 版本回溯
- 實(shí)時(shí)協(xié)作:集成 Yjs 實(shí)現(xiàn)多人協(xié)同編輯
- 執(zhí)行結(jié)果展示:在節(jié)點(diǎn)上顯示 LLM 輸出
- 權(quán)限控制:企業(yè)級(jí)用戶/團(tuán)隊(duì)管理
總結(jié)
你現(xiàn)在已經(jīng)擁有一個(gè):
- 基于 React Flow 的 AI 工作流構(gòu)建器
- 支持 右鍵操作、JSON 導(dǎo)出、后端集成
- 可直接運(yùn)行、打包、部署
以上就是使用React Flow構(gòu)建一個(gè)AI工作流工具的詳細(xì)內(nèi)容,更多關(guān)于React Flow構(gòu)建AI工作流的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
React特征Form?單向數(shù)據(jù)流示例詳解
這篇文章主要為大家介紹了React特征Form?單向數(shù)據(jù)流示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09
ahooks封裝cookie?localStorage?sessionStorage方法
這篇文章主要為大家介紹了ahooks封裝cookie?localStorage?sessionStorage的方法示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07
react+redux的升級(jí)版todoList的實(shí)現(xiàn)
本篇文章主要介紹了react+redux的升級(jí)版todoList的實(shí)現(xiàn),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-12-12
React視頻播放控制組件Video Controls的實(shí)現(xiàn)
本文主要介紹了React視頻播放控制組件Video Controls的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2025-02-02
react如何實(shí)現(xiàn)側(cè)邊欄聯(lián)動(dòng)頭部導(dǎo)航欄效果
這篇文章主要介紹了react如何實(shí)現(xiàn)側(cè)邊欄聯(lián)動(dòng)頭部導(dǎo)航欄效果,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-03-03

