我用 Next.js + React Flow 从零搭建了一个可视化 AI 工作流编排平台

低代码画布 + Function Calling + MCP,AI 应用开发从未如此灵活

前言

2026 年,AI 应用开发正经历一场深刻的范式转移。从最初直接调用 API 的"裸调"模式,到如今 "可视化编排 + 标准化协议" 的工程化体系,开发者构建 AI 应用的方式发生了质的变化。

根据行业报告,智能体开发平台正加速向低代码化、多模态融合及智能体工作流(Agentic Workflow) 方向演进。与此同时,MCP(模型上下文协议)最近刚完成史上最大规模更新------2026-07-28 规范发布,转向无状态核心,月 SDK 下载量已突破 4 亿次。

在这样的背景下,我基于 Next.js + React Flow 从零搭建了一个可视化 AI 工作流编排平台,支持拖拽编排、条件分支、Function Calling 智能代理,并内置了 MCP 服务器。这篇文章将完整分享我的技术选型、架构设计和核心实现。


一、为什么要造这个轮子?

1.1 痛点:AI 应用开发的"三座大山"

在日常 AI 应用开发中,我们经常面临三个核心问题:

  • 逻辑固化:每次新增一个工具或调整流程,都要改代码、重新部署,迭代周期长。
  • 调用复杂 :Function Calling 的 tools 数组需要手动维护,工具一多,上下文急剧膨胀。
  • 缺乏可视化:复杂的 AI 流程只能靠代码"脑补",难以向非技术人员展示和协作。

1.2 目标

我希望构建一个低代码 AI 工作流平台,让:

  • 非技术人员(产品、运营)通过拖拽节点即可完成 AI 流程编排;
  • 开发者无需每次加工具都改核心代码,实现"即插即用";
  • AI Agent能自主决策调用哪些工具(ReAct 模式),而不是预设死路径。

二、技术选型

2.1 全栈框架:Next.js 14(App Router)

选择 Next.js 的核心原因是类型安全的全栈一致性

在 Express 架构中,前端和后端的类型定义需要维护两份,一旦 WorkflowNode 的字段发生变化,前后端极易不同步。而 Next.js 允许我在 @types/ 中统一管理类型,前端 page.tsx 和后端 route.ts 共享同一套定义。

此外,Next.js API Routes 对 Web Standard ReadableStream 的原生支持,让流式输出的实现异常简洁------无需配置额外的 SSE 库。

2.2 可视化画布:React Flow(@xyflow/react)

React Flow 是目前最成熟的 React 流程图库,Dify、Flowise 等主流 AI 工作流平台都在使用。

我利用它实现了:

  • 自定义节点:输入节点、LLM 节点、条件分支节点、HTTP 请求节点、数据库查询节点、天气节点、通知节点、智能代理节点等;
  • 动态连线 :支持 sourceHandle 区分不同出口(如条件节点的 true/false);
  • 节点增删:工具栏按钮 + 键盘快捷键(Delete/Backspace)。

2.3 AI 模型接入:OpenAI / DeepSeek 兼容 API

通过环境变量配置 OPENAI_API_KEY / DEEPSEEK_API_KEYLLM_BASE_URLLLM_MODEL,支持任意兼容 OpenAI 格式的模型(GPT-4o、DeepSeek、Claude 等)。

2.4 数据库:MongoDB + Mongoose

用于存储产品数据(Product 模型),支持 productIdtitle 的模糊查询。

2.5 安全条件评估:expr-eval

用户可在条件分支节点中编写 JavaScript 表达式(如 input.includes('错误'))。为了防止 new Function 带来的代码注入风险,我使用 expr-eval 库在沙箱中安全执行。


三、整体架构

text 复制代码
┌─────────────────────────────────────────────────────────────────┐
│                        前端 (React + React Flow)                │
│  ┌──────────┐ ┌──────────┐ ┌──────────────┐ ┌───────────────┐ │
│  │ 输入节点  │ │ LLM节点  │ │ 条件分支节点 │ │ 智能代理节点  │ │
│  └──────────┘ └──────────┘ └──────────────┘ └───────────────┘ │
│  ┌──────────┐ ┌──────────┐ ┌──────────────┐                   │
│  │ 数据库节点│ │ 天气节点 │ │ 通知节点     │                   │
│  └──────────┘ └──────────┘ └──────────────┘                   │
└─────────────────────────────────────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────┐
│                   Next.js API Routes (后端执行引擎)             │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │  POST /api/run-workflow  (工作流执行 + 流式输出)        │  │
│  │  - 解析 nodes/edges JSON                                 │  │
│  │  - 按拓扑顺序执行节点 (input → condition → llm → ...)   │  │
│  │  - 支持条件分支、工具调用、错误处理                      │  │
│  │  - 最终结果以 ReadableStream 流式返回 (打字机效果)       │  │
│  └──────────────────────────────────────────────────────────┘  │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │  POST /api/mcp  (MCP 服务器)                            │  │
│  │  - get_weather: 查询天气                                │  │
│  │  - query_database: 查询 MongoDB 产品数据                │  │
│  │  - send_email: 发送邮件                                │  │
│  └──────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────┐
│                         外部依赖                                │
│  ┌──────────┐  ┌──────────────┐  ┌────────────────────────┐   │
│  │ MongoDB  │  │ OpenWeather  │  │ SMTP / 邮件服务        │   │
│  └──────────┘  └──────────────┘  └────────────────────────┘   │
└─────────────────────────────────────────────────────────────────┘

四、核心实现

4.1 节点系统:从"死节点"到"活组件"

每个自定义节点都是一个独立的 React 组件,通过 nodeTypes 注册到 React Flow:

tsx 复制代码
// app/page.tsx
const nodeTypes: NodeTypes = {
  inputNode: InputNode,
  llmNode: LLMNode,
  conditionNode: ConditionNode,
  toolAgentNode: ToolAgentNode,
  // ...
};

以条件分支节点为例,它有两个输出端口(true / false),通过 Handle 组件的 id 属性区分:

tsx 复制代码
<Handle type="source" position={Position.Right} id="true" />
<Handle type="source" position={Position.Right} id="false" />

用户拖拽连线时,React Flow 会自动记录 sourceHandle,后端据此判断走哪个分支。

4.2 执行引擎:工作流如何跑起来?

核心逻辑在 POST /api/run-workflow 中:

typescript 复制代码
let currentId: string | null = startNode.id;
let previousResult = '';

while (currentId !== null) {
  const currentNode = nodeMap.get(currentId);
  if (!currentNode) break;

  if (currentNode.type === 'inputNode') {
    previousResult = currentNode.data.value || '';
  } else if (currentNode.type === 'llmNode') {
    previousResult = await callLLMNonStream(systemPrompt, previousResult);
  } else if (currentNode.type === 'conditionNode') {
    const result = evaluateCondition(previousResult, conditionExpr);
    const handleId = result ? 'true' : 'false';
    const nextEdge = edges.find(e => e.source === currentId && e.sourceHandle === handleId);
    currentId = nextEdge ? nextEdge.target : null;
    continue;
  }
  // ... 其他节点类型

  // 普通节点:查找下一条边
  const nextEdge = edges.find(e => e.source === currentId && !e.sourceHandle);
  currentId = nextEdge ? nextEdge.target : null;
}

这里的核心设计是:节点只负责"执行"和"产出数据",路由逻辑由 edges 数组和 sourceHandle 共同决定 。这使得节点本身与流程解耦,新增节点类型只需在 while 循环中添加一个 else if 分支。

4.3 流式输出:打字机效果是怎么实现的?

最终结果通过 ReadableStream 逐字符返回:

typescript 复制代码
const stream = new ReadableStream({
  async start(controller) {
    for (const char of text) {
      controller.enqueue(new TextEncoder().encode(char));
      await new Promise(resolve => setTimeout(resolve, 10));
    }
    controller.close();
  },
});

return new Response(stream, {
  headers: { 'Content-Type': 'text/plain; charset=utf-8' },
});

前端使用 response.body.getReader() 逐块读取并更新 UI,实现类似 ChatGPT 的打字机效果。这种方式相比一次性返回,首字节时间(TTFB)降低约 80% ,且服务器内存占用极低。

4.4 Function Calling:让 AI 自己决定用哪个工具

在"智能代理节点(ToolAgentNode)"中,我实现了完整的 ReAct 模式

  1. 构建工具 Schema :根据用户启用的工具列表,动态生成 OpenAI 格式的 tools 数组;
  2. LLM 决策 :将用户问题和工具列表一起发给 LLM,LLM 返回 tool_calls
  3. 执行工具 :根据 tool_calls 中的 namearguments 调用对应的工具函数;
  4. 结果回传 :将工具执行结果以 role: 'tool' 的消息格式再次发给 LLM,生成最终自然语言回答。
typescript 复制代码
// 工具执行示例:天气查询
case 'get_weather': {
  const res = await fetch(
    `https://api.openweathermap.org/data/2.5/weather?q=${args.city}&appid=${apiKey}&units=metric&lang=zh_cn`
  );
  const data = await res.json();
  toolResult = `当前${args.city}天气:${data.weather?.[0]?.description},温度${data.main?.temp}°C`;
  break;
}

4.5 MCP 服务器:把工具变成"标准插头"

MCP(模型上下文协议)最近刚完成 2026-07-28 规范更新,转向无状态核心,已成为连接 AI Agent 与应用程序的行业标准。

我在 app/api/mcp/route.ts 中内置了一个 MCP 服务器,使用 mcp-handler 库将三个工具(天气、数据库、邮件)注册为标准 MCP 工具:

typescript 复制代码
import { createMcpHandler } from "mcp-handler";
import { z } from "zod";

const handler = createMcpHandler((server) => {
  server.registerTool(
    "get_weather",
    {
      title: "获取天气",
      description: "获取指定城市的实时天气信息",
      inputSchema: z.object({
        city: z.string().describe("城市名称,如 '北京'"),
      }),
    },
    async ({ city }) => {
      const result = await getWeather(city);
      return { content: [{ type: "text", text: result }] };
    }
  );
  // ... query_database, send_email
});

配置 Claude Desktop 后,即可在对话中直接调用这些工具:

json 复制代码
{
  "mcpServers": {
    "my-nextjs-tools": {
      "url": "http://localhost:3000/api/mcp"
    }
  }
}

MCP 与 Function Calling 的关系:MCP 是标准化协议,Function Calling 是模型输出格式。两者协同工作------MCP 负责工具发现和调用,Function Calling 负责让 LLM 以结构化格式输出调用指令。

4.6 执行日志:让 Agent 的"思考"可见

为了提升可观测性,我实现了执行日志面板 。后端在执行每个节点和工具调用时,会生成结构化日志并通过 [LOG] 前缀流式发送:

typescript 复制代码
logLines.push({
  type: 'tool_call',
  timestamp: Date.now(),
  tool: 'get_weather',
  args: { city: '北京' },
});

前端解析后展示在右下角日志面板中,支持实时滚动、清空和折叠:

text 复制代码
[14:32:05] 🧠 智能代理开始处理:北京今天天气怎么样?
[14:32:06] 🔧 调用工具: get_weather({"city":"北京"})
[14:32:07] ✅ 工具返回: 当前北京天气:晴,温度25°C
[14:32:08] 💬 最终回答:北京今天天气晴朗,气温25°C...

这让整个 AI 决策过程变得透明、可调试------用户能清楚看到 Agent 每一步在做什么。


五、项目成果与数据

模块 实现内容
节点类型 8 种(输入、LLM、条件分支、HTTP 请求、数据库、天气、通知、智能代理)
执行引擎 支持顺序执行、条件分支、错误处理、流式输出
Function Calling 支持 4 种工具(天气、数据库、邮件、HTTP),Agent 自主决策
MCP 服务器 3 个标准 MCP 工具,可被 Claude Desktop 等客户端调用
日志系统 实时执行日志,支持可视化展示工具调用链路
代码量 ~1500 行 TypeScript(前后端合计)

六、踩坑与经验

6.1 React Flow 的 sourceHandle 陷阱

React Flow 在连线时会自动为每个 Handle 生成 sourceHandle。对于只有一个输出端口的节点,默认是 'output'。如果后端查找边时使用 !e.sourceHandle,会漏掉这些边。我的解决方案是:

typescript 复制代码
const nextEdge = edges.find(e =>
  e.source === currentId &&
  (!e.sourceHandle || e.sourceHandle === 'output')
);

6.2 条件表达式安全执行

new Function 可以执行任意代码,存在安全风险。我改用 expr-eval 库,仅暴露 input 变量:

typescript 复制代码
import { Parser } from 'expr-eval';
const parser = new Parser();
const result = parser.evaluate(conditionExpr, { input });

6.3 流式输出与条件分支的冲突

最初的版本试图让 LLM 流式输出,但条件分支需要拿到完整结果才能判断走向。最终采用 "同步执行 + 最终结果流式返回" 的混合架构------节点间同步执行,最终结果以流式输出。

6.4 MCP 服务器的部署路径

MCP 服务器通过 Next.js API 路由暴露(/api/mcp),与主应用共享同一端口和环境变量,无需额外启动 Python 进程,部署和维护成本大幅降低。


七、未来展望

  1. HTTP 请求节点增强:支持更灵活的认证方式(OAuth、API Key)和响应解析;
  2. 循环节点:支持对数组进行迭代处理;
  3. 子工作流:将一个完整的工作流封装成可复用的子节点;
  4. 版本管理:工作流定义的保存、加载和历史版本回滚;
  5. MCP 工具自动发现:让低代码平台自动连接 MCP 服务器,动态获取工具列表,彻底告别硬编码。

八、总结

这个项目的核心收获是:AI 应用开发正在从"写代码"走向"编流程" 。可视化画布降低了使用门槛,Function Calling 赋予了 AI 自主决策能力,MCP 标准化了工具生态。

当这三者结合时,我们得到的不再是一个"调用 API 的小工具",而是一个可扩展、可观测、可协作的 AI 应用开发平台------让 AI 从"聊天玩具"真正进化成能解决业务问题的"数字员工"。

最后

✅完整示例代码已开源 GitHub:github.com/wxw1314/my-... 如果对你有帮助,欢迎点个 Star,感谢支持!

相关推荐
AI行业说9 分钟前
誉财自动化YC-18-M8045怎么选?2026中小服装工厂模板机选型指南
人工智能·机器人·自动化
Waiky13 分钟前
给 AI Agents 一双「眼睛」:CrawlEyes 开源了
人工智能
skiyee18 分钟前
🚀 用 17 行代码给 UniApp 加上全局登录拦截
前端·uni-app
甲维斯38 分钟前
美版豆包又更新了!Gemini 3.8Flash是真Flash!
人工智能
云智慧AIOps社区39 分钟前
2026年智能巡检机器人行业格局:三类厂商阵营、选型维度与产品推荐
运维·人工智能·机器人·巡检机器人
微三云 - 廖会灵 (私域系统开发)39 分钟前
重资产的突围:当酒店遇上 RWA,用服务权益数字化重构产业供需关系
人工智能·重构
阿里云大数据AI技术43 分钟前
从“找得到”到“找得准”:用 Hologres 构建智能达人圈选系统
大数据·人工智能
默_笙1 小时前
🌃 HTTP 不认识你:JWT 登录鉴权的完整"酒店入住"指南
前端·javascript
2601_967659881 小时前
艾雨文承推出大头阿亮第五代智能养老机器人机构版,助力养老机构智慧巡护升级
人工智能·机器人