Skip to main content

BLOGE 0.9.8-RC1 在线版 · 事实校验 2026-09-15 · English

第 2 章 —— 你的第一个 Graph

本章通过一个最小可运行示例说明 BLOGE graph 的编写、编译与执行过程,包括 DSL 方式和 Fluent Java API 方式。


学习目标

  1. 写出一个能够被编译器接受的最小 .bloge 文件。
  2. 从 Java 里加载并执行这个文件。
  3. 用 Fluent Java API 构建同一个 graph。
  4. 理解 ctxinputoutputtimeout 在运行时分别意味着什么。

前置条件

源示例

文件展示内容
hello-world.bloge最小可运行示例:一个 graph、一个 node、一个输入绑定
two-node-chain.bloge两个节点组成的链路,演示显式依赖
input-bindings.blogeConformance fixture:最基础的输入绑定语法

为什么这很重要

第 1 章已经交代了 BLOGE 的定义、核心特征,以及它为何不同于手写编排。本章继续把这些概念落到一个最小可运行示例上,使 graph 的编写、装载和执行过程都落到具体结果上。

一个单节点 graph 已经足以把四个最基础的运行时概念放到同一条执行路径里:

  • 数据如何进入 graph(context)。
  • node 如何拿到数据(input block)。
  • 引擎如何调用你的代码(operator)。
  • 执行完成后如何读结果。

后面的所有内容,都会建立在这四个概念之上。


环境准备

如果你想动手跟着本章做,请使用现在与本书同仓库的 companion 项目。

  1. 先安装 standalone companion module 依赖的核心 BLOGE artifact:

    mvn -q -pl bloge-core,bloge-dsl,bloge-test -am install -DskipTests -Dspotbugs.skip=true
  2. 运行 companion 示例测试:

    mvn -f head-first-bloge-examples/pom.xml test
  3. 打开第 2 章 companion 文件:

不要只读本章片段。最好把本书和 companion 项目并排打开,边看边运行,这样例子始终和可执行的东西绑在一起。

项目布局

Diagram: 02-your-first-graph figure 1

每一章的 .bloge 文件都放在 src/main/resources/bloge/chNN/ 下。src/test/java/ 里的测试类会编译或执行这些文件。随着你继续阅读本书,记得运行对应章节的 companion 示例,让理解始终落到真实产物上。


心智模型

Diagram: 02-your-first-graph figure 2

  1. 你先创建一个 GraphContext,把初始值放进去。
  2. 引擎解析 input bindings —— 比如 ctx.message —— 从 context(或上游输出)取值。
  3. 每个 node 的 operator 收到组装好的输入,执行逻辑并返回输出。
  4. 执行结束后,你从 GraphResult 里读取结果。

第一个可运行示例

DSL 写法

这就是 hello-world.bloge 的内容:

graph helloWorld {

node echo : EchoOperator {
input {
message = ctx.message
}
timeout = 1s
}
}

这已经是一个完整且合法的 BLOGE graph。逐项拆开看:

语法含义
graph helloWorld声明一个名为 helloWorld 的 graph
node echo声明一个名为 echo 的节点
: EchoOperator指定这个节点执行的 operator
input { message = ctx.message }把 context 里的 message 绑定到节点输入字段 message
timeout = 1s节点必须在 1 秒内完成

从 Java 里加载并运行它:

// 1. 注册你的 operator
OperatorRegistry registry = new DefaultOperatorRegistry();
registry.register("EchoOperator", (input, opCtx) -> input);

// 2. 加载 DSL 文件
GraphLoader loader = new GraphLoader(registry);
Graph graph = loader.load(Path.of("src/main/resources/bloge/hello-world.bloge"));

// 3. 构造上下文
GraphContext ctx = new GraphContext();
ctx.put("message", "hello");

// 4. 执行
GraphEngine engine = GraphEngine.builder().registry(registry).build();
GraphResult result = engine.execute(graph, ctx);

Java API 写法

同一个 graph,也可以用 Fluent Java API 构造:

Graph graph = Graph.builder("helloWorld")
.node("echo", (Map<String, Object> input, OperatorContext opCtx) ->
Map.of("message", opCtx.graphContext().get("message", String.class)))
.timeout(Duration.ofSeconds(1))
.build();

GraphContext ctx = new GraphContext();
ctx.put("message", "hello");

GraphEngine engine = GraphEngine.builder().build();
GraphResult result = engine.execute(graph, ctx);

两种写法最终生成的是同一个运行时 graph。DSL 更声明式、更适合外部编排;Java API 更适合在代码里做类型安全组合。很多团队会先用 DSL 做原型,再把稳定下来的 operator 逻辑收进 Java 类。


先读懂三种结果,再增加配置

第一个 graph 不能以“能够编译”作为终点。你还应该能从一份很小的结果里 看出:执行是否成功,以及两种常见失败停在哪个阶段。这样,后面的 Builder 选项才是在解决问题,而不是一份需要背诵的 API 清单。

图:一次成功与两个失败阶段

结果 1 —— 确实拿到了值

RC1 基础探针以 message=hello 运行 hello-world,并检查结果,而不是把 “没有抛异常”当成成功:

graph: hello-world status: SUCCESS
node: echo status: EXECUTED
output.message: hello

这里有三个独立事实:graph 已结束、echo 确实执行、输出值符合预期。graph 变大以后,这三类断言仍应保留。

结果 2 与 3 —— 执行根本没有开始

阶段可观察结果修复最小 owner
编译node 没有 Operator 绑定,诊断指向声明位置修正 .bloge node 声明
加载registry 中不存在 MissingOperator注册能力或修正名称
执行已绑定的 Operator 返回或抛错检查 node 状态、输出与失败证据

编译失败和加载失败都应满足 operator calls = 0。重试 graph 无法修复这两类 问题,因为业务代码从未被派发。

到这里,Builder 选项才有了落点:listener 观察阶段,interceptor 包裹真实 调用,并发参数改变调度,checkpoint store 改变执行状态的保留方式。先看到 问题,再选择拥有这个问题的配置项。


拆开来看

加一个第二节点

看一下 two-node-chain.bloge

graph twoNodeChain {

node normalizeName : NormalizeNameOperator {
input { name = ctx.name }
timeout = 1s
}

node buildGreeting : BuildGreetingOperator {
depends_on = [normalizeName]
input { value = normalizeName.output.value }
timeout = 1s
}
}

新增了两个概念:

概念含义
depends_on = [normalizeName]显式依赖 —— buildGreeting 必须等待 normalizeName
normalizeName.output.value读取上游节点输出里的 value 字段

注意:即使不写 depends_on,由于 buildGreeting 的输入里引用了 normalizeName.output.value,编译器也会自动推断出这个依赖。显式声明只是让意图更清楚。

Conformance 基线

Conformance 测试套件里有一个最小输入绑定示例 (input-bindings.bloge):

graph g {
node a : Op {
input {
x = ctx.foo
y = "bar"
}
}
}

它说明 input 右侧既可以是 context 引用(ctx.foo),也可以是字面量("bar")。

只在观察到问题后增加配置

不要把所有 Builder 选项复制进第一个 graph。先保留下面的路由表,等故事走到 对应问题时再回来:

观察到的需要配置 owner 与后续章节
拒绝不兼容的输入输出形状Graph.builder().schemaValidation(...).contracts(...) —— 第 8 章
观察或包裹每次调用GraphEngine.builder().listeners(...).interceptors(...) —— 第 19 章
限制同时运行的工作量.maxGlobalConcurrency(...) —— 第 21 章
保留或迁移执行状态.executionCheckpointStore(...).checkpointCodec(...).versionMismatchPolicy(...) —— 第 13 章
解析租户范围与准入.tenantContextResolver(...).tenantResourcePolicy(...) —— 第 21 章
替换调度时间实现.schedulerTimerSupport(...) —— 第 11—12 章

Graph.builder() 描述一个 graph;GraphEngine.builder() 配置可以运行多个 graph 的 runtime。说不清要解决哪个已观察问题时,就保留默认值。


常见陷阱

❌ 忘了写 operator 绑定

// 错误 —— 编译器需要知道节点要运行哪个 operator
node echo {
input { message = ctx.message }
}

每个 node 都必须通过 : OperatorName 的形式声明 operator。否则编译器根本不知道该调哪段逻辑。

// 正确
node echo : EchoOperator {
input { message = ctx.message }
}

常见故障

缺少 operator 绑定 —— 编译器错误

如果你忘了写 operator 类型:

node echo {
input { message = ctx.message }
}

解析器会立刻停下来,报出的核心信息是:

GraphDefinitionException: Expected ':' after node id

如果你保留了冒号,但后面仍然没有 operator 名称,接下来还会看到 Expected operator reference

修复: 每个 node 都必须写成 : OperatorName

operator 未注册 —— 加载阶段错误

如果 DSL 里引用了注册表中不存在的 operator:

registry.register("EchoOperator", (input, opCtx) -> input);
Graph graph = loader.load(Path.of("hello-world.bloge"));
// 但 DSL 里写的是 "ShoutOperator"

那么在加载 graph 时就会失败,还没进入执行阶段:

GraphDefinitionException: Node 'shout' references unregistered operator 'ShoutOperator'

修复:load() 之前,把 .bloge 文件里出现的每个 operator 名称都注册好。


引导式重写

hello-world 这个 graph 出发,继续往前走一步:

  1. 新增一个名为 shout 的节点,把 echo 的输出转成大写。
  2. 给它设置 timeout = 2s
  3. 预测执行顺序。 哪个节点先运行?为什么?

你的 DSL 大概会长这样:

graph helloWorld {

node echo : EchoOperator {
input { message = ctx.message }
timeout = 1s
}

node shout : ShoutOperator {
input { message = echo.output.message }
timeout = 2s
}
}

因为 shout 引用了 echo.output,引擎会自动推断依赖,所以必然是 echo 先执行,再轮到 shout


脑力检查

  1. 要执行一个 graph,最少需要哪四样东西?(Graph、OperatorRegistry、GraphContext、GraphEngine。)
  2. 如果某个节点的输入绑定引用了 ctx.userId,这个值来自哪里?(来自你在执行前构造的 GraphContext。)
  3. 同一个 input block 里能不能同时写字面量和上游引用?(可以——Conformance fixture 里 x = ctx.fooy = "bar" 就是同一个 block。)
  4. 你在 workshop 里勾画一个 workflow,想要最快的“改了就能跑”反馈循环。你会先用 DSL 文件还是 Fluent Java API?什么时候会切换到另一种?(多数团队先用 DSL,因为 graph 的形状更直观、改起来更快。当你需要更强的代码复用、更紧的类型约束,或更深的应用级组合时,再切换到 Java API。)
  5. 设计题: 如果一个 graph 有十个节点,但只有两个节点真的需要从 context 取数据,是不是每个节点都必须写 input block?(不是。只有消费外部数据或上游输出的节点才需要 input。没有 input block 的节点会收到一个空输入 map。)

练习

  1. 新建一个 my-first-graph.bloge 文件,定义一个 graph greet,包含:
    • 一个 fetchName 节点,读取 ctx.name
    • 一个 buildGreeting 节点,读取 fetchName.output.name 并生成问候语。
  2. 在 Java 里注册这两个 operator,加载这个文件,用 ctx.put("name", "Alice") 执行 graph,并打印结果。
  3. 通过 GraphResult 验证 buildGreeting 是否确实在 fetchName 之后执行。

项目环境与运行细节可参考入门指南


实验验收卡

  • 预期与观察: 最小 graph 返回 SUCCESS,节点为 EXECUTED,最终 output 可读取。
  • 失败与恢复: 拼错 operator 名得到 compile/load error;恢复名称后重跑。
  • 证明边界: 只证明最小装配和结果读取,不证明重试、持久化或业务正确性。
  • 练习合同: hello graph;只改一个 operator 标识;交付成功和失败输出;两者可重复即停止。

回顾

  • 一个最小 BLOGE graph 需要有 graph 块、至少一个带 operator 绑定的 node,以及对应的 input
  • GraphContext 负责携带初始数据;input bindings 负责把数据送进节点。
  • Fluent Java API.bloge DSL 最终构造的是同一种运行时 graph。
  • 依赖关系既可以是显式的(depends_on),也可以通过 .output 自动推断出来。

下一步

第 3 章 —— 用依赖关系思考进一步展开依赖图调度,以及“按依赖思考,而不是按顺序思考”为什么是 graph 设计的关键。


参考链接

Coding Agent: Open the versioned task guide.