Skip to main content

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

第 34 章 —— 实验

承诺: 读完本章后,你将完成基础、进阶、毕业三级 capstone。最终作品不只包含 graph,还包含技术测试、业务 Scenario/Policy、证据解读、架构图和“尚未证明事项”。


学习目标

  1. 在限定范围内从 starter 演进 graph,而不是从空文件猜语法。
  2. 分别交付 graph contract、business contract 和 release boundary 的证据。
  3. 对 effect、等待、恢复和 fallback 做单因素失败实验。
  4. 用一张架构图解释运行边界,用一张 receipt 解读图说明证据边界。
  5. 在完成声明中同时列出已证明、未证明和必须由人批准的事项。

前置条件

前面全部三十三章。这些实验假设你已经熟练掌握:

源示例

文件展示内容
order-process.bloge基础 capstone 起点——扇出、汇合、分支、韧性
ticket-routing.bloge进阶 capstone 起点——情感分析与多路分支
loan-approval.bloge毕业 capstone 起点——贷款业务与验证资产
batch-order-parallel.bloge可选题——foreach 并行处理每一项
status-polling.bloge可选题——loopuntilloopIteration
payment-wait.bloge可选题参考——await 事件关联、超时和分支
order-saga.bloge毕业题参考——saga 补偿模式
GraphTestRunner.java每个实验验证步骤使用的测试工具
MockOperator.java测试替身——returningthrowingdelayingrecording
DslTestHelper.javaDSL 编译 + expectParseError / expectCompileError
missing-timeout.bloge反模式参考——节点没有超时预算
over-broad-fallback.bloge反模式参考——降级范围过宽,掩盖了真实的故障模式

为什么这很重要

阅读 graph 的知识和亲手设计 graph 是两回事。前面每一章都只引入一个概念。而真实的工作流会同时组合所有概念:

  • 一个订单管道需要扇出 韧性 分支。
  • 一个工单路由器需要情感分析 三路分支 对不稳定 ML 服务的降级。
  • 一个批处理器需要 foreach 下游聚合 对每一项结果的测试断言。

这些实验用于检验前述概念能否在实践中组合。每个实验都从仓库中已有的一致性测试 fixture 开始,因此不需要凭空发明语法;需要完成的动作是阅读、扩展和测试。


心智模型

把三级 capstone 想象成一段逐层扩大的责任:

Diagram: 34-labs figure 1

阅读 —— 打开一致性测试 fixture。追踪每一个依赖。识别哪些节点并行运行、哪些节点需要等待。

扩展 —— 添加实验要求的功能。将上游输出接入下游输入。有意识地选择韧性策略。

验证 —— 使用 GraphTestRunnerMockOperator 执行 graph,断言 NodeStatus 值,并检查执行顺序。

图:三级 capstone 的责任递进

级别预计投入起始文件工作合同完成标准
基础60—90 分钟order-process.bloge一份 DSL 副本 + 一组 graph tests;给出明确步骤成功/拒绝两路状态和输出可复核
进阶2—3 小时ticket-routing.blogeDSL、mock、listener 测试;只给边界提示fallback 与观测事实可区分
毕业1—2 天loan starter + 验证资产graph、tests、Scenario/Policy/Fixture、图与报告;只给验收合同可运行、可解释、可复核,并写明未证明事项

每一级都先复制只读 starter 到自己的工作目录。不要修改 submodule/examplessubmodule/bloge;它们分别是远端示例基线和 RC1 事实源。


第一个可运行示例

在开始实验之前,确保你能运行最简单的测试驱动 graph 循环。这是你的热身。

// 1. 编译 DSL
var helper = new DslTestHelper(registry);
Graph graph = helper.compile("""
graph warmup {
node greet : GreetOperator {
input { name = ctx.name }
}
}
""");

// 2. 接入 mock operator
var runner = new GraphTestRunner(Map.of(
"GreetOperator", MockOperator.returning(Map.of("message", "Hello"))
));

// 3. 执行
var ctx = new GraphContext(Map.of("name", "Lab Runner"));
GraphResult result = runner.execute(graph, ctx);

// 4. 断言
assert result.isSuccess();
runner.assertNodeExecuted("greet");

如果编译通过并且断言成功,你就准备好了。


拆开来看

每个实验都遵循相同的结构。以下是每个阶段练习的技能:

阶段练习的技能对应章节
阅读 fixture依赖追踪、并行识别第 3 章
添加节点Operator 绑定、输入接线、depends_on第 2 章第 4 章
添加韧性retrytimeoutfallbackcompensate第 6 章
添加分支branch onotherwise、跳过节点推理第 5 章
添加迭代foreachloopcarryuntil第 11 章
添加挂起await、事件关联、on_timeout第 12 章
编写测试GraphTestRunnerMockOperatorDslTestHelper第 18 章

常见陷阱

❌ 给每个节点都添加韧性策略

一个常见的冲动是在 graph 中的每个节点上都加上 retry + timeout + fallback。别这样做。

看看 over-broad-fallback.bloge 中的反模式:在 authorizePayment 上使用全覆盖的 fallback,把校验错误和瞬时超时都藏在同一个"人工审核"结果后面。下游节点无法区分合理的降级和永久性 bug。

再看看 missing-timeout.bloge:一个调用外部服务却没有任何 timeout 的节点,可能让整个 graph 无限期停滞。

实验中的经验法则:

  • 外部调用 → 必须加 timeout
  • 瞬时不稳定的调用 → 加 retry 并且每次尝试带 timeout
  • 只有在下游每个消费者都能真正接受部分数据时,才添加 fallback
  • 纯本地计算(转换、聚合)→ 不需要韧性策略。

引导式重写

在进入正式实验之前,先用一个小改动练习扩展-验证的循环。

打开 order-process.bloge。当前 createOrder 仅依赖 calcPrice

node createOrder : CreateOrderOperator {
depends_on = [calcPrice]
input {
user = fetchUser.output
price = calcPrice.output
}
}

改写任务: 业务现在要求 createOrder 也记录授信检查的结果以便审计——但只在订单被批准时。添加一个新的输入绑定:

node createOrder : CreateOrderOperator {
depends_on = [calcPrice, checkCredit]
input {
user = fetchUser.output
price = calcPrice.output
creditCheck = checkCredit.output
}
}

现在验证这个修改是安全的:

var runner = new GraphTestRunner(Map.of(
"FetchUserOperator", MockOperator.returning(Map.of("id", "u1")),
"FetchProductsOperator", MockOperator.returning(Map.of("items", List.of())),
"CalcPriceOperator", MockOperator.returning(Map.of("total", 99.0)),
"CreditCheckOperator", MockOperator.returning(Map.of("approved", true)),
"CreateOrderOperator", MockOperator.recording(),
"RejectOrderOperator", MockOperator.recording()
));

var ctx = new GraphContext(Map.of(
"userId", "u1",
"productIds", List.of("p1")
));

GraphResult result = runner.execute(graph, ctx);
assert result.isSuccess();
runner.assertNodeExecuted("createOrder");
runner.assertNodeSkipped("rejectOrder");

这个模式——扩展 graph,然后编写测试证明新接线正确——就是这些实验要培养的核心能力。


脑力检查

在开始实验之前,不看回顾地回答以下问题:

  1. ticket-routing.bloge 中, 哪些节点并行运行?(答案:fetchCustomerfetchTicketHistory——两者之间没有依赖。)

  2. loan-approval.bloge 中, fetchApplication 完成后的最大并行度是多少? (答案:四——checkCreditdetectFraudverifyIncomecheckBlacklist 都只依赖 fetchApplication。)

  3. batch-order-parallel.bloge 中, summarize 能否在所有 foreach 迭代完成之前启动? (答案:不能——summarize 依赖 processOrders,而 processOrders 只有在所有迭代完成后才算完成。)

  4. 你会使用哪个 MockOperator 工厂方法来模拟一个耗时 500 毫秒然后成功的节点? (答案:MockOperator.delaying(Duration.ofMillis(500), output)。)

  5. 哪个 GraphTestRunner 方法可以证明一个分支目标没有被选中? (答案:assertNodeSkipped("nodeId")。)


实验

基础 capstone —— 读懂并扩展订单 graph

预计投入: 60—90 分钟
起始文件: order-process.bloge,复制后修改
允许范围: 一份 DSL 副本、一组 GraphTestRunner 测试;不得修改 operator 实现
提示层级: 明确步骤

需求:

  1. 添加一个 fetchInventory 节点,使其与 fetchUserfetchProducts 并行运行。输入:ctx.productIds。超时:2s
  2. fetchInventory.output 作为新的 inventory 输入接入 calcPrice。相应更新 depends_on
  3. createOrder 添加一个 compensate 块,调用 ReleaseInventoryOperator——模式参考 order-saga.bloge
  4. 验证: 编写一个测试,断言 fetchInventoryfetchUserfetchProducts 全部完成;当授信通过时 createOrder 执行;rejectOrder 被跳过。

完成标准: DSL 真实编译;批准与拒绝两路分别断言 EXECUTED/SKIPPED; 库存超时只改变预期节点;写出一句“本实验没有证明业务规则正确”。


进阶 capstone —— 让工单降级可观察

预计投入: 2—3 小时
起始文件: ticket-routing.bloge,复制后修改
允许范围: DSL、mock operator、一个 listener 测试;不得接真实工单系统
提示层级: 只给边界,不给完整 DSL

需求:

  1. analyzeSentiment 已经有了 fallback。给 fetchTicketHistory 添加 retry = { attempts: 2, backoff: 300ms, strategy: jitter },使其能够承受历史服务的瞬时故障。
  2. 添加一个 escalateToManager 节点,作为 classifyPriority.output.priority == "critical" 时的第四个分支目标。输入:来自 fetchCustomer.output.idcustomerId 和来自 analyzeSentiment.outputsentiment
  3. 验证: 编写一个测试,其中 classifyPriority 返回 { priority: "critical" },断言 escalateToManager 执行,而 assignVipAgentassignNormalAgentautoResolve 全部被跳过。

完成标准: critical、normal、fallback 三条路径可复核;同一 executionId 下能区分 metric、log 与 node status;不能从 fallback 成功推出业务成功。


毕业 capstone —— 交付可复核的贷款变更

预计投入: 1—2 天
起始文件: loan-approval.bloge 与同模块 src/test 中的 Scenario/Policy/Fixture 测试资产,复制后修改
允许范围: graph、受控 operator test doubles、Scenario、Policy、Fixture、证据解读与图;不得替业务 Owner 批准 GOLDEN,不得调用真实放款 effect
提示层级: 只给验收合同

需求:

  1. aggregateRiskmakeDecision 之间添加一个 reserveFunds 节点。reserveFunds 依赖 aggregateRisk,输入为 application = fetchApplication.outputrisk = aggregateRisk.output
  2. reserveFunds 声明 output { reservationId: String } 并添加一个调用 ReleaseFundsOperatorcompensate 块——与 order-saga.bloge 中的 saga 模式相同。
  3. 更新 makeDecision,使其也 depends_on reserveFunds
  4. reserveFunds 添加 timeout = 3sfallback = { reservationId: "NONE", status: "degraded" },使慢速的预留操作不会阻塞管道。
  5. 技术验证: 编写一个测试,让 ReserveFundsOperator 使用 MockOperator.throwing(...),证明 fallback 被触发且 graph 仍能到达 makeDecision
  6. 业务验证: 至少覆盖 auto-approvemanual-reviewreject 三个 Case;Policy 中禁止真实 effect,Fixture 固定授信、反欺诈和收入事实。
  7. 证据解读: 对报告写出 verdict、evidence trust/source binding、Requirement contribution 和 release claim 四层结论,不得互相替代。
  8. 架构表达: 提交一张 graph/runtime/effect 边界图和一张证据到 claim 的 解释图,标出业务 Owner 与发布负责人。
  9. 未证明事项: 至少列出生产容量、真实下游健康、GOLDEN 批准和发布授权。

图:毕业作品的六件交付物

完成标准: 六类资产齐全;单因素失败可以复现;报告中的每句话都能指向 Scenario、源码测试或人工职责之一;任何 PASS 都没有被扩写成发布批准。


可选扩展题库

以下三题不计入三级 capstone 完成标准。它们用于工作坊加练,答案和评分说明已 移到教师指南,避免与主任务并列造成“必须做完六题才毕业”的误读。

批量订单:添加逐项韧性和汇总分支

Fixture: batch-order-parallel.bloge

需求:

  1. foreach 主体内的 deductStock 节点添加 retry = { attempts: 1, backoff: 100ms, strategy: exponential }timeout = 3s
  2. deductStock 添加 fallback = { deducted: false, reason: "stock service unavailable" },使单个库存扣减失败不会破坏整个批次。
  3. summarize 之后,添加一个 branch on summarize.output.allSucceeded
    • truenotifySuccess(一个新的 NotifySuccessOperator 节点)
    • falsenotifyPartialFailure(一个新的 NotifyPartialFailureOperator 节点)
  4. 验证: 让一个 item 的 deductStock 使用 MockOperator.throwing(...),确认 fallback 被触发,然后断言 notifyPartialFailure 执行而 notifySuccess 被跳过。

练习的概念: foreach、逐项韧性、迭代内的 fallback、基于聚合输出的下游分支(第 5、6、10 章)。

进阶任务 —— 全局限流与逐项失败模式。 在上面需求的基础上,把 foreachbatch_size = 10,这样同时最多 10 个 item 在跑;再把 on_item_failure = continue,这样单个坏 item 不会让整批中止。用 MockOperator.throwing(...) 让 20 个 item 里有 3 个失败,然后断言:

  1. deductStock 被调用了正好 20 次(每个 item 都尝试过)。
  2. summarize.output.failedCount == 3,且 allSucceeded == false
  3. notifyPartialFailure 跑一次;notifySuccess 被 skip。
  4. batch_size = 10 下,测试观察到的并发峰值不超过 10 (在 MockOperator.of(...) 外层用 AtomicInteger active 记录当前并发, 再用第二个 peak 计数器在每次进入时更新峰值)。

这套组合 —— batch_size + on_item_failure = continue + 基于聚合的下游 分支 —— 就是生产环境做韧性批处理的标准模式。


状态轮询到事件驱动:用 Await 替代 Loop

Fixture: status-polling.bloge(起点)和 payment-wait.bloge(参考)

需求:

本实验分为两个部分。

Part A —— 扩展轮询循环:

  1. fetchResult 之后添加一个 notifyReady 节点,当结果可用时发送通知。输入:result = fetchResult.output
  2. 给循环主体内的 checkStatus 添加 timeout = 5s
  3. 验证: 断言 notifyReadyfetchResult 之后执行。

Part B —— 用 await 替代循环:

  1. 改写 graph,使用 await 代替轮询循环。在 submitJob 之后,声明:
    await awaitJobReady {
    event "job.ready" where jobId = submitJob.output.jobId
    timeout = 60s
    on_timeout {
    status = "timeout"
    reason = "Job did not become ready within 60 seconds"
    }
    }
  2. 添加一个 branch on awaitJobReady.output.status
    • "ready"fetchResult
    • otherwisehandleTimeout(一个新节点,记录失败信息)
  3. 验证: 编写两个测试——一个模拟事件到达(断言 fetchResult 执行、handleTimeout 被跳过),一个模拟超时(断言 handleTimeout 执行、fetchResult 被跳过)。

练习的概念: loopawait、事件关联、on_timeout、挂起分支、轮询 vs 事件驱动设计的对比(第 10、11、12 章)。


信贷审批 decision_table

目标: 将一个基于分支的信用等级分类器重构为 decision_table 节点,然后探索多策略决策表和运行时错误路径。

第 1 步 —— 基线:分支堆栈。 打开(或创建) ch22/credit-approval-baseline.bloge。 该图使用 branch on applicant.output.score,有三个显式 case 和一个 otherwise。确认它通过所有现有测试。

第 2 步 —— 重构为 hit=unique + 命名字段输出。 用以下代码替换分支:

decision_table credit_tier(
score = applicant.output.score
) hit=unique -> { tier: String, rate: Decimal } {
rule (score: score >= 750) -> { tier: "platinum", rate: 3.25 }
rule (score: 680 <= score < 750) -> { tier: "gold", rate: 4.50 }
otherwise -> { tier: "rejected", rate: 0.0 }
}

更新下游接线,使用 credit_tier.output.tiercredit_tier.output.rate。运行测试——所有测试均应通过。

第 3 步 —— 添加 hit=collect 进行折扣累积。 添加第二张表,收集所有适用的折扣标签:

decision_table applicable_discounts(
score = applicant.output.score,
amount = loan.output.amount
) hit=collect -> String {
rule (score: score >= 700) -> "loyalty"
rule (score: score >= 760) -> "premium"
rule (amount: amount <= 100000) -> "small-loan"
}

applicable_discounts.output.items 接线到通知节点。编写一个测试,断言 score=780, amount=80000 返回三个折扣标签。

第 4 步 —— 触发错误路径。 编写两个负面测试用例:

  1. hit=unique 表中删除 otherwise,然后传入 score=500。断言图抛出 DecisionTableViolationException,code 为 RUNTIME_DECISION_TABLE_NO_MATCH
  2. score >= 750 规则旁边添加一条重叠规则 score >= 700(两者在 score=800 时都会命中)。断言 hit=unique 时引擎抛出 RUNTIME_DECISION_TABLE_AMBIGUOUS_MATCH。验证后恢复表内容。

第 5 步 —— bloge lint 演示。hit=unique 表中删除 otherwise 子句,运行 bloge-lint(或观察编辑器警告)。确认 decision-table/missing-otherwise WARNING 出现。重新添加 otherwise 使 lint 通过。

退出标准:

  • hit=unique + 命名输出:所有等级与利率断言通过。
  • hit=collect:折扣累积测试通过(score=780, amount=80000 返回 3 个标签)。
  • 两个错误路径测试均断言了正确的 code 字符串。
  • 恢复 otherwise 后,bloge-lint 报告零 WARNING。

练习的概念: decision_table、命中策略(uniquecollect)、命名字段输出、RUNTIME_DECISION_TABLE_NO_MATCHRUNTIME_DECISION_TABLE_AMBIGUOUS_MATCHdecision-table/missing-otherwise lint 规则、DecisionTableViolationException 断言(第 5 章,附录 F)。


毕业运行与证据检查

毕业 capstone 最后必须回到一个由业务负责人定义的故事。在固定的 BLOGE 0.9.8-RC1 源码树运行基线测试:

mvn -pl bloge-starter-loan-approval -am \
-Dtest='LoanApprovalApplicationTest,LoanApprovalBusinessScenarioTest,LoanApprovalOperatorFlowTest' \
-Dsurefire.failIfNoSpecifiedTests=false test

预期范围是三个 test classes、11 个 tests:三个决策路径,以及放款成功、重试、 fallback 和 compensation 行为。只把它当作 focused starter evidence。再运行你的 毕业变体,并明确记录测试数、Scenario verdict、evidence trust 和未证明事项。 用第 24 章检查业务映射,再用第 23 章 写出 green run 仍不能证明什么。

完整参考解、常见错误和 20 分量表位于 solutions/ch34/teacher-guide.md。教师应在学员 提交六类资产后再开放;正文不紧邻泄露答案。


实验验收卡

  • 预期与观察: 三级 capstone 从 parser 到贷款 Case 再到证据治理,交付六类资产。
  • 失败与恢复: 让基线、Case 或证据绑定失败;只修该层,未绿不升级。
  • 证明边界: 证明学员变体满足毕业合同,不证明生产发布获批。
  • 练习合同: 前 33 章资产和固定 RC1;每级只增一类能力;交付六类资产;91 个基线测试及本级条件通过即停止。

回顾

  • 基础 capstone 证明你能读懂、扩展并用 graph contract 验证订单 DAG。
  • 进阶 capstone 证明你能让 fallback、分支与可观测事实保持可解释。
  • 毕业 capstone 把 graph、测试、Scenario/Policy/Fixture、证据解读、架构图和 未证明事项组合成一个可复核作品。
  • 三个可选扩展题覆盖 batch、await 与 decision table,但不改变毕业完成标准。
  • 测试、业务验证和发布批准是三层责任;任何一层通过都不能代替后一层。

下一步

你已经完成了 Head First BLOGE。最后一次产出不是“又一份 DSL”,而是一套别人 能够运行、解释和质疑的毕业作品。从这里开始:


参考链接

Coding Agent: Open the versioned task guide.