BLOGE
0.9.8-RC1在线版 · 事实校验 2026-09-15 · English
第 5 章 —— 做决策的分支
承诺: 读完本章后,你会知道如何根据节点的输出将执行路由到不同路径——并且完全理解那些未被选中的路径会发生什么。
学习目标
- 编写一个
branch on块,根据运行时的值将执行路由到若干目标节点之一。 - 使用
otherwise在没有任何显式 case 匹配时提供默认路径。 - 解释引擎如何将未被选中的目标节点标记为 SKIPPED。
- 区分
branch on(控制面路由)与when表达式(数据面值选择)。 - 诊断缺少
otherwise、表达式类型错误、目标未声明等问题,再使用包容式分支(branch mode=inclusive on ...)让多个匹配并行运行,并解释 OR 分裂与默认 XOR 分裂的差异。
前置条件
源示例
| 文件 | 展示内容 |
|---|---|
ticket-routing.bloge | 三路分支:字符串 case 加 otherwise |
loan-approval.bloge | 在多阶段风控流水线之后做分支,含扇出、transform 和 otherwise |
boolean.bloge | 最小布尔分支 —— true / false |
otherwise.bloge | 最小的纯 otherwise 分支 |
ch05/multi-flag-approval.bloge | 包容式分支 —— 多条审查轨道根据标志并行运行 |
为什么这很重要
到目前为止,你的 graph 里每个节点都会执行。但真实工作流不是这样的:
- 信用检查 通过或拒绝 —— 你不可能两条路径都走。
- 一张工单只会路由到 一个 处理程序,而不是全部。
- 贷款决策会触发批准、拒绝、或者 人工审核。
在过程式代码里你会写 if/else 块。在 BLOGE 里等价的是 branch on —— 一种声明在 graph 内部的一等路由决策,而不是藏在 operator 代码里。引擎求值条件,只激活一个下游节点,跳过 其余所有节点。
这让路由决策在 graph 定义和 studio 工具中都清晰可见,而不是隐藏在 operator 的实现里。
心智模型
把分支想象成一个铁路道岔。一条轨道被选中;其余轨道被封锁。引擎的行为:
- 等待 源节点(
classifyPriority)执行完成。 - 从该节点的输出中读取 条件字段(
.priority)。 - 按 声明顺序 逐一测试每个 case,直到找到匹配项。
- 调度匹配的目标节点;将其余所有目标标记为
SKIPPED。
如果没有任何 case 匹配且没有 otherwise,所有 目标节点都不会运行。这几乎总是一个 bug —— 这也是为什么当可能值的集合不是穷举已知时,你应该始终包含 otherwise。
第一个可运行示例
以下是
ticket-routing.bloge
中的分支示例:
这里保留完整的 63 行 graph,因为分支结果依赖两个并行读取、一次 fan-in 和三个 已声明目标;裁掉任一部分都会隐藏读者需要追踪的控制路径。
graph ticketRouting {
node fetchCustomer : FetchCustomerOperator {
input { customerId = ctx.customerId }
timeout = 3s
retry = { attempts: 2, backoff: 200ms, strategy: exponential }
}
node fetchTicketHistory : FetchTicketHistoryOperator {
input { customerId = ctx.customerId }
timeout = 3s
}
node analyzeSentiment : AnalyzeSentimentOperator {
depends_on = [fetchCustomer, fetchTicketHistory]
input {
customer = fetchCustomer.output
history = fetchTicketHistory.output
message = ctx.message
}
timeout = 5s
retry = { attempts: 1, backoff: 500ms, strategy: exponential }
fallback = { sentiment: "neutral", score: 0.0, keywords: [] }
}
node classifyPriority : ClassifyPriorityOperator {
depends_on = [analyzeSentiment]
input {
customer = fetchCustomer.output
sentiment = analyzeSentiment.output
}
}
branch on classifyPriority.output.priority {
"vip" -> assignVipAgent
"normal" -> assignNormalAgent
otherwise -> autoResolve
}
node assignVipAgent : AssignVipAgentOperator {
depends_on = [classifyPriority]
input {
customerId = fetchCustomer.output.id
priority = "vip"
}
}
node assignNormalAgent : AssignNormalAgentOperator {
depends_on = [classifyPriority]
input {
customerId = fetchCustomer.output.id
priority = "normal"
}
}
node autoResolve : AutoResolveOperator {
depends_on = [classifyPriority]
input {
customerId = fetchCustomer.output.id
keywords = analyzeSentiment.output.keywords
}
}
}
把它当成一个故事来读:
- 两个并行拉取 —— 客户资料和工单历史。
- 情感分析 从两者扇入汇合,带有重试和 fallback。
- 优先级分类 产出
"vip"、"normal"或其他值。 - 分支 ——
branch on classifyPriority.output.priority—— 精确选中一个处理程序。如果优先级是"vip",只有assignVipAgent运行。如果是"normal",只有assignNormalAgent。其他任何值都通过otherwise落入autoResolve。
关键洞察: 三个处理程序节点都声明了
depends_on = [classifyPriority],但分支确保其中只有一个实际执行。其余节点会收到SKIPPED状态。
用两条 Case 证明 XOR,而不是只跑 happy path
排他分支同时承诺两件事:只选择一个匹配目标,另一目标不会执行。只测试 审批通过路径,只证明了合同的一半。
把未选路径也当成结果来读
| Case | createOrder | rejectOrder | 业务含义 |
|---|---|---|---|
approved=true | EXECUTED | SKIPPED | 订单已创建,拒绝代码没有运行 |
approved=false | SKIPPED | EXECUTED | 已执行拒绝,没有创建订单 |
SKIPPED 不是数据缺失,也不是失败。它是 graph 已计算分支并排除该 node 的
正向证据。因此,RC1 基础探针会同时断言
assertNodeExecuted("createOrder") 与 assertNodeSkipped("rejectOrder")。
先把第一种决策讲窄
学习时先掌握 XOR,因为一个输入只选择一条路径。只有当业务语句确实允许 多个动作同时发生,例如“通知合规并且要求补充材料”时,才使用包容式 分支。当多个独立条件共同决定结果,而且规则集合需要做覆盖分析时,再使用 决策表。它们是更强的模型,不是二选一的花哨写 法。
做一次单变量变异就能看到边界:只把 approved=true 改成 false。如果只有
选中与跳过状态交换,分支是隔离的;如果无关 node 也改变,说明决策耦合了
图中没有表达出来的其他状态。
拆开来看
branch on 块的结构
branch on <sourceNode>.output.<field> {
<value1> -> <targetNode1>
<value2> -> <targetNode2>
otherwise -> <defaultNode>
}
| 部分 | 含义 |
|---|---|
branch on | 打开条件路由块的关键字对 |
<sourceNode>.output.<field> | 一个 节点输出路径 表达式 —— 引擎在运行时读取它来做决策 |
<value> -> <target> | 一个 case:如果条件等于 <value>,就调度 <target> |
otherwise -> <target> | 默认 case:如果没有其他 case 匹配,就调度这个目标 |
Case 匹配规则
编译器为每个 case 构建一个 Predicate<Object>,采用类型宽松比较:
| Case 字面量 | 运行时匹配方式 |
|---|---|
true / false | 如果运行时值是 Boolean,直接 ==。否则,使用不区分大小写的 String.valueOf() 比较。 |
"someString" | 字符串相等,或对运行时值调用 String.valueOf() 后比较。 |
42(数字) | 如果运行时值是 Number,进行 doubleValue() 比较。否则,不匹配。 |
Case 按 声明顺序 求值 —— 第一个匹配的胜出。
布尔分支
最简单的分支使用布尔值。来自 conformance fixture
boolean.bloge:
graph g {
node a : Op {}
node b : Op {}
node c : Op {}
branch on a.output.approved {
true -> b
false -> c
}
}
如果 a.output.approved 是 true,节点 b 运行,c 被跳过。如果是 false,c 运行,b 被跳过。