从零到一:如何用 4500 行代码造出 AI 工作流“乐高”——PaiAgent 实战全纪录

核心问题:如果只有两周、一个人,怎样把“拖拽节点→连线→跑通大模型→拿到语音”整条链路做成可复用的 Web 产品?
答案:先让 DAG 引擎转起来,再让 ReactFlow 画布好看,最后把 TTS 塞进调试抽屉——三步走完,AI 工作流就能“听得见”。


1. 项目速览:95% 完成度的“迷你 Activiti”长什么样?

维度 交付物
语言 Java 21 + TypeScript
代码量 4500 行(后端 3000,前端 1200,配置 200)
核心算法 Kahn 拓扑排序 + DFS 循环检测
节点类型 6 种(输入、输出、OpenAI、DeepSeek、通义千问、TTS)
运行包 一个 Spring Boot jar + 一套 Vite 静态文件
浏览器 登录→拖拽→调试→播放,全程 30 秒

一句话总结:把“大模型 + 语音”塞进乐高式节点,让不会写代码的人也能拼出 AI 流水线。


2. 为什么要自研 DAG 引擎?——“能跑”比“完美”更重要

本段核心问题:市面上有 Activiti、Camunda,为什么还要手写拓扑排序?

  1. 需求极简:只需有向无环图 + 节点按序执行,不需要子流程、事件监听器、历史追溯。
  2. 资源受限:两周交付,团队只有 1 人全栈。
  3. 可控性:自研 200 行 Kahn 算法,单步调试可见,出问题直接断点。

场景示例
“AI 播客生成”工作流:输入节点 → OpenAI 节点 → TTS 节点 → 输出节点。
引擎先算拓扑序:Input → OpenAI → TTS → Output;再依次把上游输出塞进下游 inputMap;最后把 TTS 生成的 mp3 路径回写给 Output。
整个流程 3 秒跑完,内存占用 <30 MB。

作者反思
第一次尝试把 Activiti 塞进项目,结果仅依赖就 80 MB,启动 20 秒。砍掉多余功能后,发现拓扑排序 + 工厂模式就能解决 95% 问题。结论:先让业务跑通,再谈“企业级”。


3. 数据库只留 3 张表——“能存”与“能查”的平衡点

职责 核心字段
workflow 保存画布 JSON id, name, flow_data, created_at
node_definition 节点元数据 node_type, display_name, category, icon, input_schema, output_schema, config_schema
execution_record 每次运行快照 id, flow_id, input_data, output_data, status, node_results, duration

设计要点

  • flow_data、node_results 直接存 JSON,减少联表,让前端一次性读写完整体状态。
  • 预置 6 条 node_definition 数据,服务启动即自带节点库,无需后台手工录入。

场景示例
用户保存“AI 播客”工作流 → 前端把 nodes + edges 序列化成 JSON → 后端整段写入 flow_data;下次加载→一次性读出→ReactFlow 直接渲染。零拼接 SQL,零 ORM 映射


4. 节点执行器 = 接口 + 工厂 + 适配器——让“大模型”像插件一样热插拔

本段核心问题:如何让 OpenAI、DeepSeek、通义千问三家 API 用同一套代码调用?

  1. 定义接口
public interface NodeExecutor {
    Map<String, Object> execute(WorkflowNode node, Map<String, Object> input);
    String getSupportedNodeType();
}
  1. 工厂登记
@Component
public class NodeExecutorFactory implements InitializingBean {
    private final Map<String, NodeExecutor> map = new ConcurrentHashMap<>();
    @Override public void afterPropertiesSet() {
        // Spring 启动时自动扫描所有 NodeExecutor Bean 并注册
    }
}
  1. 适配器实现
  • OpenAINodeExecutor:把 prompt + input 拼成 messages,返回 generated text。
  • TTSNodeExecutor:把文本喂给模拟接口,返回 /audio/uuid.mp3 的 URL。
    新增节点只需再写一个类并打上 @Component零修改旧代码

场景示例
用户把“通义千问”节点拖到画布→配置 prompt“把{{input}}翻译成英文”→运行。
引擎根据 nodeType=qwen 从工厂拿到 QwenNodeExecutor→注入阿里云 SDK→返回译文→下游 TTS 节点继续消费。
切换大模型就像换一块乐高板


5. ReactFlow 画布三步曲:拖→连→配,让编辑体验像 Figma 一样顺滑

本段核心问题:怎样在 140 行 TSX 里实现“节点面板 + 画布 + 配置栏”三栏交互?

步骤 关键点 代码片段
节点面板 onDragStart 把 nodeType 写进 dataTransfer event.dataTransfer.setData('nodeType', 'openai')
FlowCanvas onDrop 读 nodeType,实例化 Node const newNode = { id: 'openai-' + Date.now(), position, data: { label, type } }
点击节点 → 右侧面板动态渲染 schema 表单 <Form items={selectedNode.configSchema} />

状态同步策略

  • 全局只有一个 workflowStore.nodes 数组。
  • ReactFlow 的 onNodesChange 事件实时写回 Zustand,保证画布与 Store 永远双向绑定
  • 保存时把整个 nodes + edges 序列化,一次性 POST /api/workflows

场景示例
产品经理想把“温度”从 0.7 调到 0.9:点击 OpenAI 节点→右侧出现 Slider→拖动→Store 更新→画布节点 data 实时刷新→点击保存→后端落库。全程无刷新,像改 PPT 一样改 AI 流程


6. 调试抽屉:把“黑盒执行”变成“白盒追剧”

本段核心问题:用户点击“执行”后,如何让他看到“现在跑到哪一步、每个节点吐出什么”?

  1. 后端逐节点返回 ExecutionNodeResult 列表,字段:nodeId, status, input, output, duration
  2. 前端用 Ant Design 的 Timeline 组件竖着排,绿色=成功,红色=失败,灰色=等待。
  3. TTS 节点若成功,额外把 output.audioUrl 塞进 <AudioPlayer />,可在线播、可下载。

场景示例
运行“AI 播客”工作流→抽屉自动弹出→时间轴顺序亮起:
① Input 节点 0.01 s → ② OpenAI 节点 1.2 s → ③ TTS 节点 2.1 s → ④ Output 节点 0.01 s。
每一步的 JSON 都折叠展示,点开后能看到 OpenAI 吐出的 300 字脚本、TTS 返回的 mp3 地址。
用户不再面对“转圈→突然成功”的空白体验,而是像追剧一样看“AI 花絮”。

作者反思
最早版本只给“成功/失败”二值结果,被测试同事吐槽“像 404 页面”。加上时间轴后,调试效率提升 3 倍——原来卡在 OpenAI 限速、还是 TTS 文件写入失败,一目了然。


7. TTS 节点:从“哑巴流水线”到“开口说话”的最后一公里

本段核心问题:怎样让生成的文本“秒变”可播放音频,又不被第三方 TTS 服务拖垮进度?

  • 提供 simulation 模式:随机选一段内置 mp3,把文件名拼成 /audio/uuid.mp3 返回,开发期零等待
  • 预留真实 SDK 插槽:Azure、阿里云接口已封装,只需在 application.yml 填 Key 即可切换。
  • 静态资源映射:Spring Boot 加一行 addResourceHandler("/audio/**").addResourceLocations("file:audio_output/")浏览器直链可播

场景示例
用户跑完“AI 播客”→调试抽屉出现播放器→点击播放→女声朗读“人工智能的未来发展……”→右键下载→拿到 60 秒 mp3,可直接上传小红书。
整条链路 5 秒完成,用户感知不到后端是否真调了 Azure。


8. 11 个 REST 接口就让前后端“聊得欢”——少即是多

模块 接口数 典型端点 职责
认证 3 POST /api/auth/login 登录、登出、当前用户
工作流 5 GET /api/workflows CRUD + 执行
节点类型 1 GET /api/node-types 拉取节点库
执行 1 POST /api/workflows/{id}/execute 触发运行

设计原则

  • 统一返回 Result<T> 包装体,字段:code, msg, data,前端 Axios 拦截器直接拆箱。
  • 路径语义化,全部名词,动词用 HTTP Method 表达,降低沟通成本
  • 执行接口异步但立即返回 executionId,前端轮询 /api/executions/{id} 拿实时日志——避免长连接,减少运维负担

9. 部署踩坑实录:从“能跑”到“敢上线”还差几步?

本段核心问题:代码写完就能睡大觉?本地神童,线上“神童”吗?

坑点 症状 解法
CORS 前端 5173 调后端 8080 被浏览器拦截 后端加 addCorsMappings 允许 localhost:5173
静态音频 404 生产环境找不到 /audio/uuid.mp3 file: 绝对路径 + 容器外挂卷
大模型超时 OpenAI 3 秒无响应 → 前端 Axios 超时 把 Axios timeout 提到 60 s,并给节点加“模拟”开关

作者反思
第一次打包把 audio_output/ 忘写 Dockerfile,结果用户播放全部 404。结论:“能跑”≠“能上线”, checklist 里一定加一条“静态资源路径”。


10. 实用摘要 / 操作清单(一页可落地)

  1. 装环境:JDK 21、Node 18、MySQL 8
  2. 导数据:mysql -u root -p < backend/src/main/resources/schema.sql
  3. 启后端:cd backend && ./mvnw spring-boot:run
  4. 启前端:cd frontend && npm i && npm run dev
  5. 登录:admin/123
  6. 拖节点:Input → LLM → TTS → Output
  7. 连线:按数据流方向连
  8. 配参数:LLM 写提示词,TTS 选音色
  9. 点调试:输入文本 → 执行 → 看时间轴 → 播放音频
  10. 点保存:下次登录继续编辑

11. 一页速览(One-page Summary)

  • 目标:两周交付可生产的 AI 工作流平台
  • 手段:自研 DAG 引擎 + ReactFlow 画布 + 轻量 REST
  • 产出:4500 行代码、11 接口、6 节点、3 表、1 音频播放器
  • 验证:AI 播客 30 秒生成→5 秒播放→一键下载
  • 关键词:Kahn 拓扑排序、工厂模式、适配器模式、ReactFlow、TTS

12. FAQ(可检索短句)

  1. 为什么选 Kahn 而不是 DFS 做拓扑?
    Kahn 入度表易读、易断点,DFS 递归深挂起难调试。

  2. 节点如何热插拔?
    实现 NodeExecutor 接口 + @Component,工厂自动注册。

  3. 音频文件会撑爆磁盘吗?
    模拟模式只生成 1 个模板文件;真实模式需加定时清理脚本。

  4. 可以并行跑节点吗?
    当前串行,后续加 @Async 线程池即可。

  5. 支持多人协作吗?
    目前是单用户版,加租户字段即可扩展。

  6. 生产环境必须接真实 API 吗?
    不是,simulation 模式可一直用,但声音固定。

  7. 想把 TTS 换成本地模型怎么办?
    新建 LocalTTSNodeExecutor 实现接口,改 node_definition 表即可。