Skip to main content

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

第 5 章 —— 做决策的分支

承诺: 读完本章后,你会知道如何根据节点的输出将执行路由到不同路径——并且完全理解那些未被选中的路径会发生什么。


学习目标

  1. 编写一个 branch on 块,根据运行时的值将执行路由到若干目标节点之一。
  2. 使用 otherwise 在没有任何显式 case 匹配时提供默认路径。
  3. 解释引擎如何将未被选中的目标节点标记为 SKIPPED
  4. 区分 branch on(控制面路由)与 when 表达式(数据面值选择)。
  5. 诊断缺少 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 的实现里。


心智模型

Diagram: 05-branches-that-decide figure 1

把分支想象成一个铁路道岔。一条轨道被选中;其余轨道被封锁。引擎的行为:

  1. 等待 源节点classifyPriority)执行完成。
  2. 从该节点的输出中读取 条件字段.priority)。
  3. 声明顺序 逐一测试每个 case,直到找到匹配项。
  4. 调度匹配的目标节点;将其余所有目标标记为 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
}
}
}

把它当成一个故事来读:

  1. 两个并行拉取 —— 客户资料和工单历史。
  2. 情感分析 从两者扇入汇合,带有重试和 fallback。
  3. 优先级分类 产出 "vip""normal" 或其他值。
  4. 分支 —— branch on classifyPriority.output.priority —— 精确选中一个处理程序。如果优先级是 "vip",只有 assignVipAgent 运行。如果是 "normal",只有 assignNormalAgent。其他任何值都通过 otherwise 落入 autoResolve

关键洞察: 三个处理程序节点都声明了 depends_on = [classifyPriority],但分支确保其中只有一个实际执行。其余节点会收到 SKIPPED 状态。


用两条 Case 证明 XOR,而不是只跑 happy path

排他分支同时承诺两件事:只选择一个匹配目标,另一目标不会执行。只测试 审批通过路径,只证明了合同的一半。

图:XOR Case 与 node 状态矩阵

把未选路径也当成结果来读

CasecreateOrderrejectOrder业务含义
approved=trueEXECUTEDSKIPPED订单已创建,拒绝代码没有运行
approved=falseSKIPPEDEXECUTED已执行拒绝,没有创建订单

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.approvedtrue,节点 b 运行,c 被跳过。如果是 falsec 运行,b 被跳过。

otherwise 兜底

来自 conformance fixture otherwise.bloge

graph g {
node a : Op {}
node b : Op {}
branch on a.output.status {
otherwise -> b
}
}

一个只有 otherwise 的分支意味着"无论值是什么,都调度 b"。这种写法不常见但合法 —— 在原型开发阶段可以用作占位。

SKIPPED 的含义

当引擎选中一个分支目标时,其余所有目标节点在 NodeStatus 枚举中被标记为 SKIPPED。一个被跳过的节点:

  • 永远不会执行 其 operator。
  • 不产出任何输出 —— 任何引用被跳过节点输出的下游节点将无法获得该数据。
  • GraphResult 和执行监听器中显示为 SKIPPED

包容式分支:当多条路径都需要运行时

以上所有内容描述的都是排他式分支(XOR 分裂)—— 默认行为,只有第一个匹配的分支触发,其余全部跳过。branch on 也支持包容式模式(OR 分裂),可以让所有匹配的分支并行触发。

为什么需要它

假设有一笔贷款申请需要沿着多个独立维度进行审查。在评估风险标志之后,该申请可能同时需要人工审核、合规检查以及定价审查。使用排他式分支你只能选一个——但真实的工作流需要全部同时进行。

XOR 分裂 vs OR 分裂一览

排他式(XOR 分裂,默认)包容式(OR 分裂,inclusive
触发的分支数恰好一个 —— 第一个匹配所有匹配的,并行执行
otherwise 触发时机没有显式 case 匹配时零个 分支匹配时
未匹配的目标标记为 SKIPPED标记为 SKIPPED
DSL 关键字branch on ... { }branch mode=inclusive on ... { }
Java API.branch().on().when()....branch().on().inclusive().when()...

DSL 语法

on 前设置 mode=inclusive

branch mode=inclusive on assessFlags.reviewFlags {
"NEEDS_REVIEW" -> manualReview
"NEEDS_COMPLIANCE" -> complianceReview
"NEEDS_PRICING" -> pricingReview
otherwise -> autoApprove
}

如果 assessFlags.reviewFlags 同时包含 "NEEDS_REVIEW""NEEDS_PRICING"manualReviewpricingReview 都会并行运行,而 complianceReview 被跳过。otherwise 目标(autoApprove)仅在没有任何 case 匹配时才运行。

Java API

BranchBuilder 上使用 .inclusive() 方法:

graph.branch("assessFlags")
.on("reviewFlags")
.inclusive() // 启用 OR 分裂
.when(v -> v instanceof List<?> l
&& l.contains("NEEDS_REVIEW"), "manualReview")
.when(v -> v instanceof List<?> l
&& l.contains("NEEDS_COMPLIANCE"), "complianceReview")
.when(v -> v instanceof List<?> l
&& l.contains("NEEDS_PRICING"), "pricingReview")
.otherwise("autoApprove");

底层实现是将 Edge.Conditionalinclusive 字段设为 true

完整示例 —— 多标志审批

参见 ch05/multi-flag-approval.bloge 获取完整 graph。关键流程:

  1. fetchApplication 获取贷款申请。
  2. assessFlags 分析风险并产出审查标志列表。
  3. 包容式分支 扇出到每条匹配的审查轨道:
    • "NEEDS_REVIEW"manualReview
    • "NEEDS_COMPLIANCE"complianceReview
    • "NEEDS_PRICING"pricingReview
  4. 如果标志列表为空,otherwise 路由到 autoApprove

由于分支是包容式的,单笔申请可以同时触发两条甚至全部三条审查轨道。

何时使用包容式 vs 排他式

使用排他式(默认)的场景使用包容式的场景
恰好只有一个结果适用(批准 / 拒绝 / 延期)多个独立的结果可以同时适用
条件是单值枚举或布尔值条件是一个集合或标志位的位掩码
你需要第一个匹配短路的语义你需要向所有匹配的下游节点扇出

otherwise 行为差异

  • 排他式: 当没有显式 case 匹配时 otherwise 触发 —— 它充当兜底默认值。
  • 包容式: otherwise 零个 case 匹配时触发。如果至少有一个 case 匹配了,即使其他 case 没有匹配,otherwise 也会被跳过。

当你需要一个总是运行的节点 —— upstream_policy = ALL_RESOLVED

默认调度规则是:节点只在 至少一个上游 COMPLETED 时才运行。这对普通的 fan-in 来说是对的,但有些节点故意挂在分支下游,无论走的是哪条路径都需要运行 —— 例如:

  • 一个 结果记录器,记录哪条路径胜出
  • 一个 清理 节点,在成功路径和失败路径之后都要跑
  • 一个 指标 收集器,不关心业务结果

如果你写 depends_on = [branchTargetA, branchTargetB, branchTargetC],而 分支只选中了其中一条路径,其他几个节点就是 SKIPPED —— 于是你的记录器 也会被 SKIPPED,因为它没有任何 COMPLETED 的上游。

upstream_policy = ALL_RESOLVED 是告诉引擎:"只要我的 所有 上游都到达 终态(包括 SKIPPEDCANCELLED),就启动我。" 这里的语义是"等待所有上游 resolve(终结)",而不是"等待所有上游成功"。

node recordOutcome : RecordOutcomeOperator {
depends_on = [assignVipAgent, assignNormalAgent, autoResolve]
upstream_policy = ALL_RESOLVED
input {
// SKIPPED 的上游没有 output —— 搭配安全导航操作符
vip = assignVipAgent.output?.agentId ?? null
normal = assignNormalAgent.output?.agentId ?? null
auto = autoResolve.output?.ticketId ?? null
}
}
.node("recordOutcome", new RecordOutcomeOperator())
.dependsOn("assignVipAgent", "assignNormalAgent", "autoResolve")
.upstreamPolicy(UpstreamPolicy.ALL_RESOLVED)

小测验: upstream_policy = ALL_RESOLVED 和 "只把 recordOutcome 接在那条实际运行的路径下游" 的差别在哪里?提示:想想你要画几条边,以及 加上第四条分支时会发生什么。


常见陷阱

❌ 对非输出表达式做分支

// WRONG — branch condition must be a node output path
branch on ctx.priority {
"high" -> fastTrack
otherwise -> normalQueue
}

编译器要求条件表达式是一个 节点输出路径(或 transform 字段路径)。你不能直接对 context 值做分支。如果需要根据 context 数据进行路由,可以引入一个节点或 transform 将该值暴露为输出:

// CORRECT — use a transform to expose the context value
transform routing {
priority = ctx.priority
}

branch on routing.priority {
"high" -> fastTrack
otherwise -> normalQueue
}

❌ 对开放值集合遗漏 otherwise

// DANGEROUS — what if decision is "review"?
branch on makeDecision.output.decision {
"approved" -> approveLoan
"rejected" -> rejectLoan
}

如果 makeDecision 返回 "review",没有 case 匹配,没有任何目标节点会运行。当可能值的集合不是穷举已知时,始终应包含 otherwise

// SAFE
branch on makeDecision.output.decision {
"approved" -> approveLoan
"rejected" -> rejectLoan
otherwise -> manualReview
}

❌ 混淆 branch onwhen

branch onwhen 共享箭头语法(->)和 otherwise,但它们是 完全不同 的:

branch onwhen
所属层面控制面 —— 决定哪些 节点 运行数据面 —— 决定一个 字段 取什么值
出现位置graph 主体,顶层表达式内部(input binding 或 transform)
效果激活一个节点,跳过其余产出一个值

当你只需要计算一个值时,不要用 branch on,应该使用 when

❌ 混淆包容式分支与并行执行

// 错误假设 —— 这仍然是排他式分支!
branch on classifyRisk.level {
"HIGH" -> escalate
"MEDIUM" -> review
otherwise -> autoApprove
}

在其他位置增加并发并不会让一个排他式分支“并行运行”。没有 mode=inclusive 时,只有第一个匹配的 case 触发。如果需要多个分支同时触发,必须在 branch 块上显式设置 mode=inclusive

// 正确 —— mode=inclusive 启用 OR 分裂
branch mode=inclusive on assessFlags.reviewFlags {
"NEEDS_REVIEW" -> manualReview
"NEEDS_COMPLIANCE" -> complianceReview
otherwise -> autoApprove
}

还要注意:包容式分支适用于条件值可以匹配多个 case 的情况(例如标志列表)。如果条件是一个单值字段(如字符串枚举),包容式和排他式的行为完全相同——此时优先使用排他式以保持清晰。

❌ 把 branch on 当决策表用

branch on 适合路由——决定哪些下游节点运行。当你用它做分类策略查找时,结构很快会变别扭:一组输入本来只是要映射到一个输出值,却被拆成了多个分支目标节点。

这个味道很好识别:每个分支目标存在的唯一目的,就是返回同一种 shape 的结果。

// 别扭 —— 只是为了分类一个值,却创建了一组路由节点
branch on creditScore.output.band {
"platinum" -> assignPlatinumTier
"gold" -> assignGoldTier
otherwise -> rejectApplication
}

这种设计会带来三个问题:

  1. 输出分散。 结果散落在多个节点输出路径里,而不是一个稳定位置。
  2. 多匹配语义很弱。 branch on 可以选一个臂或扇出多个臂,但很难清晰表达“收集所有匹配规则并返回它们”。
  3. 缺少策略 lint。 工具无法警告规则集不完备,也无法指出两条策略行冲突。

当逻辑本质是一张规则表时,使用 decision_table

graph creditScreening {
decision_table credit_tier(score = applicant.output.score) hit=first -> String {
rule (score: score >= 750) -> "platinum"
rule (score: 680 <= score < 750) -> "gold"
otherwise -> "rejected"
}
}

无论以后增加多少规则,结果始终在 credit_tier.output.value

需求优先选择
将执行路由到一个或多个下游节点branch on
把输入分类成一个值decision_table hit=firsthit=unique
允许多条规则命中,但要求输出一致decision_table hit=any
返回所有匹配结果decision_table hit=collect

完整语法、命中策略、lint 规则和错误码见附录 F —— 决策表。第 9 章展示 decision-table/missing-otherwisedecision-table/collect-otherwise 的 lint 实战,第 34 章则把这个陷阱做成了动手练习。


常见故障

缺少 otherwise 且出现意外值

branch on makeDecision.output.decision {
"approved" -> approveLoan
"rejected" -> rejectLoan
}

如果 makeDecision 返回 "review",就不会有任何 case 命中:

GraphResult {
status: COMPLETED,
nodeStatuses: {
approveLoan: SKIPPED,
rejectLoan: SKIPPED
}
}

Graph 会完成,但 没有任何下游工作真正发生。任何依赖 approveLoanrejectLoan 输出的节点都会拿不到数据。

修复: 加上 otherwise -> manualReview,确保至少总有一条分支会被激活。


引导式重写

看一下完整的 loan-approval.bloge 示例。它并行运行四项风险检查,聚合结果,然后做分支:

branch on makeDecision.output.decision {
"approved" -> approveLoan
"rejected" -> rejectLoan
otherwise -> manualReview
}

思考以下问题:

  1. 分支之前有多少个节点运行? 数一数:fetchApplication,然后四个并行检查(checkCreditdetectFraudverifyIncomecheckBlacklist),然后 aggregateRisk,然后 makeDecision —— 总共七个节点。
  2. 如果 makeDecision 返回 "escalated" 会怎样? otherwise case 捕获它并路由到 manualReview
  3. approveLoanrejectLoan 能同时运行吗? 不能 —— 只有一个分支目标会被选中。
  4. riskSummary transform 与分支的关系是什么? 它将上游输出投影用于审计日志。因为它不依赖 makeDecision,可以独立解析 —— 但它对选择哪条分支路径没有任何影响。

现在尝试自己修改这个 graph:

  • 添加第四个 case:"deferred" -> deferApplication
  • 声明新的 deferApplication 节点,使用 DeferApplicationOperator,依赖 makeDecision
  • 预测:当决策为 "deferred" 时,其他三个目标节点会怎样?

脑力检查

  1. 用什么关键字对打开条件路由块? branch on。)
  2. branch on 后面必须跟什么类型的表达式? (节点输出路径表达式,如 nodeId.output.field,或 transform 字段路径,如 transformId.field。)
  3. 如果一个分支有三个 case 但没有 otherwise,当没有任何 case 匹配时会怎样? (没有目标节点运行 —— 所有目标都被跳过。)
  4. 在同一次 graph 运行中,使用默认(排他式)branch on 时,两个分支目标能否同时执行? (不能 —— 每次排他式 branch on 求值只选中一个目标。)
  5. branch onwhen 的区别是什么? branch on 是控制面路由,决定哪些节点运行。when 是数据面表达式,决定一个字段取什么值。)
  6. 设计题: 如果一个 graph 里有两个互不依赖的功能都需要分支,能不能在同一个 graph 中声明两个独立的 branch on 块?如果可以,引擎如何处理它们?(可以。每个 branch on 都会在其源节点完成时独立求值,彼此互不干扰。这在复杂 workflow 中很常见,因为多个决策往往分布在不同的并行路径上。)
  7. 你需要给 branch on 添加什么关键字使其变为包容式(OR 分裂)分支?otherwise 的行为有何不同? (在条件表达式后添加 inclusive。在包容式分支中,otherwise 仅在零个 case 匹配时触发;而在排他式分支中,当没有显式 case 匹配时就会触发。)

练习

  1. 在编辑器中打开 ticket-routing.bloge
  2. 添加第四个处理程序节点,名为 escalateToManager,使用 EscalateToManagerOperator。它应依赖 classifyPriority,并将 fetchCustomer.output.idanalyzeSentiment.output 作为输入。
  3. 在分支中添加一个新 case"critical" -> escalateToManager
  4. 画出每种可能优先级值的执行流程:
    • "vip" → 哪个节点运行?
    • "critical" → 哪个节点运行?
    • "normal" → 哪个节点运行?
    • "unknown" → 哪个节点运行?(提示:otherwise。)
  5. 加分项: 把分支改写成只使用 otherwise(不带任何显式 case)。这对 graph 的行为意味着什么?这样做有用吗?

实验验收卡

  • 预期与观察: 两个 XOR Case 交换 EXECUTED 与 SKIPPED。
  • 失败与恢复: 让所有条件都不匹配;修复阈值或 otherwise。
  • 证明边界: 只证明给定输入的分支选择,不证明全部业务边界。
  • 练习合同: 批准与拒绝 Case;只改风险阈值;交付状态矩阵;每个 Case 恰选一条路径即停止。

回顾

  • branch on控制面路由 —— 它决定哪些下游节点执行、哪些被跳过。
  • 条件必须是 节点输出路径transform 字段路径 表达式。
  • Case 使用 类型宽松比较,并按 声明顺序 测试 —— 第一个匹配的胜出。
  • otherwise默认 case。当可能值的集合不是穷举已知时,始终应包含它。
  • 未被选中的目标节点被标记为 SKIPPED —— 它们永远不会执行,也不产出任何输出。
  • branch on(控制面)和 when(数据面)共享语法风格,但在 语义上完全不同。用 branch on 路由执行;用 when 计算值。
  • 包容式分支branch mode=inclusive on ...)是 OR 分裂:所有匹配的分支并行触发。当条件可以同时匹配多个 case 时使用(例如标志列表)。otherwise 仅在零个 case 匹配时触发。

下一步

第 6 章 —— 韧性设计里,你会学习如何在节点上声明 timeout、retry 和 fallback 策略 —— 让你的 graph 保持可靠,而不必在 operator 代码里堆砌基础设施逻辑。


参考链接

Coding Agent: Open the versioned task guide.