数据截至 (上游 commit e741923f72c3)
Sim — 架构与原理
30 秒导读: Sim 是 一个开源的 AI agent 工作空间。你在画布上拖块、连线,搭出一条「收到消息 → 让模型判断 → 调工具 → 回复」的 agent 工作流;也可以直接在聊天里用一句话让 AI 替你把这条流搭出来。本页讲全景:这些块最后是怎么变成一次真实执行的。 答案是一张带「哨兵节点」的 DAG,和一个用就绪队列 + 边激活/失活推进它的调度器。
路径约定: 本组文档所有源码路径都写成克隆根相对的完整路径(例
apps/sim/executor/execution/engine.ts:181)。行号锚定 frontmatter 里的sourceCommit。
1. 这是什么(零基础也能懂)
一句话定义: Sim 是一个自托管友好的 AI agent 工作空间——用可视化画布(或自然语言)编排「模型 + 工具 + 分支 + 循环」,再把它当成一个能被 HTTP、定时器、webhook 触发的服务跑起来。
给谁用、解决什么问题。 你想让 AI 干一件跨系统的活——比如「每天早上读 Gmail 里的新工单,让模型分类,重要的发到 Slack 并在 Notion 建一条记录」。纯写代码要处理鉴权、重试、日志、并发、断点续跑;纯 SaaS 自动化工具又不懂 LLM 那套(工具调用、流式、上下文)。Sim 想同时给你两样:编排的可视化和LLM 原生的执行语义。
它由几件东西组成:
| 部分 | 干什么 | 位置 |
|---|---|---|
| 画布(Workflows) | 拖块连 线,块的位置与参数存成 Postgres 行 | apps/sim/app/workspace/、apps/sim/stores/ |
| 执行引擎 | 把画布编译成 DAG、调度执行、产出日志 | apps/sim/executor/ |
| 块注册表 | 302 个块类型(Slack / Gmail / Agent / Loop…) | apps/sim/blocks/registry-maps.ts:370 BLOCK_REGISTRY |
| 工具层 | 块背后真正发 HTTP 的那一层,3774 条工具定义 | apps/sim/tools/registry.ts:5542 tools |
| 模型 Provider | 21 家 LLM 供应商的统一适配 | apps/sim/providers/registry.ts:31 providerRegistry |
| Copilot | 用自然语言让 AI 直接改画布、跑工作流 | apps/sim/lib/copilot/ |
| 实时协作服务 | 独立的 Bun + Socket.IO 服务,多人同画布 | apps/realtime/ |
用起来什么样。 最小闭环只有三步:npx simstudio 起服务 → 浏览器里拖出「Start → Agent → Slack」三个块连起来 → 点 Run。点下去之后发生的事,就是本组文档要讲清楚的东西。
一句话直觉: 把画布当成电路图,把执行引擎当成跑这张电路图的仿真器——块是元件,线是导线。这个仿真器最有意思的地方在于:导线可以被「断电」,而断电会沿着下游一路传播。分支就是这么实现的(§5.2)。
仓库长什么样。 这是一个 bun + turbo 的 monorepo,根 package.json:7-10 声明 workspaces: ["apps/*", "packages/*"],共 4 个 app 目录 + 16 个 package 目录:
| 目录 | 包名 | 干什么 |
|---|---|---|
apps/sim | sim | 主应用:Next.js 页面 + API 路由 + 画布 + 执行引擎 |
apps/realtime | @sim/realtime | Bun + Socket.IO 服务,多人协作编辑画布 |
apps/docs | docs | 文档站 |
apps/pii | @sim/pii | Python/FastAPI 的 PII 识别脱敏服务,独立容器镜像 |
packages/db | @sim/db | Drizzle schema + 客户端,94 张表 |
packages/workflow-types | @sim/workflow-types | 纯类型:BlockState / Loop / Parallel |
| 其余 13 个 | @sim/* | auth、logger、utils、audit、security、platform-authz、workflow-persistence、realtime-protocol、runtime-secrets、testing、tsconfig、CLI(simstudio)、TS SDK |
packages/python-sdk是 Python 包,没有package.json,不参与 JS 工作区构建。
一条硬边界: apps/realtime 刻意不依赖 Next.js、React、块/工具注册表和执行引擎——它只管协作光标和画布同步,不管跑工作流。这条边界由 CI 脚本 scripts/check-monorepo-boundaries.ts 强制。
2. 顶层全景
2.1 一次执行的数据形态变化
怎么读这张图: 从上到下是同一份工作流依次换的六种表示;每一站左边是产物,箭头旁写的是干这件事的函数。
画布 · React Flow 你拖的块和线
│ 每次编辑发一条 socket op,一块一行落库
▼
① 行存储 workflow_blocks / workflow_edges / workflow_subflows
│ Serializer.serializeWorkflow()
▼
② 序列化工作流 SerializedWorkflow(纯 JSON:blocks / connections / loops / parallels)
│ DAGBuilder.build()
▼
③ 执行图 DAG 每个节点带 incomingEdges / outgoingEdges;循环与并行摊平成哨兵节点
│ ExecutionEngine.run()
▼
④ 调度 就绪队列 readyQueue,入边被消耗光的节点才可以跑
│ BlockExecutor → 16 个 handler 之一
▼
⑤ 执行一个块 NormalizedBlockOutput(Agent / API / Function / Condition / Wait…)
│ EdgeManager.processOutgoingEdges()
▼
⑥ 算出下一批就绪节点 ───▶ 回到 ④;直到无事可做,或撞上一个「暂停」
调度内核长这样(全篇最该记住的一张图):
┌────────────────────────────────────────────────┐
│ 就绪队列 readyQueue │
│ 入边都被消耗光的节点排在这里 │
└────────────────────────┴───────────────────────┘
│ 出队:队列里有几个就同时起几个
▼
┌────────────────────────┬───────────────────────┐
│ 并发执行中的块 executing │
│ 不设并发上限,谁先跑完就先处理谁 │
└────────────────────────┴───────────────────────┘
│ 某个块产出 output
▼
┌────────────────────────┬───────────────────────┐
│ EdgeManager.processOutgoingEdges │
│ - 该走的边: 划掉目标节点的这条入边 │
│ - 不该走的边: 标记失活,沿下游级联剪枝 │
│ - 入边被清空的目标: 成为新的就绪节点 │
└────────────────────────┴───────────────────────┘
│ 新就绪的节点
└──▶ 回到最上面的就绪队列
2.2 主干部件一句话职责
| 部件 | 干什么 | 文件 · 符号 |
|---|---|---|
| 画布 | React Flow 渲染块与边,编辑即改库 | apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx |
| 行存储 | 每个块一行、每条边一行、每个容器一行 | packages/db/schema.ts:300 workflowBlocks、:226 workflowEdges、:258 workflowSubflows |
Serializer | 行 / 前端状态 → SerializedWorkflow | apps/sim/serializer/index.ts:159 serializeWorkflow |
DAGBuilder | 序列化结构 → DAG(节点 + 入/出边 + 哨兵) | apps/sim/executor/dag/builder.ts:52 build |
ExecutionEngine | 就绪队列调度、并发追踪、暂停 / 取消收口 | apps/sim/executor/execution/engine.ts:181 run |
EdgeManager | 决定哪条边激活、把失活沿下游级联剪枝 | apps/sim/executor/execution/edge-manager.ts:253 shouldActivateEdge |
NodeExecutionOrchestrator | 分派:普通块交给 BlockExecutor,哨兵节点自己处理 | apps/sim/executor/orchestrators/node.ts:42 |
LoopOrchestrator / ParallelOrchestrator | 循环该不该再来一轮、并行分支怎么分批与汇总 | apps/sim/executor/orchestrators/loop.ts:69、parallel.ts:43 |
BlockExecutor | 解析入参 → 找 handler → 归一化输出 | apps/sim/executor/execution/block-executor.ts:96 execute |
VariableResolver | 把参数里的 <块名.字段>、{{ENV}} 解析成真值 | apps/sim/executor/variables/resolver.ts:153 |
| Handler 注册表 | 16 类块处理器(agent / api / condition / wait / …) | apps/sim/executor/handlers/registry.ts:33 createBlockHandlers |
| Provider 层 | 统一各家 LLM 的请求 / 流式 / 工具调用 | apps/sim/providers/index.ts:164 executeProviderRequest |
| Tool 层 | 按工具 ID 发出真实外部调用 | apps/sim/tools/index.ts:1513 executeTool |
PauseResumeManager | 暂停落库、恢复时重建执行、排队的 resume 收尾 | apps/sim/lib/workflows/executor/human-in-the-loop-manager.ts:490 |
LoggingSession | 块级开始 / 结束、trace span、最终落库 | apps/sim/lib/logs/execution/logging-session.ts:200 |
2.3 旁支一:触发面
主干只回答「怎么跑」,不回答「谁让它跑」。触发面有四条入口,全部收敛到同一个函数:
手动 Run / API POST ─┐
Chat 部署端点 ─┤
Webhook(外部回调) ─┼──▶ executeWorkflowCore() ──▶ §2.1 的主干
Schedule(定时) ─┘ 单一入口
webhook 与 schedule 走 trigger.dev 后台任务(apps/sim/background/webhook-execution.ts、apps/sim/background/schedule-execution.ts:656),API 与 Chat 走 Next.js 路由(apps/sim/app/api/workflows/[id]/execute/route.ts、apps/sim/app/api/chat/[identifier]/route.ts)。区别只在「谁把请求变成 ExecuteWorkflowOptions」,之后的路完全一样。336 条触发器声明在一张表里:apps/sim/triggers/registry.ts:482 TRIGGER_REGISTRY。
2.4 旁支二:Copilot(Mothership)
这是「让 AI 替你搭工作流」的那半边。它不在本仓库里跑 LLM 主循环:
浏览器 Chat
│ SSE
▼
本仓库客户端半边 远端 Go 服务(不在本仓库)
apps/sim/lib/copilot/request/* copilot.sim.ai
│ │
│ ─────── HTTP + SSE ─────────────▶ ├─ LLM 主循环
│ ◀────── 工具调用指令 ──────────── ├─ 上下文 / 子 agent 编排
▼ └─ 决定调哪个工具
按 catalog 的 route 分桶:sim / go / subagent / client
│
└─ route='sim' 的工具在本仓库执行(改画布、跑工作流、读知识库…)
远端地址默认值写死在 apps/sim/lib/copilot/constants.ts:3(SIM_AGENT_API_URL_DEFAULT = 'https://www.copilot.sim.ai'),实际解析走 apps/sim/lib/copilot/server/agent-url.ts:43 getMothershipBaseURL,出站请求统一经 apps/sim/lib/copilot/request/go/fetch.ts:29 fetchGo,SSE 消费循环在 apps/sim/lib/copilot/request/go/stream.ts:146 runStreamLoop。
Copilot 也能反过来出现在工作流里:MothershipBlockHandler(apps/sim/executor/handlers/mothership/mothership-handler.ts:746)让一个块本身就是「叫 AI 来干活」。
3. 主线走一遍:按下 Run 之后
这一节只走高层链路,不进代码细节;每一站的「为什么」留给对应章节。
executeWorkflow ← 建 executionId、开日志会话、埋点、收口暂停状态
↓
executeWorkflowCore ← 取工作流状态 + 环境变量 → 序列化 → 造 Executor
↓
DAGExecutor.execute ← 建 DAG、还原快照、组装执行流水线
↓
ExecutionEngine.run ← 就绪队列循环,直到没活干 / 出错 / 暂停 / 取消
第 1 站:executeWorkflow(apps/sim/lib/workflows/executor/execute-workflow.ts:92)
负责一次执行的「外壳」:生成 executionId、建 LoggingSession、把回调(onStream / onBlockStart / onBlockComplete)打包,跑完后发埋点,并调 handlePostExecutionPauseState(apps/sim/lib/workflows/executor/pause-persistence.ts:28)决定这次是「暂停了要存快照」还是「结束了要处理排队的恢复请求」。
第 2 站:executeWorkflowCore(apps/sim/lib/workflows/executor/execution-core.ts:376)
这是官方注释里说的 single source of truth,四件事顺序很重要:
- 取状态——三选一:显式覆盖、草稿表、已部署快照(
loadWorkflowState,execution-core.ts:482)。 - 取环境变量——个人 + 工作区的加密变量,解密后进执行上下文。
- 定起点——没指定触发块时按执行种类(api / chat / external / manual)找起始块。
- 序列化 + 造执行器——
new Serializer().serializeWorkflow(...)(:489)→new Executor({...})(:694)→executorInstance.execute(...)(:708)。
第 3 站:DAGExecutor.execute(apps/sim/executor/execution/executor.ts:87)
Executor 只是 DAGExecutor 的别名(apps/sim/executor/index.ts:6)。这一站把序列化结构编译成 DAG——先算「从触发块出发能到哪些块」,再给每个循环 / 并行造一对哨兵,最后把连线翻译成节点上的 incomingEdges / outgoingEdges——然后还原快照里的并行批次与保存过的入边,在 buildExecutionPipeline(:346)里把六个部件按依赖串起来:VariableResolver → BlockExecutor → EdgeManager → Loop/ParallelOrchestrator → NodeExecutionOrchestrator → ExecutionEngine,最后 return await engine.run(triggerBlockId)(:102)。
第 4 站:ExecutionEngine.run(apps/sim/executor/execution/engine.ts:181)
循环极简:只要还有活干(就绪队列非空,或还有在飞的执行)就继续调度(hasWork,:202);出现取消、错误、提前停止就跳出。
initializeQueue → while (hasWork) { processQueue } → 等所有在飞的执行落地
│
├─ 队列里所有节点全部并发发起
├─ 谁先完成,谁的出边就交给 EdgeManager 处理
└─ 处理结果 = 一批新就绪节点 → 入队
跑完后有四种归宿:正常返回、status: 'paused'(buildPausedResult,:493)、status: 'cancelled'、抛错。撞上暂停时,引擎把整张图当前的边状态打包成快照返回,由外层落进 paused_executions 表,等人或等时间到了再从快照恢复。