从零到一:如何用 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 人全栈。 -
可控性:自研 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 用同一套代码调用?
-
定义接口
public interface NodeExecutor {
Map<String, Object> execute(WorkflowNode node, Map<String, Object> input);
String getSupportedNodeType();
}
-
工厂登记
@Component
public class NodeExecutorFactory implements InitializingBean {
private final Map<String, NodeExecutor> map = new ConcurrentHashMap<>();
@Override public void afterPropertiesSet() {
// Spring 启动时自动扫描所有 NodeExecutor Bean 并注册
}
}
-
适配器实现
-
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. 调试抽屉:把“黑盒执行”变成“白盒追剧”
本段核心问题:用户点击“执行”后,如何让他看到“现在跑到哪一步、每个节点吐出什么”?
-
后端逐节点返回 ExecutionNodeResult列表,字段:nodeId, status, input, output, duration。 -
前端用 Ant Design 的 Timeline组件竖着排,绿色=成功,红色=失败,灰色=等待。 -
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. 实用摘要 / 操作清单(一页可落地)
-
装环境:JDK 21、Node 18、MySQL 8 -
导数据: mysql -u root -p < backend/src/main/resources/schema.sql -
启后端: cd backend && ./mvnw spring-boot:run -
启前端: cd frontend && npm i && npm run dev -
登录: admin/123 -
拖节点:Input → LLM → TTS → Output -
连线:按数据流方向连 -
配参数:LLM 写提示词,TTS 选音色 -
点调试:输入文本 → 执行 → 看时间轴 → 播放音频 -
点保存:下次登录继续编辑
11. 一页速览(One-page Summary)
-
目标:两周交付可生产的 AI 工作流平台 -
手段:自研 DAG 引擎 + ReactFlow 画布 + 轻量 REST -
产出:4500 行代码、11 接口、6 节点、3 表、1 音频播放器 -
验证:AI 播客 30 秒生成→5 秒播放→一键下载 -
关键词:Kahn 拓扑排序、工厂模式、适配器模式、ReactFlow、TTS
12. FAQ(可检索短句)
-
为什么选 Kahn 而不是 DFS 做拓扑?
Kahn 入度表易读、易断点,DFS 递归深挂起难调试。 -
节点如何热插拔?
实现NodeExecutor接口 +@Component,工厂自动注册。 -
音频文件会撑爆磁盘吗?
模拟模式只生成 1 个模板文件;真实模式需加定时清理脚本。 -
可以并行跑节点吗?
当前串行,后续加@Async线程池即可。 -
支持多人协作吗?
目前是单用户版,加租户字段即可扩展。 -
生产环境必须接真实 API 吗?
不是,simulation 模式可一直用,但声音固定。 -
想把 TTS 换成本地模型怎么办?
新建LocalTTSNodeExecutor实现接口,改 node_definition 表即可。
