给自媒体团队写了个画布式 AI 工作流,500 任务/分钟,pgvector 真香

0 阅读8分钟

给自媒体团队写了个画布式 AI 工作流,500 任务/分钟,pgvector 真香

朋友前两天找我吐槽:他做口播号,电脑里装了 ChatGPT、Midjourney、Kling、HeyGen、飞瓜五个工具,每条视频要在工具间复制粘贴七八次,一天出三条已经是体力极限

我说我们刚好做了一个东西,把这五件事塞进同一块画布。配置一次、批量跑、结果回流,4 核 8G 服务器就能跑。他试了两周,给我发了一条消息:"一天 12 条,停不下来。"

这篇文章把我们这套东西的架构、踩过的坑、关键性能数据完整地讲一遍。


TL;DR(给赶时间的同学)

  • 项目叫 三千AI智能体,是一个面向自媒体/MCN 的私有化 AI 工作台
  • 技术栈:NestJS + Nuxt 4 + PostgreSQL 17.6(pgvector + zhparser)+ Redis 8.2.2 BullMQ + Docker Compose
  • 核心:画布式工作流引擎,20 个节点串起脚本 → 配图 → 配音 → 数字人合成
  • 单机 4C8G 实测:200 流式连接 / 500 任务每分钟队列吞吐 / 1000–2000 日活承载
  • 项目地址:github.com/2961799660/…,演示:ai1.zijie.lol

一、为什么不用现成的 LangGraph / Dify / n8n?

这是被问得最多的问题,先回答:

工具为什么没直接用
Dify偏对话/RAG 编排,画布交互弱,二开成本高
n8n通用自动化,对图片/视频长任务调度不友好
LangGraph偏后端 DAG,没有给运营人员看的可视化画布
Coze不能私有化,企业客户卡死在这一条

我们的核心需求是:运营/剪辑能直接拖拽长任务(5min+ 视频生成)异步可恢复完全私有化。三条画死了,于是自己写。


二、整体架构图

flowchart LR
    User((用户)) -->|HTTPS| Nginx
    Nginx -->|3000| Nuxt[Nuxt 4 SSR/SPA<br/>画布前端]
    Nginx -->|4090| API[NestJS API<br/>BullMQ 生产者]
    API --> PG[(PostgreSQL 17.6<br/>pgvector + zhparser)]
    API --> Redis[(Redis 8.2.2)]
    Redis --> Worker[BullMQ Worker<br/>长任务消费]
    Worker -->|HTTP| Models[模型 API 矩阵<br/>GPT/Claude/Kling/Seedream...]
    Worker --> PG
    Worker -->|WebSocket| Nuxt

一台 4C8G 全跑得动,PostgreSQL 同时承担业务库 / 向量库 / 中文全文检索三件事,省掉 Milvus + ES 两个组件,资源占用减少 60%


三、画布工作流引擎:核心抽象只有三层

第一版我们差点掉进过度设计的坑,写了 8 层抽象,后来推倒重来。最终的核心抽象只有三层:

// 1. 节点定义(运行时拿到的执行单元)
interface CanvasNode {
  id: string
  type: NodeType            // 'image_gen' | 'lip_sync' | ...
  config: Record<string, any>
  status: NodeStatus        // 'pending' | 'queued' | 'running' | 'succeeded' | 'failed' | 'canceled'
  inputs: NodePort[]
  outputs: NodePort[]
}


// 2. 边(DAG 依赖关系)
interface CanvasEdge {
  from: { nodeId: string; portId: string }
  to:   { nodeId: string; portId: string }
}


// 3. 视图层(坐标/样式,跟业务彻底解耦)
interface CanvasView {
  nodeId: string
  x: number; y: number
  width: number; height: number
  collapsed: boolean
}

血泪教训 1视图层一定要跟业务分开存。我们最早把 x/y 坐标塞在 config 里,结果有一次产品要做"自动布局",改一个字段全表迁移,加班加到吐。

血泪教训 2节点状态机要写成 6 态枚举,不要用 running: boolean。后来 BullMQ 重试、用户取消、Worker OOM 三种场景一上来,布尔值彻底不够用。


四、20 个内置节点是怎么组织的?

┌────────────────────────────────────────────┐
│ 🎨 图像 (9)                                │
│ 图像生成/局部重绘/画面扩展/超分/白底图    │
│ 自动蒙版/局部替换/写实化/图像融合         │
├────────────────────────────────────────────┤
│ 🎬 视频 (6)                                │
│ 文生视频/视频剪辑/视频合成                │
│ 分镜串联/首尾帧/去水印                    │
├────────────────────────────────────────────┤
│ 👤 数字人 (2)                              │
│ 口型同步/虚拟数字人生成                   │
├────────────────────────────────────────────┤
│ 🔧 其他 (3)                                │
│ 音频处理/文本处理/音频处理                │
└────────────────────────────────────────────┘

每个节点抽象成一个 BullMQ Job:

// nodes/image-gen.processor.ts
@Processor('image-gen')
export class ImageGenProcessor {
  @Process()
  async handle(job: Job<ImageGenPayload>) {
    const { prompt, model, size } = job.data
    
    // 1. 调模型
    const result = await this.modelHub.invoke(model, { prompt, size })
    
    // 2. 写对象存储
    const url = await this.oss.upload(result.buffer)
    
    // 3. 写数据库 + 触发下游
    await this.nodeService.markSucceeded(job.data.nodeId, { url })
    await this.eventBus.emit('node.completed', { nodeId: job.data.nodeId })
  }
}

关键设计:节点之间不直接调用对方,全部通过 node.completed 事件触发,DAG 引擎拿到事件后判断下游是否所有依赖都满足,再 enqueue。这样任意中间节点重试都不会污染上下游


五、ZIP 模式 vs CROSS 模式

这是被运营同学疯狂表扬的两个模式:

ZIP 模式(一对一批量)

prompts: [A, B, C]
sizes:   [1024, 768, 512]
=>  3 个任务: (A, 1024), (B, 768), (C, 512)

CROSS 模式(参数交叉)

prompts: [A, B, C]
sizes:   [1024, 768]
=>  6 个任务: (A,1024)(A,768)(B,1024)(B,768)(C,1024)(C,768)

实现关键:参数级别的笛卡尔积要在调度器里做,不能让前端拼,否则 6×6×6 = 216 个任务前端连发会被 Nginx 限流。


六、为什么选 PostgreSQL 一把梭?

很多人第一反应是 "向量库就该用 Milvus / Qdrant"。我们一开始也这么想的,跑了两周以后果断切回 pgvector,原因:

维度pgvector独立向量库
事务一致性业务表和向量表同库事务双写一致性自己维护
运维成本1 个服务2 个服务
中文搜索配 zhparser 直接用还要再上 ES
百万级查询性能50–80ms(HNSW 索引)30–50ms
小团队选哪个✅ pgvector

向量量级到亿级再考虑独立向量库。我们当前生产环境最大单库 800w 向量,pgvector + HNSW 索引 P95 < 100ms,完全够用。


七、BullMQ 长任务调度的 5 个坑

视频生成动辄 3–5 分钟,BullMQ 的默认配置一个都不能用,全踩过:

  1. stalledInterval 默认 30s,长任务直接被判 stalled

    • 改成 stalledInterval: 0,禁用 stalled 检测,自己心跳
  2. Job 超时默认 30s

    • 长任务必须显式 timeout: 600_000(10min)
  3. Redis maxmemory-policy 不能用 allkeys-lru

    • 任务会被驱逐,必须用 noeviction
  4. Worker 并发数别贪心

    • 每个 Worker concurrency: 2,超过容易 OOM
  5. 失败重试要带 backoff

    • attempts: 3, backoff: { type: 'exponential', delay: 5000 }

八、多模型聚合层怎么写最省事

不是 if model === 'gpt' else if ...,那种写法到第 5 个模型就崩溃。我们用的是 Provider Strategy

interface ModelProvider {
  invoke(payload: InvokePayload): Promise<ModelResult>
  stream(payload: InvokePayload): AsyncIterable<ModelChunk>
  costPerCall(payload: InvokePayload): number
}


@Injectable()
export class ModelHub {
  constructor(private providers: Map<string, ModelProvider>) {}
  
  async invoke(modelName: string, payload: InvokePayload) {
    const provider = this.providers.get(modelName)
    if (!provider) throw new ModelNotFoundError(modelName)
    
    // 自动失败回退
    try {
      return await provider.invoke(payload)
    } catch (err) {
      const fallback = this.getFallback(modelName)
      if (fallback) return fallback.invoke(payload)
      throw err
    }
  }
}

当前接入:

类型已接入模型
💬 LLMGPT、Claude、Gemini、Kimi、深度求索、智谱 GLM、通义千问、文心一言、xAI Grok
🖼️ 文生图闪绘、精绘、可灵 omni-image、豆包 Seedream 5.0
🎥 文生视频创影、Kling V3、Kling 动作控制、豆包 Seedance 2.0 Pro / 2.0 / 1.x Lite

真香点:管理后台可以按算力消耗配置 —— 1 次 GPT-4 = 5 算力、1 次豆包 = 1 算力,给 SaaS 玩法留了口子。


九、MCP 协议接入:把外部工具搬进对话框

MCP(Model Context Protocol)算是最近半年开发者圈最值得跟的协议。我们已经接入:

  • 飞书文档
  • Notion
  • EXA 搜索
  • 抖音 / 小红书数据
  • 视频文案提取

接入第三方 MCP Server 的代码不到 30 行:

const mcp = new McpClient({ endpoint: 'https://your-mcp.example.com' })
await mcp.connect()
const tools = await mcp.listTools()  // 自动注册到对话工具池

十、性能数据(生产实测)

指标数值说明
单机并发对话200+ 流式SSE 长连接
画布节点队列吞吐500 任务/分钟短任务(文本类)
数字人合成60s 视频 ≈ 3–5 分钟取决于模型方
pgvector P95< 100ms800w 向量 + HNSW
单机日活承载1000–20004C8G 配置
部署上线10 分钟docker compose up -d

十一、最小化部署清单

# 服务器:4C / 8G / 40G / Linux 或 macOS
git clone <release-bundle>
cp .env.example .env
# 填好域名、数据库密码、模型 Key
docker compose up -d


# 进容器启用 pgvector / zhparser
docker exec -it sanqian-pg psql -U postgres -d sanqian
> CREATE EXTENSION IF NOT EXISTS vector;
> CREATE EXTENSION IF NOT EXISTS zhparser;
> CREATE TEXT SEARCH CONFIGURATION chinese (PARSER = zhparser);

Nginx 关键配置(别忘了开 SSE / WebSocket)

location /api/ {
    proxy_pass http://127.0.0.1:4090;
    proxy_http_version 1.1;
    proxy_set_header Connection "";
    proxy_buffering off;          # SSE 必须
    proxy_read_timeout 600s;      # 长任务必须
}


location /ws {
    proxy_pass http://127.0.0.1:4090;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_read_timeout 600s;
}

十二、写在最后

这套东西现在用法大致三类人:

  1. 自媒体团队 —— 一天稳定出 10+ 条
  2. MCN 机构 —— 多账号矩阵 + IP 档案 + 数据中台
  3. AI SaaS 创业者 —— 买源码改 LOGO 自己卖

最后放链接,欢迎来拍砖:

如果你也在做画布式工作流 / 长任务调度 / 多模型聚合相关的事情,评论区聊,我们也想看看别人的方案有没有更优雅的写法。

喜欢这种"我做了个 X"实战拆解的,点赞 + 收藏让算法看见,下一篇我打算写画布前端怎么用 Vue Flow 撑起 200+ 节点不卡,缺关注。