跳到主要内容

经典执行引擎:把图拓扑构建成 LangChain 流水线

30 秒导读: 你在画布上连了一堆节点(模型、提示词、记忆、链),点"发送"。这条老引擎(type=CHATFLOW / MULTIAGENT)负责把那张图变成一条能真跑的 LangChain 流水线:先把节点/连线算成有向图找出起点和终点,再按依赖顺序逐个把节点实例化,把上游实例回填给下游的 {{nodeId.data.instance}} 占位符,最后只 run() 终点那个节点,顺着实例引用一路拉动整条链。

本章讲 Flowise 的经典执行引擎——typeCHATFLOW(普通 Chatflow / 链)、MULTIAGENT(多智能体、Sequential Agents)的流程都走它。V2 引擎(AGENTFLOW)是另一套解释器,见 03-agentflow-v2-engine.md,本章不覆盖;节点/图的数据模型见 01-flow-and-node-model.md,请求生命周期与流式/队列见 04-request-lifecycle-streaming-queue.md


1. 这是什么(零基础也能懂)

一句话定义: 一个把"可视化流程图"翻译成"可执行 LangChain 链"的运行时。它不发明新的执行模型,而是借用 LangChain 的组合能力——每个节点 init() 出一个 LangChain 对象(模型 / 提示词 / 链 / 记忆),下游节点把上游对象当成自己的构造参数装进去,最后终点是一条组装好的链,run() 一下就出结果。

它要解决的问题: 画布上的节点是"声明式"的——它们只说"我依赖谁",没说"谁先跑"。引擎要做三件事:

  1. 定序——谁先实例化、谁后实例化(记忆和模型得先造好,链才能引用它们)。
  2. 连线——怎么把"上游节点造出来的对象"塞进"下游节点的输入槽"。
  3. 收口——整张图最后从哪个节点开始拉动执行。

用起来什么样: 用户看不到引擎,只看到一次对话请求。引擎在服务端一次调用里跑完:

POST /api/v1/prediction/<chatflowid>
{ "question": "帮我总结这段话", "streaming": true }


executeFlow(...) ← 本章主角,把图跑成一条链,返回 { text, chatId, ... }

一句话直觉: 把它想成 Makefile + 依赖构建。节点是构建目标,连线是依赖,引擎按拓扑顺序"编译"每个目标(new + init),编译产物(LangChain 实例)被下一个目标引用,最后"编译"到终点,run 一次整条链就活了。


2. 顶层全景(它大概怎么转)

引擎入口是 executeFlowpackages/server/src/utils/buildChatflow.ts:301)。它把一次请求拆成"建图 → 定位起终点 → 逐节点构建 → 跑终点"四步。

怎么读下面这张图: 从上到下是执行顺序;左边是"图拓扑"相关工具函数,右边是"节点实例化"主循环。

请求 (question / overrideConfig / uploads / streaming)


┌───────────────────────────────────────────┐
│ executeFlow (buildChatflow.ts:301) │
│ 编排:文件上传→建图→起终点→buildFlow→run │
└───────────────────────────────────────────┘

① 建图与定向 │
constructGraphs ──────────►│ graph(邻接表) + nodeDependencies(入度)
(utils/index.ts:155) │

② 定位终点/起点/深度 │
getEndingNodes ──────────►│ 终点节点(链/Agent)
(index.ts:295) │
getStartingNodes ─────────►│ 起点 + depthQueue(反向图算深度)
(index.ts:230) │


┌───────────────────────────────────────────┐
│ buildFlow (index.ts:516) │
│ BFS 队列:对每个节点 │
│ import 文件 → new nodeClass() │
│ → resolveVariables(插值) → init() │
│ → 实例回填 flowNodes[i].data.instance │
└───────────────────────────────────────────┘
│ 返回已装配好 instance 的 flowNodes

┌────────────────────────┴────────────────────────┐
│ │
普通 Chatflow / 链 多智能体 / Sequential
initEndingNode → endingNodeInstance.run() buildAgentGraph(...)
(buildChatflow.ts:750 / :788) (buildAgentGraph.ts:36)
│ │
▼ ▼
{ text, sourceDocuments, ... } LangGraph 流式结果

部件一句话职责:

部件干什么在哪个文件:符号
executeFlow总编排:处理上传、建图、算起终点、调 buildFlow、跑终点、落库、遥测buildChatflow.ts:301
constructGraphsnodes/edges 变成邻接表 graph + 入度表 nodeDependencies,支持反向/无向index.ts:155
getEndingNodes找出终点节点(出度为 0 且是链/Agent 类)并校验index.ts:295
getStartingNodes反向图从终点回推起点,同时算出每个节点的 depthQueue(层级)index.ts:230
buildFlowBFS 主循环:逐节点 import → new → resolveVariables → init,把实例回填index.ts:516
resolveVariables把节点输入里的 {{...}} 占位符替换成真实值(上游实例 / 问题 / 历史 / 变量)index.ts:1027
initEndingNode单独初始化终点节点,返回它的 data 和实例buildChatflow.ts:144
buildAgentGraph多智能体/Sequential 的入口,在已装配节点上跑 LangGraphbuildAgentGraph.ts:36
isFlowValidForStream判断这条流能不能 SSE 流式输出index.ts:1470

主线走一遍(高层): 请求进来 → constructGraphs 得到有向图和入度 → getEndingNodes 找终点、getStartingNodes(喂反向图)找起点和深度 → buildFlow 从起点 BFS 到终点,一路把每个节点造出实例 → 若是普通链,initEndingNode + 终点 .run() 出结果;若是多智能体,转 buildAgentGraph 跑 LangGraph。


3. 核心原理(逐个机制,由浅入深)

3.1 建图与定向:从"节点+连线"到"邻接表+入度"

它要解决的小问题: ReactFlow 给的是一堆 nodesedges(每条边有 source/target)。要定序,得先有"谁指向谁"(邻接表)和"谁依赖几个上游"(入度)。

思路: 遍历所有边,正向时把 target 记进 graph[source]、并给 nodeDependencies[target] 入度 +1。入度为 0 的就是没有上游的起点

constructGraphs 有三种模式,靠 options 切换(index.ts:155):

模式options边怎么记用途
正向(默认)graph[source].push(target)target 入度+1主执行方向:起点→终点
反向isReversed: truegraph[target].push(source)target 入度+1从终点回推起点、算依赖
无向isNonDirected: true两个方向都记ifElse 剪枝时算"连通块"

真实实现: 反向分支在 index.ts:169-184——注意它 return 得早,反向图只连边不做无向补充。正向分支在 index.ts:186-204。入度表初始化把每个节点先置 0(index.ts:163-167)。

为什么要反向图? 因为一张图可能有多个终点,Flowise 是以终点为锚回推的:executeFlow 对每个终点调 getStartingNodes(nonDirectedGraph, endingNodeId)buildChatflow.ts:527-536)。反向图让"从终点往回走"变成正常的正向遍历。

关键细节: getStartingNode(单数,index.ts:213)只是"入度为 0 即起点"的朴素版;真正用于定序的是 getStartingNodes(复数,index.ts:230),它顺便算深度(见 3.2)。别搞混这俩。

3.2 定位起点、终点与深度

终点怎么找(getEndingNodesindex.ts:295): 遍历 graph,出度为 0 且入度 > 0 的节点是终点候选(index.ts:301-307);单节点图特判为终点。然后逐个校验:终点必须是这几类之一,否则报错(index.ts:325-337)。

允许作终点的 category说明
Chains各类链
Agents工具 Agent 等
EngineLlamaIndex 引擎
Multi Agents多智能体(supervisor/worker)
Sequential Agents顺序智能体

多终点时失败的自动忽略,只要有一个能过校验就行(verifiedEndingNodesindex.ts:313-343)。

起点+深度怎么算(getStartingNodesindex.ts:230): 喂进去的是反向图。它用递归 walkGraph 从终点出发,给每个节点记一个"离终点多远"的 depthQueue,遇到多路径取 Math.maxindex.ts:236-244)。然后把深度反转(Math.abs(depth - maxDepth)index.ts:246-252)——这样离起点近的深度小。深度为 0 的就是起点(index.ts:254-256)。

为什么深度重要? buildFlow 用它做"同层补齐"——保证同一层的节点都会被排进队列,即使它们之间没有直接连线(见 3.3 的 sameDepthNodeIds)。

另有一个更简单的 BFS 版 calculateNodesDepthindex.ts:2039),从起点正向算深度、支持"发现更短路径就更新"(index.ts:2066-2068)——主要给 V2 和其它场景用,经典引擎主路径用的是 getStartingNodes 的反推深度。

3.3 逐节点构建:buildFlow 的 BFS 主循环

它要解决的小问题: 有了起点、终点、深度,怎么按正确顺序把每个节点"造出来",并保证造下游前上游已经就绪?

思路: BFS。从起点入队,每弹出一个节点就实例化它,再把它的下游节点入队——但只有当一个节点的所有上游都已初始化,才允许它进队。这靠一张反向图 reversedGraph 做依赖检查。

主循环骨架(index.ts:577-754,锚点在 577-640):

初始化:起点入队 nodeQueue,maxLoop=3,reversedGraph=反向图 (index.ts:559-567)
while (队列非空): (index.ts:577)
弹出 { nodeId, depth }
找到 reactFlowNode
── import(节点文件) → new nodeModule.nodeClass() (index.ts:585-587)
── flowNodeData = cloneDeep(节点data)
── 若 overrideConfig 且开启 → replaceInputsWithConfig (index.ts:592-594)
── resolveVariables(插值 {{...}}) (index.ts:598-607)
── outputResult = await newNodeInstance.init(...) (index.ts:641)
── flowNodes[i].data.instance = outputResult ← 回填! (index.ts:703)
── 把合格的下游节点入队(见下) (index.ts:726-747)

为什么先 newinit,还要 resolveVariables 夹在中间? 因为 init() 需要拿到已经插好值的输入——比如某个链节点的 model 输入是 {{chatOpenAI_0.data.instance}},得先把它替换成上游真正 init 出来的模型对象,init 才能把模型装进链里。所以顺序必须是"上游先 init 好 → 下游 resolveVariables 时才能取到上游 instance → 下游再 init"。

回填是整个机制的心脏(index.ts:703): flowNodes[nodeIndex].data.instance = outputResult。每个节点 init 的产物存回它自己的 data.instance,下游节点通过 {{该nodeId.data.instance}} 就能引用到(见 3.4)。

下游入队的四道闸(index.ts:726-747): 对每个邻居节点,依次检查后才入队:

代码含义
忽略表if (ignoreNodeIds.includes(...)) continue (:728)ifElse 剪掉的分支不走
已初始化if (initializedNodes.has(...)) continue (:729)别重复造
上游未齐if (reversedGraph[...].some(未init)) continue (:730)依赖没就绪就不进队
防环exploredNode + remainingLoop (:732-746)见下

防环:maxLoop = 3index.ts:559): 图里可能有环(比如带循环的流程)。每个节点在 exploredNode 里记 remainingLoop(初始 3)和 lastSeenDepth。再次遇到同一节点:同深度就跳过(:735);remainingLoop 到 0 就 break:737-739);否则 -1 后再入队(:740-742)。这就是"最多转 3 圈"的兜底,防止死循环。

同层补齐(index.ts:716-722): 除了直接邻居,还把 depthQueue 里"下一层"的节点也加进邻居列表——保证同层节点不会因为没有直接连线而被漏掉。

终点后置(index.ts:749-753): 没有下游(!neighbourNodeIds.length)的节点被 spliceflowNodes 末尾。这样返回的数组里终点在最后,initEndingNode 在多终点时可以直接取 reactFlowNodes[length-1]buildChatflow.ts:174)。

两个特殊节点在循环里被特判:

  • setVariableindex.ts:664-672):把它 init 出的 dynamicVariables 收集起来,供后续节点用 $vars.xxx 引用。
  • ifElseFunctionindex.ts:675-701):根据 init 返回的 type 决定走哪条分支,另一条分支及其所有下游被算进 ignoreNodeIds(用无向图 getAllConnectedNodes 求连通块,index.ts:691-697)——这就是流程里的条件分支剪枝。

注意 isUpsert 复用(index.ts:609-631): 同一个 buildFlow 也被向量库 upsert 复用——碰到 stopNodeId 就调 vectorStoreMethods.upsert 然后 break。经典对话执行走的是 else 分支的 initisUpsert:false,见 buildChatflow.ts:593)。

3.4 变量插值:{{...}} 怎么变成真实值

它要解决的小问题: 节点输入里全是占位符——{{chatOpenAI_0.data.instance}}{{question}}{{chat_history}}。谁来、什么时候、按什么规则替换成真值?

入口 resolveVariablesindex.ts:1027): 遍历节点的 inputs,每个值(或数组里每项)调 getVariableValueindex.ts:1039-1075)。是否把占位符当"可接受变量"处理,取决于该输入参数的 acceptVariable 标记(index.ts:1060)。

核心 getVariableValueindex.ts:874 是一个手写的 {{ }} 扫描器:用一个 variableStack 匹配 {{}}index.ts:893-905),取出中间的 variableFullPath,按前缀分派:

占位符替换成代码锚点
{{question}}当前用户问题(转义换行/制表)index.ts:912-914
{{file_attachment}}上传文件内容 uploadedFilesContentindex.ts:916-918
{{chat_history}}convertChatHistoryToText(chatHistory)index.ts:920-922
{{$vars.xxx}}全局/运行时变量(getGlobalVariableindex.ts:924-931
{{$flow.xxx}}flowConfig 里的字段(chatId/sessionId…)index.ts:933-939
{{<nodeId>.data.instance[.key]}}上游节点 data.instance(或其子路径)index.ts:944-985

这些前缀常量定义在文件顶部(index.ts:67-73):QUESTION_VAR_PREFIX='question'FILE_ATTACHMENT_PREFIX='file_attachment'CHAT_HISTORY_VAR_PREFIX='chat_history' 等。

最关键的一支——引用上游实例(index.ts:944-985): 把路径按 . 拆开,第一段 variableNodeIdreactFlowNodes 里找那个节点,取它的 data.instanceindex.ts:948)。这就是 3.3 里 init 回填的实例被"接回来"的地方——上游造好的 LangChain 对象,在这里被塞进下游的输入。若路径超过 3 段(xxx.data.instance.key),还会把 instance 当 JSON 解析后按子路径取值(index.ts:951-979)。

两种替换语义(isAcceptVariable):

  • 接受变量模式index.ts:980-1015):把所有命中收进 variableDict,最后按 path 排序后逐个字符串替换回原串。排序是为了先替换更长的路径(可能含嵌套,index.ts:993)。对象值要 JSON.stringify 并处理引号转义(index.ts:997-1010)——还特判了 agentflow 节点,只留 id 避免循环引用(index.ts:999-1002)。
  • 非接受模式index.ts:982-984):直接把 returnVal 整个替换成那个 instance(通常是把某个输入槽整体设为上游对象)。

一段直觉演示(示意,非源码):

// 假设下游"对话链"节点的 inputs 是:
// { model: "{{chatOpenAI_0.data.instance}}", prompt: "回答:{{question}}" }
// resolveVariables 把它变成:
inputs.model = <上游 chatOpenAI_0 init 出来的真实 ChatOpenAI 对象> // 引用回填
inputs.prompt = "回答:帮我总结这段话" // {{question}} 展开
// 然后链节点 init(inputs) 就能把真实模型装进链里

4. 编排入口:executeFlow 怎么把上面串起来

executeFlowbuildChatflow.ts:301)是这条引擎的"总导演"。它被 utilBuildChatflow 在非队列模式下直接调用(buildChatflow.ts:1104;队列模式则把活派给 worker,见 04 章)。

它的步骤(按代码顺序):

  1. 补默认值 + 处理上传:324-479):给 incomingInput 填默认;把图片/RAG 文件/音频落存储,音频还走语音转文字覆盖 question:377-406),file:full 类型拼进 uploadedFilesContent:408-413)。
  2. V2 分流:481-506):chatflow.type === 'AGENTFLOW' 直接转 executeAgentFlow(本章不覆盖)。
  3. 解析图 + 会话:508-519):JSON.parse(flowData)nodes/edgesfindMemoryNode + getMemorySessionIdsessionId
  4. 算起终点:521-537):constructGraphsgetEndingNodes → 对每个终点用反向图 getStartingNodes 汇总 startingNodeIdsdepthQueue
  5. 取历史 + override 配置:542-566):getChatHistory 从记忆节点拉历史;getAPIOverrideConfig 取节点/变量覆盖开关。
  6. buildFlow:571-601):BFS 把所有节点装配出 instance(即 3.3)。
  7. 分两条路收口:
    • 多智能体 / Sequential:605-733,判定见 :539-540):调 buildAgentGraph,拿回流式结果后落库、遥测、返回。
    • 普通 Chatflow / 链:734-933):checkIfStreamValid 判流式 → initEndingNode 拿终点实例 → endingNodeInstance.run(...):788)→ 可选 postProcessing(:815-857)→ 落库、遥测、TTS、返回。

override / 记忆 / 流式的三个决策点:

关切谁决定锚点
override(用 API 传的配置盖节点输入)getAPIOverrideConfig + replaceInputsWithConfigbuildFlow:592-594initEndingNode:180-182index.ts:1091
记忆/会话(sessionId、chat_history)findMemoryNode / getMemorySessionId / getChatHistorybuildChatflow.ts:517-519:208
是否流式checkIfStreamValidisFlowValidForStreambuildChatflow.ts:943index.ts:1470

流式判定 isFlowValidForStreamindex.ts:1470 是个多条件与门,全为真才流式:

  1. 图里存在 Chat Model / LLM,且它没被显式关掉 streaming(:1500-1517)——旧模型还得在白名单 streamAvailableLLMs 里(:1472-1498)。
  2. 终点是可流式的链/Agent/Engine(各有黑/白名单,:1519-1535)。
  3. 没有 Output Parser 节点(有解析器就不能流式,:1537-1546)。

外层 checkIfStreamValidbuildChatflow.ts:943)再叠加:有"自定义函数终点"直接禁流(:954-955),且请求本身得带 streaming=true:980)。另外 postProcessing 开启时也强制 isStreamValid=false:743-744)。

终点执行 initEndingNodebuildChatflow.ts:144): 多终点时取 buildFlow 后置到末尾的那个节点(:171-174),再做一次 override + resolveVariables:180-193),import + new nodeClass({ sessionId }) 返回实例(:197-199)。真正拉动执行的是 executeFlow 里的 endingNodeInstance.run(endingNodeData, finalQuestion, runParams):788)——顺着 data.instance 的引用链,整条 LangChain 流水线被一次跑通。


5. 多智能体 / Sequential 简述:在同一套装配上跑 LangGraph

普通链的终点是"一个链对象 .run()"。多智能体不一样——它需要多个 agent 之间来回路由,这用 LangChain 的 LangGraph(状态图执行器)来做。但前半程完全复用经典引擎buildFlow 照样把每个 agent/worker/supervisor 节点 init 成实例,只是收口时不 run 终点,而是转给 buildAgentGraphbuildAgentGraph.ts:36)。

buildAgentGraph 先按节点类型分出两种拓扑,再编译成 LangGraph:

拓扑触发条件编译函数LangGraph 结构
Multi Agents(supervisor/worker)没有 Sequential 节点compileMultiAgentsGraph:429supervisor 作中枢,worker 作叶子,用 addConditionalEdges(next) 由 supervisor 决定下一个 worker
Sequential Agents存在 category === 'Sequential Agents' 的节点compileSeqAgentsGraph:636seqStart/seqEnd/seqLoop 等节点显式连边,depthQueue 定顺序

判定在 :131 if (!seqAgentNodes.length) 走 multi-agent,否则 isSequential=true 走 sequential(:149-151)。分类靠三张过滤(:115-117):name==='worker'name==='supervisor'category==='Sequential Agents'

Multi-agent 编译(:429-614):new StateGraph<ITeamState>:460)→ 逐个 worker init 并按 parentSupervisorName 归到各 supervisor 名下(:474-503)→ supervisor initaddNode + 给每个 worker 加"回到 supervisor"的边(:550-554)+ addConditionalEdgesx.next 路由到目标 worker(:563-569)→ workflowGraph.compile({ checkpointer }):575)→ graph.stream(...) 返回流(:603)。

Sequential 编译(:636-...):seqState 节点取状态通道 channels:663-669)→ new StateGraph:671)→ 校验必须恰好一个 seqStart、至少一个 seqEnd/seqLoop:677-686)→ 按边和 depthQueue 把 agent/tool/condition 节点连成图后 compile。

编译出的图统一 streambuildAgentGraph:169-for await 循环里消费流事件、映射节点名、累积 agentReasoning/工具/来源文档,最后回吐给 executeFlowbuildChatflow.ts:607-731)落库返回。LangGraph 本身的原理不在本章范围。


6. 巧妙之处(可借鉴)

  • 以终点为锚、反向图回推起点buildChatflow.ts:527-536 + index.ts:230):不从起点猜终点,而是先定终点再反推——天然支持"多终点、失败即忽略"的健壮性(getEndingNodesverifiedEndingNodesindex.ts:313-343)。
  • data.instance 回填 = 依赖注入index.ts:703index.ts:948):上游 init 产物存回节点自身,下游用 {{nodeId.data.instance}} 取回。一个字段就把"构建顺序"和"对象传递"两件事解耦了。
  • maxLoop=3 的软防环index.ts:559:737-742):不禁止环,而是限次数——允许"有意的循环流程"跑几圈又不会死循环。
  • resolveVariables 手写扫描器而非正则index.ts:893-989):用栈匹配 {{ }},能稳妥处理嵌套与对象序列化的引号转义(:997-1010),比一把梭正则更可控。
  • 一个 buildFlow 三用:对话执行、向量库 upsert(stopNodeId + breakindex.ts:609-631)、多智能体前置装配全共用同一套 BFS,减少重复。

7. 边界与局限(诚实)

  • 同步一次性构建:经典引擎在一次调用里把整图建完再跑,没有 V2 的"队列驱动、逐步可中断"的解释器语义。要那种能力见 03 章
  • maxLoop=3 是硬编码index.ts:559):真需要多轮循环的经典流程会被截断,代码里看不出可配置。
  • 流式白/黑名单是"演进包袱"isFlowValidForStream 顶部注释自承 Deprecated, add streaming input param to the component insteadindex.ts:1471:1510),硬编码模型名单会随新模型加入而过时。
  • getStartingNodes 用递归 walkGraphindex.ts:236-244):注释假设是 DAG(index.ts:235);真有环时深度计算依赖后续 buildFlow 的防环兜底,而非这里。
  • Document Loader 有特判绕过checkIfDocLoaderShouldBeIgnoredindex.ts:449):接向量库的 doc loader 在构建时被跳过(因为 upsert 时已加载),代码 TODO 标注这是待清理的历史逻辑(index.ts:443)。

8. 代码地图(导航索引)

用符号名 grep 比行号更抗漂移。全部 as-of sourceCommit

主题文件路径符号名
编排总入口packages/server/src/utils/buildChatflow.tsexecuteFlow
引擎被调用处(非队列)packages/server/src/utils/buildChatflow.tsutilBuildChatflowexecuteFlow(executeData),:1104)
建图 + 入度 + 反向/无向packages/server/src/utils/index.tsconstructGraphs
找终点并校验packages/server/src/utils/index.tsgetEndingNodes
反推起点 + 深度packages/server/src/utils/index.tsgetStartingNodes
朴素起点(入度 0)packages/server/src/utils/index.tsgetStartingNode
连通块(ifElse 剪枝)packages/server/src/utils/index.tsgetAllConnectedNodes
正向 BFS 深度packages/server/src/utils/index.tscalculateNodesDepth
逐节点构建主循环packages/server/src/utils/index.tsbuildFlow
占位符插值入口packages/server/src/utils/index.tsresolveVariables
单值 {{...}} 求值packages/server/src/utils/index.tsgetVariableValue
占位符前缀常量packages/server/src/utils/index.tsQUESTION_VAR_PREFIX / CHAT_HISTORY_VAR_PREFIX / FILE_ATTACHMENT_PREFIX
override 覆盖输入packages/server/src/utils/index.tsreplaceInputsWithConfig
全局/运行时变量packages/server/src/utils/index.tsgetGlobalVariable
流式可行性判定packages/server/src/utils/index.tsisFlowValidForStream
外层流式判定packages/server/src/utils/buildChatflow.tscheckIfStreamValid
终点节点初始化packages/server/src/utils/buildChatflow.tsinitEndingNode
取会话历史packages/server/src/utils/buildChatflow.tsgetChatHistory
多智能体/Sequential 入口packages/server/src/utils/buildAgentGraph.tsbuildAgentGraph
multi-agent 编译packages/server/src/utils/buildAgentGraph.tscompileMultiAgentsGraph
sequential 编译packages/server/src/utils/buildAgentGraph.tscompileSeqAgentsGraph