跳到主要内容

节点生态:LLM 调用、84 个内置节点与插件系统

30 秒导读: 前几章讲的是"图怎么被调度、控制流怎么撑起分支循环";这一章讲图里每个方块自己在干什么。 重点看两件事:一是最重的那条路径——Chat 节点如何把"组装消息 → 流式拉取 → 出错重试 → 算 token 与花费" 一口气做完;二是 Rivet 怎么用一张注册表 + 一个插件回调,把 84 个内置节点和一堆外部模型/服务 统一挂进来。

本章承接:引擎(02) 讲"谁来 process",控制流(03) 讲"control-flow-excluded 怎么控分支"。这里不重复调度与控制流,只讲节点内部扩展机制


1. 先建立直觉:节点是什么、Chat 节点为什么最重

节点 = 图里一个会算东西的方块。 引擎按拓扑顺序把上游算好的值喂给它,它 process 一把,吐出 新的值给下游。绝大多数节点都很轻(拼字符串、抽 JSON、走个 if)。

Chat 节点是整个货架里工程含量最高的一支。 因为"调一次大模型"远不止发个 HTTP 请求:

  • 把用户给的乱七八糟输入(字符串 / 消息 / 消息数组)归一成一串 chat message;
  • 一边流式收增量、一边实时回传给 UI(你在画布上看到字一个个蹦出来就是它);
  • 网络抖动 / 限流 / 超时要自动重试,还不能把用户已经看到的进度弄丢;
  • 收完要数 token、按模型价目表算钱,从一个 cost 端口吐出去。

所以本章把 Chat 节点单独拎出来精讲,其余 80 多个节点只做分类点名,细节指回源码。

一句类比:把 Chat 节点当成"带自动重拨、带跑马灯、带计价器的电话机"——你只管说要打给谁, 它负责拨通、断线重拨、把对方的话一个字一个字念给你听、挂机后给你一张话费单。


2. 顶层全景:一次 Chat 调用怎么转

Chat 节点的实现被拆成两层:薄薄的 ChatNodeImpl(节点外壳)+ 厚厚的 ChatNodeBase(真正干活)。 ChatNodeImpl 的每个方法几乎都是原样转发给 ChatNodeBase:

真源码:packages/core/src/model/nodes/ChatNode.ts:61-63ChatNodeImpl.process 只有一行—— return ChatNodeBase.process(this.data, this.chartNode, inputs, context)。为什么要拆?因为像 chatLoopNode 这类节点要复用同一套 LLM 逻辑,把逻辑放进 ChatNodeBase 这个普通对象(不是类), 谁都能 import 来调。

怎么读下面这张图: 从上到下是一次 process 的时间顺序;虚线框是"会重复发生"的重试圈。

输入端口 (prompt / systemPrompt / model / functions / …)


① 归一化输入 getInputOrData(...) 逐个取参数
getChatNodeMessages 把 prompt 归一成 ChatMessage[],system prompt 塞最前
│ ChatNodeBase.ts:1573 / :1609-1617

② 查模型表 openaiModels[model] → maxTokens / cost / displayName
(查不到就用兜底 Custom) ChatNodeBase.ts:993-1002


③ (可选) 客户端数 token context.tokenizer.getTokenCountForMessages(...)
超限就砍 maxTokens 并 warn ChatNodeBase.ts:1033-1051


┌─ ④ retry(...) 重试圈 ──────────────────────────────────┐
│ p-retry 包住整个请求 ChatNodeBase.ts:1094 │
│ │ │
│ ▼ │
│ streamChatCompletions(...) 流式拉增量 :1253 │
│ │ │
│ ▼ for await (chunk of chunks) │
│ 拼 response / function_calls :1286-1384 │
│ │ │
│ ▼ 每 ~100ms 节流一次 │
│ context.onPartialOutputs?.(output) 跑马灯回传 :1278 │
│ │ │
│ 出错 → onFailedAttempt 判断要不要重拨 :1499-1563 │
└─────────────────────────────────────────────────────────┘
│ 流收完

⑤ 结算 算 token → getCostForTokens → cost 端口
ChatNodeBase.ts:1448-1478


输出端口 (response / function-calls / all-messages / responseTokens / cost / usage / duration)

部件一句话职责:

部件干什么在哪
ChatNodeImpl节点外壳:端口定义、UI、create,process 转发给 basenodes/ChatNode.ts:13
ChatNodeBase真正的处理逻辑,被 Chat / ChatLoop 复用nodes/ChatNodeBase.ts:102
getChatNodeMessages把杂乱输入归一成 ChatMessage[]nodes/ChatNodeBase.ts:1573
streamChatCompletions对 OpenAI 兼容端点发流式请求,yield 每个 chunkutils/openai.ts:512
openaiModels模型 → maxTokens / 每千 token 单价 表utils/openai.ts:27
getCostForTokens(tokenCount/1000) * 单价nodes/ChatNodeBase.ts:1622

3. 核心机制:Chat 节点这条最重的路径

3.1 消息组装——把"什么都能接"归一成消息数组

要解决的小问题: prompt 端口的类型被声明成 ['chat-message','chat-message[]'] (ChatNodeBase.ts:263-269),但实际用户可能连了个字符串、字符串数组、单条消息、消息数组,甚至别的。 节点必须把它们全部收拢成一个 ChatMessage[]

思路:ts-patternmatch 做模式匹配,一种输入形状对应一条归一规则;都不匹配就尽力 coerce 成字符串当 user 消息。

真实实现 getChatNodeMessages @ nodes/ChatNodeBase.ts:1576-1607:

// 示意,非源码:重点看"每种输入形状 → 归一成 ChatMessage[]"
let messages = match(prompt)
.with({ type: 'chat-message' }, p => [p.value]) // 单条 → 包成数组
.with({ type: 'chat-message[]' }, p => p.value) // 已是数组 → 原样
.with({ type: 'string' }, p => [{ type: 'user', message: p.value }])
.with({ type: 'string[]' }, p => p.value.map(v => ({ type: 'user', message: v })))
.otherwise(p => /* 尽力 coerce 成 string / chat-message */ [...]);

关键细节:system prompt 单独走一条端口,并强制塞到最前。 如果消息里已有一条 system 消息, 先把它删掉再把新的 system prompt 插到 [0],避免出现两条系统消息(ChatNodeBase.ts:1609-1617)。

3.2 流式增量 + onPartialOutputs 回传——画布上的跑马灯

要解决的小问题: 大模型回一大段要几秒到几十秒,不能让用户干等。要边收边把"目前收到的半截答案" 推给 UI 显示。

思路: streamChatCompletions 是个异步生成器(async function*),内部用 fetchEventSource 读 SSE,把每个 data 行 JSON.parseyield 出来(utils/openai.ts:512-561)。请求体里强制带上 stream: truestream_options: { include_usage: true }(utils/openai.ts:532-537)——后者让 最后一个 chunk 带上 token 用量,省得客户端自己数。

节点侧用 for await 消费,每个 chunk 把 delta.content 往对应 choice 的数组里追加, delta.tool_calls按 index 累加 name / arguments 字符串,并尝试实时 JSON.parselastParsedArguments(ChatNodeBase.ts:1296-1335)。

巧妙处——节流回传,别把 UI 冲垮。 每来一个 chunk 都回传会太频繁,于是包了一层节流:

真实实现 nodes/ChatNodeBase.ts:1277-1284:

// 示意,非源码:距上次回传超过阈值(默认 100ms)才真正回传一次
let throttleLastCalledTime = Date.now();
const onPartialOutput = (output) => {
const now = Date.now();
if (now - throttleLastCalledTime > (context.settings.throttleChatNode ?? 100)) {
context.onPartialOutputs?.(output); // 推给 UI / 上层 Subgraph
throttleLastCalledTime = now;
}
};

流循环结束后,还会无条件再回传一次最终结果,确保最后一帧不被节流吞掉 (context.onPartialOutputs?.(output) @ nodes/ChatNodeBase.ts:1387)。

这也解释了 useAsGraphPartialOutput 这个开关(默认 true,ChatNodeBase.ts:125): 它决定本节点的流式增量要不要透传给"调用本图的 Subgraph 节点",让父图也能看到跑马灯。

3.3 p-retry 重试——断线自动重拨,但不乱重拨

要解决的小问题: 网络会抖、会限流(429)、会超时(408)。要自动重试,但对"我们自己写错了" (4xx 参数错误)这种不该重试的错要立刻抛出。

思路:p-retry 把整个"发请求 + 收流"包起来(import retry from 'p-retry' @ ChatNodeBase.ts:28, 调用点 ChatNodeBase.ts:1094)。退避参数几乎是"永不放弃"式:

参数含义
forevertrue一直重试
retries10000上限一万次
maxRetryTime1000*60*5但最多重试 5 分钟
factor2.5指数退避倍率
minTimeout / maxTimeout500 / 5000退避 0.5s 起、封顶 5s
randomizetrue加抖动,避免同时重拨

以上见 nodes/ChatNodeBase.ts:1490-1498

关键是 onFailedAttempt 里的分诊逻辑(ChatNodeBase.ts:1499-1563):

  • signal.aborted → 直接抛 Aborted,不再重试;
  • 网络类错误(fetch failed / terminated)→ return,即"吞掉错误,继续重试";
  • 429 限流 / 408 超时 → 还有重试次数就顺便把"正在重试…"当作 response 回传给 UI再重试 (ChatNodeBase.ts:1535-1557),用户不会以为卡死;
  • 其它 4xx(400–499)→ 这是我们自己请求写错了,立刻抛出,重试没意义。

3.4 计 token 与算钱——从 tokenizer 到 cost 端口

两处都要 token 数: 一是发请求前防止超模型上下文上限,二是收完后算钱。

发请求前(可选,客户端算)。 只有关掉 useServerTokenCalculation 时才在客户端数: context.tokenizer.getTokenCountForMessages(messages, functions, tokenizerInfo) (ChatNodeBase.ts:1034)。默认 tokenizer 是 GptTokenizerTokenizer (integrations/GptTokenizerTokenizer.ts)。数出来若 >= openaiModel.maxTokens 直接报错; 若"输入 + maxTokens"超限,则把 maxTokens 砍到剩余额度的 95% 并加一条 warning (ChatNodeBase.ts:1036-1050)。

模型价目表 openaiModels 每个模型一条记录,形如(utils/openai.ts:27-38):

// 示意,非源码:每千 token 单价(prompt/completion 分开计)
'gpt-5': {
maxTokens: 400000,
cost: { prompt: 1.25e-6, completion: 10e-6 },
displayName: 'GPT-5',
supported: { parallelFunctionCalls: true },
},

查不到的自定义模型有兜底:maxTokensoverrideMaxTokens ?? 8192cost 全 0 (ChatNodeBase.ts:993-1002)——即"自定义模型不计价"。

收完后算钱。 优先用服务器回的 usage(prompt/completion tokens),否则用客户端数的。 成本公式极简:

真实实现 getCostForTokens @ nodes/ChatNodeBase.ts:1622-1624:

export function getCostForTokens(tokenCount: number, costPerThousand: number) {
return (tokenCount / 1000) * costPerThousand; // 每千 token 单价 × 千 token 数
}

prompt cost + completion cost 之和写进 cost 端口(ChatNodeBase.ts:1453-1478), 同时 usage 端口带上 prompt_cost / completion_cost / total_cost 明细。

一个诚实的坑(inferred): utils/openai.ts:42gpt-5-mini 的 prompt 单价写成 0.25 - 6(等于 -5.75),显然是把 0.25e-6 手误成减法。这会让该模型的 prompt 成本算成负数。 只是价目表里的一个笔误,不影响主流程逻辑。

3.5 两条出口:流式 vs 非流式

不是所有请求都能流式。ChatNodeBase.ts:1139 判断:o1-beta 模型(o1-preview/o1-mini)或开了音频输出 走非流式的 chatCompletions(utils/openai.ts:487,一次性 fetch + response.json());其余走 流式 streamChatCompletions。两条出口最后都写同样的输出端口并 Object.freeze(output) 后塞进模块级 cache(ChatNodeBase.ts:100,受 data.cache 开关控制,:1129-1133)。


4. 84 个内置节点的功能地图

先说清数量:packages/core/src/model/nodes/ 下有 84 个 .ts 文件,但其中 ChatNodeBase.ts 是被复用的共享实现、不是节点本身;registerBuiltInNodes 里实际 .register(...) 的是 83 个节点类型(model/Nodes.ts:253-338)。下面按"这节点在 agent 工作流里干的活"分类点名—— 只归类、不展开,单节点细节请按文件名进源码看。

文本 / prompt 组装:

节点干什么文件
Text文本模板 / 插值TextNode.ts
Prompt造一条 chat-message(system/user/assistant)PromptNode.ts
Assemble Prompt / Assemble Message把多条片段拼成消息序列AssemblePromptNode.ts / AssembleMessageNode.ts
Trim Chat Messages按 token 预算裁剪历史TrimChatMessagesNode.ts
Chunk按 token 切块ChunkNode.ts
Chat / Chat LoopLLM 调用 / 带循环的对话ChatNode.ts / ChatLoopNode.ts

逻辑控制(本章不展开,详见 03): IfNode / IfElseNode / MatchNode / CompareNode / CoalesceNode / LoopControllerNode / LoopUntilNode / RaceInputsNode / AbortGraphNode / DelayNode / PassthroughNode

数据处理与抽取:

节点干什么文件
Extract JSON从文本里抠出 JSONExtractJsonNode.ts
Extract YAML抠 YAMLExtractYamlNode.ts
Extract Regex正则抽取ExtractRegexNode.ts
Extract Object Path按路径取字段ExtractObjectPathNode.ts
Extract Markdown Code Blocks抠 markdown 代码块ExtractMarkdownCodeBlocksNode.ts
To JSON / To YAML / To Markdown Table / To Tree序列化成各格式ToJsonNode.ts
Evaluate / Hash表达式求值 / 哈希EvaluateNode.ts / HashNode.ts

数组与对象: ArrayNode / ObjectNode / FilterNode / JoinNode / SliceNode / SplitNode / ShuffleNode / PopNode / DestructureNode / NumberNode / BooleanNode / RandomNumberNode

图组合(把子图当函数用):

节点干什么文件
Graph Input / Graph Output子图的入参 / 出参端口GraphInputNode.ts / GraphOutputNode.ts
Subgraph内联调用另一张图SubGraphNode.ts
Call Graph / Graph Reference / List Graphs动态按引用调图 / 引用图 / 列图CallGraphNode.ts
Context / Get Global / Set Global读上下文 / 读写全局变量ContextNode.ts / GetGlobalNode.ts / SetGlobalNode.ts
Raise Event / Wait For Event / Delegate Function Call事件与函数调用委派RaiseEventNode.ts

数据集与向量(RAG 的料):

节点干什么文件
Create / Load / Append / Replace Dataset数据集增改CreateDatasetNode.ts
Get Dataset Row / Get All Datasets读取GetDatasetRowNode.ts / GetAllDatasetsNode.ts
Dataset Nearest Neighbors数据集内近邻检索DatasetNearestNeigborsNode.ts
Get Embedding取向量GetEmbeddingNode.ts
Vector Store / Vector Nearest Neighbors向量库写入 / 检索VectorStoreNode.ts / VectorNearestNeighborsNode.ts

IO(伸手到外部世界): HttpCallNode(HTTP 调用)/ ReadFileNode / ReadDirectoryNode / ReadAllFilesNode(文件读取)/ ExternalCallNode(调宿主注入的函数)/ UserInputNode(要人输入)/ DocumentNode / ImageNode / AudioNode / PlayAudioNode / CronNode / CommentNode

代码 / MCP(下面 §5 单讲): CodeNodeMCPDiscoveryNodeMCPToolCallNodeMCPGetPromptNode


5. 两类"能跑外部逻辑"的节点

5.1 Code 节点——沙箱里跑 JS

做什么: 让用户在节点里写一段 JS,拿到 inputs 对象、返回带类型的 outputs (nodes/CodeNode.ts:44-55 的默认代码示范了这个约定)。

执行不在节点里做,而是委托给 context.codeRunner(nodes/CodeNode.ts:184-197),并把几个能力开关 一起传下去:allowFetch / allowRequire / allowRivet / allowProcess / allowConsole (CodeNode.ts:22-31)。这几个开关就是沙箱边界——决定这段代码能不能上网、能不能 require

真实的执行器 IsomorphicCodeRunner @ integrations/CodeRunner.ts:26:它按开关把 console / fetch / Rivet 等对象拼进参数列表,然后用 new AsyncFunction(...argNames) 把用户代码编译成一个异步函数再调 (CodeRunner.ts:72-74)。

// 示意,非源码:只有被允许的能力才会被注入进作用域
const argNames = ['inputs']; const args = [inputs];
if (options.includeFetch) { argNames.push('fetch'); args.push(fetch); }
if (options.includeRivet) { argNames.push('Rivet'); args.push(Rivet); }
// require / process 在这个执行器里直接拒绝(需要 Node executor)
const AsyncFunction = async function(){}.constructor;
const fn = new AsyncFunction(...argNames, code);
return await fn(...args);

边界要点:

  • 这是 Function 构造器执行,不是真隔离的 VM——同源代码能访问被注入的对象。真正的安全隔离靠 运行时选哪个执行器(见 05)。
  • includeRequire / includeProcess 在 Isomorphic 执行器里直接抛错"requires the Node executor" (CodeRunner.ts:37-44)——浏览器端跑不了。
  • 想彻底禁掉动态代码,宿主可换成 NotAllowedCodeRunner,它对任何代码都抛 "Dynamic code execution is disabled"(CodeRunner.ts:80-90)。

5.2 MCP 节点——把 Rivet 接到 MCP 服务器

做什么: 通过 [Model Context Protocol] 发现并调用外部工具/prompt。三个节点分工: MCPDiscoveryNode(列服务器上的工具)、MCPToolCallNode(调工具)、MCPGetPromptNode(取 prompt)。

和 Code 节点一样,节点本身不实现协议,而是委托给 context.mcpProvider (nodes/MCPToolCallNode.ts:268,没配就报 "MCP Provider not found")。它支持两种传输:

传输触发条件调用
http有 serverUrl(且必须含 /mcp)context.mcpProvider.httpToolCall(...) MCPToolCallNode.ts:284
stdio用本地 MCP 配置里的 serverIdcontext.mcpProvider.stdioToolCall(...) MCPToolCallNode.ts:298

MCPProvider 接口本身定义在 integrations/mcp/MCPProvider.ts:135(getHTTPTools / getStdioTools / httpToolCall / stdioToolCall 等),具体实现由宿主提供。

模式已经很清楚了:凡是"要碰外部世界 / 要执行外部逻辑"的节点(Code、MCP、Dataset、Vector、 Embedding、Chat 的 endpoint),节点只描述意图,真正的能力由 InternalProcessContext 上的 provider 注入。 这让同一张图在浏览器、Node、测试里能换不同实现跑——详见 05


6. 插件系统:用一个回调把外部模型挂进注册表

内置节点覆盖不了所有厂商(Anthropic、Google、Hugging Face…),Rivet 用插件扩展。核心就三个东西: 一个接口、一张注册表、一个取密钥的函数。

6.1 RivetPlugin 接口——插件长什么样

真实定义 model/RivetPlugin.ts:4-24,关键字段:

字段作用
id全局唯一标识
nameUI 里显示名
register?一个回调:(register) => { register(someNode); ... },插件在这里把自己的节点交出去
configSpec?配置项声明(API key、endpoint 等),UI 据此生成表单
contextMenuGroups?往右键"新建节点"菜单里加分组

一个最小插件长这样(真实的 anthropicPlugin @ plugins/anthropic/plugin.ts:4-30):

// 示意,非源码:结构对齐真实的 anthropicPlugin
export const anthropicPlugin: RivetPlugin = {
id: 'anthropic',
name: 'Anthropic',
register: (register) => { register(chatAnthropicNode); }, // 把节点交出去
configSpec: {
anthropicApiKey: {
type: 'secret',
label: 'Anthropic API Key',
pullEnvironmentVariable: 'ANTHROPIC_API_KEY', // 允许从环境变量兜底
},
// anthropicApiEndpoint: { type: 'string', default: 'https://api.anthropic.com/v1', ... }
},
};

6.2 registerPluginNode——插件节点怎么进注册表

注册表是 NodeRegistration。它对内置节点有 register(model/NodeRegistration.ts:26),对插件节点有 registerPluginNode(model/NodeRegistration.ts:54)。

关键在 registerPlugin(model/NodeRegistration.ts:105-110):它就是调用插件的 register 回调,并把 "每次收到一个节点定义就 registerPluginNode 进来"这件事作为参数传进去:

真实实现 model/NodeRegistration.ts:105-110:

registerPlugin(plugin: RivetPlugin) {
if (plugin.register) {
plugin.register((definition) => this.registerPluginNode(definition, plugin));
}
this.#plugins.push(plugin);
}

registerPluginNode 内部会把插件节点定义包成一个 PluginNodeImplClass 子类,存进 #infos / #implsMap, 并plugin 一起记下(NodeRegistration.ts:76-88)——这一步很重要,因为节点运行时要靠它找回 "我属于哪个插件",才能读到该插件的配置(下面 6.3)。重复的 node type 会直接抛 Duplicate node type (NodeRegistration.ts:62)。

内置的 8 个插件在 plugins.ts:20-30 汇总成一张 plugins 表: anthropic / autoevals / assemblyAi / pinecone / huggingFace / gentrace / openai / google。 它们各自的 nodes/ 目录里放着要挂的节点(如 anthropic 的 ChatAnthropicNode、openai 的 Assistant/Thread 系列、pinecone 的向量节点)。

6.3 getPluginConfig——从设置或环境变量取密钥

节点运行时不直接碰全局设置,而是通过 context.getPluginConfig(name) 拿配置(例如 ChatAnthropicNode.ts:536context.getPluginConfig('anthropicApiKey'))。这个 context 方法在 GraphProcessor.ts:1640 处绑定到了 getPluginConfig(plugin, settings, name),并且已经把"当前节点属于 哪个插件"绑进去了

取值优先级(真实实现 utils/getPluginConfig.ts:3-32):

① 该插件没在 configSpec 里声明 name → 返回 undefined(未知配置项)
② settings.pluginSettings[plugin.id][name] 有值(字符串) → 用它 ← UI 里填的
③ 否则看 configSpec[name].pullEnvironmentVariable → 取 settings.pluginEnv[envName] ← 环境变量兜底
④ 都没有 → undefined
// 示意,非源码:先查 UI 设置,再退回环境变量
const spec = plugin.configSpec?.[name];
if (!spec) return undefined;
const fromSettings = settings.pluginSettings?.[plugin.id]?.[name];
if (typeof fromSettings === 'string' && fromSettings) return fromSettings;
const envName = spec.pullEnvironmentVariable === true ? name : spec.pullEnvironmentVariable;
if (envName && settings.pluginEnv?.[envName]) return settings.pluginEnv[envName];
return undefined;

为什么这么设计: 开发时你可以只 export ANTHROPIC_API_KEY=... 就跑(靠 ③);上生产再在 UI/settings 里显式配(靠 ②)。同一份图不用改就能在两种场景切换。


7. 巧妙之处(可借鉴)

  • 薄外壳 + 厚 base,逻辑可复用。 ChatNodeImpl.process 一行转发给 ChatNodeBase.process (ChatNode.ts:61),让 chatLoopNode 等能直接复用整套 LLM 逻辑,不必继承一棵深类树。
  • 重试里顺手回传"正在重试…"。 限流/超时时把提示文字当 response 推给 UI(ChatNodeBase.ts:1535-1557), 用户永远知道系统在动,而不是白屏干等。
  • 节流回传 + 结束补一帧。 平时按 ~100ms 节流(ChatNodeBase.ts:1278-1284),流结束再无条件回传一次 (:1387),兼顾流畅与"最后一帧不丢"。
  • 能力按开关注入,而非默认全给。 Code 节点的 allowFetch/allowRequire/...(CodeNode.ts:22-31)+ 执行器实现(CodeRunner.ts)让沙箱边界显式、可控、可替换。
  • 插件配置"UI 优先、环境变量兜底"。 一个 getPluginConfig 同时服务开发便利与生产显式配置 (getPluginConfig.ts:3-32)。

8. 边界与局限(诚实)

  • Chat 节点深度绑定 OpenAI 兼容形状。 消息、toolsresponse_format、cost 都按 OpenAI 的字段建模 (ChatNodeBase.ts + utils/openai.ts);别家模型(Anthropic/Google)只能走插件节点另起一套,不是复用 Chat 节点。
  • openaiModels 价目表是硬编码快照,会过时且可能有笔误。 例如 gpt-5-mini 的 prompt 单价被写成 0.25 - 6(utils/openai.ts:42),算出来是负数;自定义模型一律按 0 计价(ChatNodeBase.ts:996-1001)。 cost 端口只作参考,别当账单。
  • Code 节点默认执行器不是强隔离。 IsomorphicCodeRunnerAsyncFunction 执行,靠"不注入能力"限制 而非真 VM 沙箱(CodeRunner.ts:72-74);要安全得靠宿主换执行器。
  • MCP / Dataset / Vector / Embedding 等节点没有 provider 就直接报错(如 MCPToolCallNode.ts:268), 它们的可用性完全取决于宿主注入了什么。

9. 横向对比(同组其它章)

  • 谁来调 process、拉取式数据流怎么转 → 02 引擎
  • control-flow-excluded 怎么撑起分支/循环/竞速(本章 §4 的逻辑控制类节点)→ 03 控制流
  • provider(tokenizer / codeRunner / mcpProvider)从哪来、图怎么嵌进产品跑 → 05 运行时与测试
  • 节点、数据值、图模型的词汇表 → 01 数据模型

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

主题文件路径符号名
Chat 节点外壳(转发)packages/core/src/model/nodes/ChatNode.tsChatNodeImpl / chatNode
Chat 处理主逻辑packages/core/src/model/nodes/ChatNodeBase.tsChatNodeBase.process
消息归一化packages/core/src/model/nodes/ChatNodeBase.tsgetChatNodeMessages
成本公式packages/core/src/model/nodes/ChatNodeBase.tsgetCostForTokens
流式请求(异步生成器)packages/core/src/utils/openai.tsstreamChatCompletions
非流式请求packages/core/src/utils/openai.tschatCompletions
模型价目/上限表packages/core/src/utils/openai.tsopenaiModels
内置节点注册链packages/core/src/model/Nodes.tsregisterBuiltInNodes
Code 节点packages/core/src/model/nodes/CodeNode.tsCodeNodeImpl
代码执行器(沙箱)packages/core/src/integrations/CodeRunner.tsIsomorphicCodeRunner / NotAllowedCodeRunner
MCP 工具调用节点packages/core/src/model/nodes/MCPToolCallNode.tsMCPToolCallNodeImpl.process
MCP provider 接口packages/core/src/integrations/mcp/MCPProvider.tsMCPProvider
插件接口packages/core/src/model/RivetPlugin.tsRivetPlugin
节点注册表packages/core/src/model/NodeRegistration.tsregisterPlugin / registerPluginNode
取插件配置packages/core/src/utils/getPluginConfig.tsgetPluginConfig
内置插件汇总packages/core/src/plugins.tsplugins
示例插件packages/core/src/plugins/anthropic/plugin.tsanthropicPlugin