跳到主要内容

从图到产品:嵌入运行时、调试器、序列化与测试

30 秒导读: 前四章讲的是 Rivet 的"引擎内核"——节点与数据模型GraphProcessor 拉取式引擎control-flow-excluded 撑起的分支循环LLM 节点与插件生态。但引擎自己不碰真实世界:它不读文件、不连 MCP、不跑代码、不存盘、不画界面。本章讲的就是包在引擎外面的那一圈"外壳与工具链"——把一张图变成能跑、能调、能存、能测、能交付的产品。


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

一句话定义: 这一章讲的是 Rivet 从"一个能算图的库"变成"一个能用的产品"所需要的全部外围子系统

先建立一个直觉。第 2 章里的 GraphProcessor 是一台纯粹的计算引擎:你喂给它一张图和一份ProcessContext(运行上下文),它就能把图算出来。但它故意不知道怎么读磁盘、怎么发 HTTP、怎么起 MCP 子进程——这些"脏活"全被抽象成接口,留给外面的宿主去实现。

这么设计的好处是同一个引擎能跑在两个完全不同的地方:

落地路径谁在用宿主包
库(嵌入到你自己的 Node 程序里)后端服务、脚本、Serverless@ironclad/rivet-node
App(桌面图形界面里可视化编排)用鼠标连线搭图的人@ironclad/rivet-app(Tauri 桌面壳)

它解决什么问题: 想象你在 Rivet 桌面 App 里用鼠标搭好了一张 LLM 工作流,现在你想:

  • 把它部署成一个 HTTP API,让生产服务调用 → 靠 rivet serve(本章 §4)。
  • 本地脚本里嵌入它,读文件、调 MCP → 靠 runGraphInFile(本章 §2)。
  • 图跑在远端服务器上,但你想用桌面 App 的界面实时看它每个节点的执行 → 靠 WebSocket 远程调试器(本章 §3)。
  • 把图存成文件、明年用新版 Rivet 还能打开 → 靠版本化序列化(本章 §5)。
  • 改了图之后想自动回归测试它有没有跑坏 → 靠 Trivet 测试框架(本章 §6)。

一句话直觉: 如果 GraphProcessor 是一台发动机,那么本章讲的就是底盘、油箱、仪表盘、钥匙和年检——发动机再好,没有这些也开不上路。


2. 顶层全景(外壳怎么把引擎包起来)

2.1 五个子系统一张图

本章的五个外围子系统,围绕中央的 GraphProcessor 各管一段。先看它们怎么协作:

┌──────────────────────────────────────┐
│ GraphProcessor(引擎内核) │
│ —— 见第 2 章,本章不重复 —— │
└──────────────────────────────────────┘
▲ ▲ │
注入运行上下文 │ │ attach 挂事件 │ 发事件
(ProcessContext)│ │ ▼
┌────────────────────────┴──┐ ┌────┴──────────────────────────┐
│ ① Node 宿主包 │ │ ② 远程调试器 (WebSocket) │
│ createProcessor / runGraph│ │ startDebuggerServer │
│ 注入 4 个 Native 实现: │ │ 把 nodeStart/nodeFinish/... │
│ NativeApi / Dataset / │ │ 广播给桌面 App 客户端 │
│ CodeRunner / MCP / RefLdr│ └───────────────────────────────┘
└────────────┬───────────────┘
│ 被复用
┌────────────┴───────────────┐ ┌───────────────────────────────┐
│ ③ CLI:rivet serve / run │ │ ④ 序列化(存/读 .rivet-*) │
│ 把图暴露成 HTTP / 命令行 │ │ serializeProject 固定写 v4 │
└────────────────────────────┘ │ deserialize 回退 v4→v3→v2→v1 │
└───────────────────────────────┘
┌────────────────────────────┐ ┌───────────────────────────────┐
│ ⑤ 桌面 App(Tauri 壳) │ │ ⑥ Trivet 测试框架 │
│ Vite/React + jotai + 自研 │ │ 用"验证图"给"被测图"断言 │
│ 画布 NodeCanvas/Wire │ │ runTrivet │
└────────────────────────────┘ └───────────────────────────────┘

怎么读这张图: 中间是引擎;上下左右六个框都是"外围"。①是所有落地方式的地基(它决定引擎能碰到哪些真实世界能力);③⑤都建立在①之上;②是把引擎的内部事件"直播"出去;④⑥则相对独立地负责持久化和质量。

2.2 各子系统一句话职责

子系统干什么关键文件
Node 宿主包组装 ProcessContext 并驱动引擎跑图packages/node/src/api.ts
5 个 Native 实现给引擎"手脚":读文件、存数据集、跑代码、连 MCP、加载引用项目packages/node/src/native/*
远程调试器WebSocket 服务端,把引擎事件广播给远端 Apppackages/node/src/debugger.ts
CLIserve(HTTP API)/run(一次性命令)packages/cli/src/
序列化图/项目/数据集 ↔ YAML 文本,带版本回退packages/core/src/utils/serialization/*
桌面 AppTauri Rust 壳 + Vite/React 前端 + 自研画布packages/app/src/*src-tauri/*
Trivet用一张"验证图"对"被测图"做断言的测试框架packages/trivet/src/*

3. 核心机制一:Node 宿主包如何组装 ProcessContext

这一节讲整个外壳最关键的一件事:引擎不知道怎么碰真实世界,宿主替它把"手脚"都插好

3.1 它要解决的小问题

GraphProcessor.processGraph(context, inputs, contextValues) 的第一个参数 context(ProcessContext)是一堆接口:NativeApi(文件系统)、DatasetProvider(数据集)、CodeRunner(执行代码节点)、MCPProvider(连 MCP 服务器)、ProjectReferenceLoader(加载被引用的子项目)……引擎只调接口方法,从不关心背后是 Node 的 fs 还是浏览器的 IndexedDB

宿主包的工作,就是把这些接口用 Node 平台的真实能力实现出来,再一次性注入。

3.2 主入口:三个便利函数

@ironclad/rivet-node 对外暴露一条极短的调用链,从文件到输出只要一行:

// 示意,非源码:嵌入 Rivet 到你自己的 Node 程序
import { runGraphInFile } from '@ironclad/rivet-node';

const outputs = await runGraphInFile('./my.rivet-project', {
graph: 'main', // 图的 id 或名字
inputs: { question: 'hi' }, // 图输入
openAiKey: process.env.OPENAI_API_KEY,
});
// 重点看:你没碰任何 Native 实现,宿主默认全给你插好了

真实实现是三层套娃,一层比一层高层:

函数干什么源码
loadProjectFromFile(path)读文件文本 → loadProjectFromString 反序列化成 Projectpackages/node/src/api.ts:25
createProcessor(project, options)造出 processor 并包一个 run(),run() 里注入 ProcessContextpackages/node/src/api.ts:44
runGraph / runGraphInFile上面两步的组合,一把梭packages/node/src/api.ts:35,101

3.3 关键一步:run() 里注入了什么

createProcessor 先调核心的 coreCreateProcessor(第 2 章的引擎工厂)拿到裸 processor,再返回一个替换过 run() 的对象。真正的注入发生在这个 run() 里(packages/node/src/api.ts:68-98):

// 真实源码骨架(packages/node/src/api.ts:68),已省略 settings 细节
async run() {
const outputs = await processor.processor.processGraph(
{
nativeApi: options.nativeApi ?? new NodeNativeApi(),
datasetProvider: options.datasetProvider,
mcpProvider: options.mcpProvider ?? new NodeMCPProvider(),
codeRunner: options.codeRunner ?? new NodeCodeRunner(),
projectReferenceLoader:
options.projectReferenceLoader ?? new NodeProjectReferenceLoader(),
settings: { /* openAiKey / pluginEnv / chatNodeTimeout ... */ },
// ...
},
processor.inputs,
processor.contextValues,
);
return outputs;
}

读法: 每个接口都是 options.X ?? new NodeX()——调用方给了就用给的,没给就 new 一个 Node 默认实现。这就是"手脚可插拔"的落点。processGraph 是引擎真正开跑的那个方法(见第 2 章)。

createProcessor 还顺手做了两件外壳杂务(packages/node/src/api.ts:50-58):把 processor.executor 标成 'nodejs';若传了 remoteDebugger,就 remoteDebugger.attach(processor)——把引擎接上本章 §4 的调试器。

3.4 五个 Native 实现,各补一只手脚

这五个类就是"引擎的手脚"。逐个看它们补的是什么能力:

Native 实现补的能力关键源码值得注意的坑
NodeNativeApi文件系统:readdir(支持递归/glob/ignore)、读写文本/二进制packages/node/src/native/NodeNativeApi.ts:15exec() 故意抛 Not Implemented(:73)——不让图任意执行 shell
NodeDatasetProvider数据集读写,可落盘到 .rivet-data 文件packages/node/src/native/NodeDatasetProvider.ts:28fromProjectFile.rivet-project 后缀换成 .rivet-data(:74)
NodeCodeRunner执行 Code 节点里的用户 JSpackages/node/src/native/NodeCodeRunner.ts:5new AsyncFunction(...) 动态构造函数(:56),按需注入 console/require/process/fetch/Rivet
NodeMCPProvider连 MCP 服务器(HTTP/SSE/STDIO 三种传输)packages/node/src/native/NodeMCPProvider.ts:8HTTP 先试 StreamableHTTP,失败回退 SSE(:21-30);STDIO 走子进程(:107)
NodeProjectReferenceLoader加载被主项目引用的子项目packages/node/src/native/NodeProjectReferenceLoader.ts:7reference.hintPaths 逐个相对路径试读,全失败才报错(:13-26)

一个值得记住的安全边界: NodeNativeApi.exec 直接抛异常(packages/node/src/native/NodeNativeApi.ts:73)——即便宿主给了文件读写能力,也不开放任意命令执行。要跑代码,只能走 NodeCodeRunner 那条受控的 AsyncFunction 路径。

3.5 插件环境变量的自动拉取

宿主还替你把插件要的环境变量从 process.env 里抠出来。getPluginEnvFromProcessEnv(packages/node/src/api.ts:106)遍历注册表里每个插件的 configSpec,凡是标了 pullEnvironmentVariable 的字符串配置,就去 process.env 取对应变量塞进 pluginEnv。这样你在库里跑图时,插件密钥不用手动一个个传。


4. 核心机制二:WebSocket 远程调试器

这一节讲一个很"产品化"的能力:图在远端服务器上真跑,但你在本地桌面 App 里像看本地执行一样,实时看到每个节点的启停、输出、报错。

4.1 它要解决的小问题

桌面 App 的调试界面是最好用的——能看到每个节点高亮、看到中间输出、能暂停/继续。但生产环境里图往往跑在没有界面的服务器上。怎么让本地 App 的界面"遥控"并"透视"远端的执行?

答案:在服务器端起一个 WebSocket 服务,把引擎的每一个内部事件都序列化后广播给连上来的 App 客户端;App 反过来也能通过这条连接发 run/pause/abort 等指令。

4.2 数据流:双向

桌面 App(前端) 远端服务器(Node 宿主)
───────────── ──────────────────────
useRemoteDebugger startDebuggerServer
new WebSocket("ws://host:21888") WebSocketServer(port 21888)
│ │
│ ── run / pause / abort / ──────────▶│ handleMessage: ts-pattern
│ user-input / preload │ match(message.type)
│ │ │
│ │ processor.pause() 等
│ │ │
│ ◀── nodeStart / nodeFinish / ───────│ attach(): processor.on(evt)
│ partialOutput / done / error │ → broadcast(JSON)
▼ ▼
界面实时高亮/显示输出 引擎照常 processGraph

读法: 左边 App 发控制指令,右边服务器把指令翻译成对 processor 的方法调用;右边引擎发执行事件,服务器广播回左边界面。默认端口 21888

4.3 服务端:startDebuggerServer

入口 startDebuggerServer(packages/node/src/debugger.ts:52)开一个 WebSocketServer,默认 port = 21888(:65)。每个连接的消息处理分两类:

① 客户端 → 引擎的控制指令,用 ts-patternmatch 分派(packages/node/src/debugger.ts:101):

消息 type动作源码行
rundynamicGraphRun(动态起一次图运行)debugger.ts:102
abort / pause / resumeprocessor.abort()/pause()/resume()debugger.ts:141-149
user-inputprocessor.userInput(nodeId, answers)(回答 User Input 节点)debugger.ts:150
preloadprocessor.preloadNodeData(...)(预置节点输出)debugger.ts:154
set-dynamic-data上传整个项目(需 allowGraphUpload)debugger.ts:122

② 引擎 → 客户端的事件广播,在 attach(processor) 里把 processor 的每个事件都转成 broadcast(packages/node/src/debugger.ts:206)。订阅的事件几乎覆盖引擎的全部生命周期:nodeStartnodeFinishnodeErrorstartdonepartialOutputgraphStartpauseuserInput……(debugger.ts:214-283)。

4.4 一个体现工程味的细节:partialOutput 限流

LLM 节点会逐 tokenpartialOutput。如果每个 token 都通过 WebSocket JSON 序列化发一遍,会把连接打爆。所以 attach 里给 partialOutput 单独做了按节点限流(packages/node/src/debugger.ts:247-256):

// 真实源码(packages/node/src/debugger.ts:247)
processor.on('partialOutput', (data) => {
if (
lastPartialOutputsTimePerNode[data.node.id] == null ||
(lastPartialOutputsTimePerNode[data.node.id] ?? 0) + throttlePartialOutputs < Date.now()
) {
this.broadcast(processor, 'partialOutput', data);
lastPartialOutputsTimePerNode[data.node.id] = Date.now();
}
});

重点看: 每个节点记一个"上次广播时间",两次广播至少间隔 throttlePartialOutputs(默认 100ms)。注释里直白写着这么做的原因——不然序列化端会"ridiculous"。

4.5 客户端:App 怎么连

App 侧用 useRemoteDebugger 这个 hook 连接(packages/app/src/hooks/useRemoteDebugger.ts:27)。默认 URL 就是 ws://localhost:21888(:29),断线会用 setTimeout 自动重连(:68)。有一个特殊 URL ws://localhost:21889/internal 被判定为内部执行器(:39)——即 App 自带的 executor 子进程,不算"真正的远程调试"。


5. 核心机制三:CLI —— serve 与 run

这一节讲怎么不写一行代码就把图跑起来或部署出去。CLI 是 Node 宿主包最薄的一层封装。

5.1 两个命令

CLI 入口 packages/cli/src/cli.tsyargs 注册了两个命令(:9-20):

命令用途实现
rivet run <projectFile> [graphName]跑一次图,把输出打到 stdoutpackages/cli/src/commands/run.ts:40
rivet serve [projectFile]把图暴露成 HTTP REST API(Hono 服务)packages/cli/src/commands/serve.ts:83

5.2 run:一次性执行

# 示意:跑一次图,从 --input 传参
rivet run ./my.rivet-project main --input question=hi --include-cost

run(packages/cli/src/commands/run.ts:40)做的事很直白:loadProjectFromFile 读项目 → 从 --input k=v / --context k=v / stdin 收集输入 → createProcessor(...).run()JSON.stringify 打印输出。默认删掉 cost 字段,除非 --include-cost(:115)。

5.3 serve:部署成 HTTP API

serve(packages/cli/src/commands/serve.ts:83)用 Hono 起一个 HTTP 服务:POST / 收 JSON 输入、跑图、返回 JSON 输出(:115)。几个产品化选项:

选项作用源码
--dev每次请求都重新读项目文件(热更新)serve.ts:116
--stream用 SSE(Server-Sent Events)把执行事件流式推给客户端serve.ts:128,200
--allow-specifying-graph-id允许在 URL 路径里指定要跑哪张图serve.ts:145
--expose-cost响应里带上 token 成本serve.ts:276

流式模式 streamGraph(serve.ts:200)会调 createProcessor 拿到 getSSEStream,把 nodeStart/nodeFinish/partialOutputs 事件包成 SSE 流返回——本质上是把 §4 那套事件,换个 HTTP 出口给非 App 的客户端用。

CLI 还内置了很贴心的图名纠错:若找不到指定的 graph,用 didyoumean2 给出"你是不是想跑 X?"的建议(serve.ts:318run.ts 同理)。


6. 核心机制四:项目序列化与向后兼容

这一节讲 Rivet 怎么保证"今天存的图,明年用新版还能打开"。

6.1 它要解决的小问题

图是用户的资产,格式却会随版本演进。序列化层要同时满足两个要求:写的时候只写最新格式(简单、确定性),读的时候能吃下所有历史格式(向后兼容)。

6.2 写:永远写 v4

serializeProject(packages/core/src/utils/serialization/serialization.ts:18)直接委托给 projectV4Serializer——只写 v4,不给选择。v4 序列化器(serialization_v4.ts:100)做两件关键事:

  1. safe-stable-stringify确定性排序(:110)——保证同一张图每次序列化出逐字节相同的文本,对 git diff 友好。
  2. 输出 YAML,顶层带 version: 4 标记(serialization_v4.ts:114)。

6.3 读:v4 → v3 → v2 → v1 逐级回退

deserializeProject(packages/core/src/utils/serialization/serialization.ts:24)是一串 try/catch 瀑布:先试 v4 反序列化器,抛错就试 v3,再不行 v2,最后 v1,全挂才 throw 'Could not deserialize project'

deserializeProject(text)

┌──────────▼──────────┐ 成功 → 返回 [Project, AttachedData]
│ projectV4Deserializer│──────────────────┐
└──────────┬──────────┘ │
抛错 │ │
┌──────────▼──────────┐ │
│ projectV3Deserializer│──成功 → 返回 ─────▶│
└──────────┬──────────┘ │
抛错 │ │
┌──────────▼──────────┐ │
│ projectV2Deserializer│──成功 → 返回 ─────▶│
└──────────┬──────────┘ │
抛错 │ │
┌──────────▼──────────┐ ▼
│ projectV1Deserializer│──成功 → 返回 调用方拿到 Project
└──────────┬──────────┘
抛错 │
throw 'Could not deserialize project'

怎么读: 从上往下是"从新到旧"逐个尝试,命中即返回;每级失败会 console.warn 一条(如 serialization.ts:33),方便排查是哪一版格式的问题。图(deserializeGraph)和数据集用同样的回退策略(serialization.ts:69)。

一个小设计: v4 反序列化器还能顺带解析出 AttachedData(附加数据),而 v3/v2/v1 回退时一律返回空的 {}(serialization.ts:36,45,54)——老格式没有这个概念,诚实地给空。


7. 桌面 App 的形态(Tauri + Vite/React + 自研画布)

这一节讲最终用户看到的那个"连线搭图"的桌面应用长什么样。

一处与直觉不符、需要澄清的地方: Rivet 桌面 App 的画布是完全自研的,不是 reactflow 库;状态管理用的是 jotai,不是 Recoil。下面按源码实际情况讲。

7.1 双层结构:Rust 壳 + Web 前端

Tauri 应用天然分两层:

技术职责源码
外壳Rust(Tauri)开原生窗口、菜单、文件系统权限、调用系统能力packages/app/src-tauri/src/main.rs
前端Vite + React全部 UI 与画布逻辑,跑在 webview 里packages/app/src/*

Rust 侧 main.rstauri::Builder 装配应用(src-tauri/src/main.rs:19),通过 invoke_handler 暴露几个原生命令给前端调用:get_environment_variableread_relative_project_fileallow_data_file_scope 等(:21-26)。应用标识符是 com.ironcladapp.rivet(src-tauri/tauri.conf.json:77)。前端由 beforeDevCommand: "yarn start"(即 Vite)提供(tauri.conf.json:4)。

7.2 状态:jotai 原子

前端状态用 jotai 的原子(atom)组织,而非 Recoil。以图状态为例(packages/app/src/state/graph.ts):

  • graphState = atomWithStorage('graphState', emptyNodeGraph(), storage)(:28)——当前编辑的图,自动持久化到本地存储。
  • graphMetadataState / nodesState 等是派生原子(:31,39),读写都转发到 graphState,保证单一数据源。

packages/app/src/state/ 下按领域切成几十个原子文件:execution.ts(执行)、settings.ts(设置)、trivet.ts(测试)、graphBuilder.ts(画布交互:选中、悬停、拖线)等。

7.3 画布:自研的节点-连线渲染

画布是 App 的灵魂,核心组件在 packages/app/src/components/:

组件职责源码
NodeCanvas画布容器:平移/缩放、框选、右键菜单、拖拽编排components/NodeCanvas.tsx
DraggableNode / VisualNode单个节点的可拖拽渲染components/DraggableNode.tsx
Wire / WireLayer节点端口之间的连线(贝塞尔曲线)components/Wire.tsxWireLayer.tsx
Port节点上的输入/输出端口components/Port.tsx

拖拽用的是 @dnd-kit(NodeCanvas.tsx:1DndContext),连线拖拽、画布定位等交互逻辑拆在一堆自定义 hook 里(useDraggingNodeuseDraggingWireuseCanvasPositioning 等,见 NodeCanvas.tsx 顶部 import)。整套是"reactflow 风格"的观感,但实现是自己写的,以便精确控制端口、连线校验和大图性能。


8. 核心机制五:Trivet 测试框架

这一节讲怎么给"图"写自动化测试——LLM 工作流也需要回归测试。

8.1 它要解决的小问题

你改了一张 LLM 图,怎么知道没改坏?人肉点一遍不现实。Trivet 的巧思是:用另一张 Rivet 图来断言被测图的输出——因为断言逻辑(比如"输出里应该包含某关键词""JSON 应该有某字段")本身也可能需要 LLM 判断,那干脆也用图来表达。

8.2 核心概念

概念是什么定义
TrivetTestSuite一个测试套件,绑定一张被测图和一张验证图packages/trivet/src/trivetTypes.ts:16
TrivetTestCase一条用例:input + expectedOutputtrivetTypes.ts:25
测试图 (testGraph)被测的图,吃 input 产出 outputtestSuite.testGraph 指向
验证图 (validationGraph)断言的图,吃 {input, expectedOutput, output} 产出一堆布尔testSuite.validationGraph 指向

8.3 一次测试怎么跑:runTrivet

runTrivet(packages/trivet/src/api.ts:78)对每条用例做两次图运行:

TrivetTestCase { input, expectedOutput }

▼ ① 跑被测图
runGraph(project, testGraph, input) ──▶ outputs

▼ ② 把三样喂给验证图
runGraph(project, validationGraph, {
input, expectedOutput,
output: outputs ◀── 被测图的实际输出
}) ──▶ validationOutputs(每个 graphOutput 是一个断言结果)

▼ ③ 判定
每个验证输出经 validateOutput():
boolean → 直接用;string → "true"/"TRUE" 才算真
全部为真 → 该用例 passing

对应源码:跑被测图在 api.ts:128,组装验证输入在 api.ts:135-148,跑验证图在 api.ts:152,把每个验证输出映射成结果在 api.ts:157-169

8.4 两个值得记住的细节

① 断言输出的宽松布尔约定。 validateOutput(packages/trivet/src/api.ts:22)只接受 booleanstring:字符串里只有 'true' / 'TRUE' 算真(TRUTHY_STRINGS,:20),其它类型直接抛错。这让验证图可以用一个文本节点输出 "true" 来表达"通过"。

② iterationCount:对付 LLM 的不确定性。 runTrivet 支持把每条用例跑 N 次(iterationCount,trivetTypes.ts:9),全部迭代都通过才算这条用例过(api.ts:124 的循环)。这是专门为 LLM 输出的随机性设计的——一次偶然通过不算数。

③ 测试跑图用 DummyNativeApi。 Trivet 自带的 createTestGraphRunner(api.ts:51)注入的是 DummyNativeApi(:33)——所有文件方法都抛 Method not implemented。测试环境故意不给文件系统能力,逼图保持纯粹、可复现。


9. 巧妙之处(可借鉴的技术)

  • "手脚"全接口化,一份引擎两处落地。 引擎只依赖 NativeApi/CodeRunner/MCPProvider 等接口,Node 和浏览器各给一套实现,同一个 processGraph 就能跑在服务器和桌面 App 里。落点:packages/node/src/api.ts:68?? new NodeX() 默认注入。

  • 默认拒绝任意命令执行。 NodeNativeApi.exec 直接抛 Not Implemented(packages/node/src/native/NodeNativeApi.ts:73),代码执行只走受控的 AsyncFunction(NodeCodeRunner.ts:56)。安全边界写死在代码里,而非靠文档约束。

  • 调试事件按节点限流。 partialOutput 逐 token 会打爆 WebSocket,于是按节点记时间戳限流(packages/node/src/debugger.ts:247)。一个小 if 解决序列化风暴。

  • 确定性序列化。 safe-stable-stringify 保证同图逐字节相同(serialization_v4.ts:110),让 .rivet-project 文件对 git diff 友好——图也能像代码一样做 code review。

  • 写死最新版、读时逐级回退。 写只写 v4、读 v4→v1 瀑布(serialization.ts:18,24),把"向后兼容"这件复杂的事收敛成一条清晰的 try/catch 链。

  • 用图测图。 Trivet 让断言逻辑本身也是一张 Rivet 图(packages/trivet/src/api.ts:152),把"如何判定 LLM 输出对不对"这种需要智能的判断,交回给用户用他们熟悉的画布表达。


10. 边界与局限(诚实说明)

  • exec 不可用。 Node 宿主的 NativeApi.exec 未实现(NodeNativeApi.ts:73);图里无法直接跑 shell 命令。

  • 数据集落盘要谨慎。 NodeDatasetProvidersave 选项会在每次修改后都写整个文件(NodeDatasetProvider.ts:18-19 的注释明确说"只适合小数据集")——大数据集会有性能问题。

  • 序列化最旧只到 v1;更早或损坏的文件直接 throw(serialization.ts:58),没有更细的错误恢复。

  • Trivet 断言表达力受限于布尔约定。 验证图的输出必须能归结为 boolean'true'/'TRUE' 字符串(api.ts:22),其它类型会抛错;复杂断言得在验证图内部自己消化成布尔。

  • 远程调试器默认无鉴权。 startDebuggerServer 默认 host = 'localhost'、端口 21888(debugger.ts:65),allowGraphUpload 等能力靠选项开关控制,代码本身不带身份认证——暴露到公网需自行加防护。(inferred:源码未见鉴权逻辑)


11. 横向对比(在本组各章中的位置)

本章是"外壳与工具链",与前四章的"引擎内核"互补:

讲什么与本章的关系
index.mdRivet 是什么 · 全景 · 阅读地图入口
01 节点与数据模型节点、DataValue、图模型本章序列化(§6)存的就是这些结构
02 GraphProcessor 引擎拉取式数据流引擎、processGraph本章 §3 就是给它注入 ProcessContext
03 控制流/循环/竞速control-flow-excluded本章调试器广播的 nodeExcluded 事件即来自此
04 LLM 节点与插件84 个内置节点、插件系统本章 §3.5 拉取的正是插件的环境变量
05(本章)运行时宿主、调试器、序列化、App、测试把上面一切变成可交付产品

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

主题文件路径关键符号
Node 宿主主入口packages/node/src/api.tsloadProjectFromFilecreateProcessorrunGraphgetPluginEnvFromProcessEnv
文件系统能力packages/node/src/native/NodeNativeApi.tsNodeNativeApireaddirexec(抛错)
数据集落盘packages/node/src/native/NodeDatasetProvider.tsNodeDatasetProviderfromProjectFilesave
代码节点执行packages/node/src/native/NodeCodeRunner.tsNodeCodeRunnerrunCodeAsyncFunction
MCP 连接packages/node/src/native/NodeMCPProvider.tsNodeMCPProvider#getHTTPClient#getStdioClient
引用项目加载packages/node/src/native/NodeProjectReferenceLoader.tsNodeProjectReferenceLoaderloadProject
远程调试器服务端packages/node/src/debugger.tsstartDebuggerServerattachbroadcast
远程调试器客户端packages/app/src/hooks/useRemoteDebugger.tsuseRemoteDebuggerconnectRef
CLI 入口packages/cli/src/cli.tsrunserve 命令注册
CLI servepackages/cli/src/commands/serve.tsservestreamGraphrunGraph
CLI runpackages/cli/src/commands/run.tsrun
序列化调度packages/core/src/utils/serialization/serialization.tsserializeProjectdeserializeProject
v4 序列化实现packages/core/src/utils/serialization/serialization_v4.tsprojectV4SerializerprojectV4Deserializer
Tauri 外壳packages/app/src-tauri/src/main.rstauri::Builderinvoke_handler
App 图状态packages/app/src/state/graph.tsgraphStatenodesState(jotai)
App 画布packages/app/src/components/NodeCanvas.tsxNodeCanvasWireLayerDraggableNode
Trivet 主循环packages/trivet/src/api.tsrunTrivetcreateTestGraphRunnervalidateOutput
Trivet 类型packages/trivet/src/trivetTypes.tsTrivetTestSuiteTrivetTestCaseTrivetOpts