使用React Flow构建一个AI工作流工具

 更新时间:2026年03月30日 08:32:26   作者:问道飞鱼  
本文介绍了基于ReactFlow构建AI工作流工具的方法,涵盖核心概念、目标功能、开发环境准备、代码实现、工程构建与部署等内容,该工具支持可视化拖拽、右键菜单、节点类型、JSON导出及后端执行等功能,需要的朋友可以参考下

一、React Flow 核心概念(AI 工作流视角)

概念说明AI 工作流中的意义
Node(节点)图中的基本单元,代表一个操作如:User InputLLM CallRAG RetrieverCode 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.csssrc/logo.svg,保留 src/App.jssrc/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 服务。

七、后续扩展建议

  1. 保存到数据库:将 JSON 存入 PostgreSQL/MongoDB
  2. 版本管理:支持 flow 版本回溯
  3. 实时协作:集成 Yjs 实现多人协同编辑
  4. 执行结果展示:在节点上显示 LLM 输出
  5. 权限控制:企业级用户/团队管理

总结

你现在已经拥有一个:

  • 基于 React Flow 的 AI 工作流构建器
  • 支持 右键操作、JSON 导出、后端集成
  • 可直接运行、打包、部署

以上就是使用React Flow构建一个AI工作流工具的详细内容,更多关于React Flow构建AI工作流的资料请关注脚本之家其它相关文章!

相关文章

  • react实现Modal弹窗效果

    react实现Modal弹窗效果

    这篇文章主要为大家详细介绍了react实现Modal弹窗效果,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-08-08
  • React Native中NavigatorIOS组件的简单使用详解

    React Native中NavigatorIOS组件的简单使用详解

    这篇文章主要介绍了React Native中NavigatorIOS组件的简单使用详解,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-01-01
  • 详解React 在服务端渲染的实现

    详解React 在服务端渲染的实现

    这篇文章主要介绍了详解React 在服务端渲染的实现,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-11-11
  • react-pdf实现将pdf文件转为图片,用于页面展示

    react-pdf实现将pdf文件转为图片,用于页面展示

    这篇文章主要介绍了react-pdf实现将pdf文件转为图片,用于页面展示问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-07-07
  • react hooks d3实现企查查股权穿透图结构图效果详解

    react hooks d3实现企查查股权穿透图结构图效果详解

    这篇文章主要为大家介绍了react hooks d3实现企查查股权穿透图结构图效果详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-01-01
  • React+Ts实现二次封装组件

    React+Ts实现二次封装组件

    本文主要介绍了React+Ts实现二次封装组件,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-04-04
  • Mobx实现React 应用的状态管理详解

    Mobx实现React 应用的状态管理详解

    这篇文章主要为大家介绍了Mobx 实现 React 应用的状态管理,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-12-12
  • ChatGLM 集成LangChain工具详解

    ChatGLM 集成LangChain工具详解

    这篇文章主要为大家介绍了Svelte和React框架使用比较,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-04-04
  • react-router-dom入门使用教程(前端路由原理)

    react-router-dom入门使用教程(前端路由原理)

    这篇文章主要介绍了react-router-dom入门使用教程,主要包括react路由相关理解,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-08-08
  • React引入css的三种方式小结

    React引入css的三种方式小结

    这篇文章主要介绍了React引入css的三种方式小结,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-12-12

最新评论