BLOGE
0.9.9-RC1在线版 · 编制 2026-11-02 · English
第 1 章 —— BLOGE 是什么,以及它为什么存在
本章建立对 BLOGE 是什么、它为什么存在,以及 它最核心的结构特征 的整体认识。
学习目标
- 用一句尽量少术语的话解释 BLOGE 是什么。
- 说出让 BLOGE 区别于临时拼装编排代码的三个特性:可见、有结构、有韧性。
- 从概念层面解释什么是 graph —— 由业务步骤组成、数据流与控制流都显式可见的有向无环图。
- 解释为什么并发、重试和失败传播应该由引擎负责,而不是由业务开发者手写在服务代码里。
前置条件
- 具备基础 Java 知识(不需要很资深)。
- 已安装可用的 JDK 25+ 与 Maven 3.6+ (参见入门指南)。
源示例
| 文件 | 展示内容 |
|---|---|
order-process.bloge | 一个完整的订单处理教学示例:包含扇出、汇合、分支与韧性策略 |
bff-dashboard.bloge | 一个 BFF 聚合图:五个并行拉取节点,带有超时、重试和降级 |
30 秒认识 BLOGE
从整体上看,BLOGE 是一个面向业务工作流的编排引擎。它并不取代业务代码本身,而是把“步骤之间如何依赖、哪些步骤可以并行、失败后如何处理”这类运行逻辑从零散实现中提取出来,组织成一个显式的 graph。
BLOGE 的定义
BLOGE 是一个 Java 优先的编排引擎。它把业务工作流表示成由 node 和 dependency 组成的 graph,并由 engine 负责执行顺序、并行、重试、超时、分支、等待和恢复,因此原本散落在 service 代码中的运行逻辑被收束为一套显式的结构。
BLOGE 从 DAG 图模型进一步扩展到更高级的编排模式:多轮 session
(bloge-session-ext + bloge-session-durable)、显式状态机(bloge-state-ext
bloge-state-durable),以及 LLM 驱动的 agent 循环与工具调度 (bloge-agent-ext)。 这些能力在后续章节中讲解——当前阶段,graph 模型是一 切的基础。
BLOGE 是 Biz Logic Orchestration Graph Engine 的缩写。这个名称本身已经给出了系统的基本边界:它面向 业务逻辑,处理的是 编排 问题,以 graph 作为执行模型,并由 engine 负责运行。
在这个意义上,BLOGE 既不是单纯的 DSL,也不是单纯的流程图工具。它同时包含工作流模型与执行引擎;流程的形状、依赖关系和失败策略被声明出来之后,引擎按此完成调度、并发控制和韧性处理。
本书用来理解 BLOGE 的三个设计特征
本书使用三个设计特征比较 BLOGE 与临时拼装的编排代码。它们分别对应可读性、执行组织方式和运行期行为的声明方式,是帮助理解设计取向的框架,不是独立的性能或适用性结论。
- 可见 —— 流程本身以 graph 的形式直接呈现出来,业务路径、分支位置和上下游关系都可以在模型中看到,而不需要通过阅读多层 service 代码来还原。
- 有结构 —— 依赖关系属于工作流模型的一部分。哪些 node 可以并行、哪些 node 必须等待,并不由代码书写顺序决定,而是由 graph 中声明的依赖关系决定。
- 有韧性 —— 重试、超时、降级、等待和持久进度等运行期策略直接附着在工作流上 ,因此失败处理也成为模型的一部分,而不是散落在各处的临时胶水逻辑。
BLOGE 不是什么
明确 BLOGE 的边界,同样有助于理解它的定位。下面三种看法都不准确,因为它们都只抓住了表面形式,而没有抓住它作为编排模型与执行引擎的整体性质。
- 它不是顺序代码的另一层外壳。 如果只是把一串方法调用换成另一种写法,执行关系仍然主要由书写顺序决定;而在 BLOGE 里,真正决定执行形状的是 graph 中声明出来的依赖关系。
- 它不是单纯的流程图工具。 graph 在这里不是静态说明图,而是可被引擎读取、调度和执行的工作流模型。
- 它不是要求业务团队手工补齐调度与恢复机制。 并发、重试、超时、降级和等待等运行期行为不需要在每个业务步骤外层重新拼装,而是由引擎依据模型统一处理。
最简洁的心智捷径可以记成一句话:BLOGE 把工作流意图变成显式 graph,再把运行层面的负担交给引擎。
为什么不是手写编排
只知道 BLOGE 是什么还不够,你还需要知道它为什么值得存在。真正的问题不是“能不能把流 程写在 service 里”,而是当流程越来越长、越来越依赖外部系统、越来越需要恢复和排障时,手写编排会迅速失控。
为什么这很重要
很多业务系统一开始都很简单:
1. 取用户
2. 取商品
3. 计算价格
4. 做授信检查
5. 创建订单(或者拒绝)
在普通服务代码里,你通常会把它写成顺序方法调用。这样一开始没问题——直到问题出现:
- 步骤 1 和步骤 2 本来互不依赖,却被顺序执行,平白浪费总耗时。
- 步骤 4 可能出现瞬时失败,于是你开始在业务代码里写重试循环,基础设施逻辑和领域逻辑缠到一起。
- 没人能“看见”整个流程。 一旦线上出问题,排障的人必须先读代码,才能还原到底发生了什么。
这正是 BLOGE 存在的原因。当你先明确 BLOGE 是一个“执行显式 graph 的引擎”之后,这个价值就更容易理解:你声明流程的形状——谁依赖谁、失败时怎么办——然后把调度、并发和韧性策略交给引擎处理。
心智模型
把 BLOGE graph 想成一张交给引擎的流程卡片:
每个方框都是一个 node,每条箭头都是一个 dependency。引擎读到这张卡片后,就能知道:
fetchUser和fetchProducts彼此没有依赖 → 可以并行运行。calcPrice同时依赖两者 → 必须等两边都完成后再启动。checkCredit的输出驱动一个 branch —— 引擎根据结果选择正确的下游节点。
你描述的是 谁依赖谁;引擎决定的是 什么时候运行谁。
第一个可运行示例
下面是从
order-process.bloge
里摘出的一个简化片段:
graph orderProcess {
node fetchUser : FetchUserOperator {
input { userId = ctx.userId }
timeout = 3s
retry = { attempts: 2, backoff: 200ms, strategy: exponential }
}
node fetchProducts : FetchProductsOperator {
input { productIds = ctx.productIds }
timeout = 5s
}
node calcPrice : CalcPriceOperator {
depends_on = [fetchUser, fetchProducts]
input {
user = fetchUser.output
products = fetchProducts.output
}
}
node checkCredit : CreditCheckOperator {
depends_on = [fetchUser, calcPrice]
input {
userId = fetchUser.output.id
amount = calcPrice.output.total
}
retry = { attempts: 3, backoff: 100ms, strategy: jitter }
fallback = { approved: false, reason: "credit service unavailable" }
}
branch on checkCredit.output.approved {
true -> createOrder
false -> rejectOrder
}
}
按顺序读这段 DSL:
- 两个互不依赖的拉取节点 —— 引擎会并发执行它们。
- 一个计算节点 同时引用两个上游输出 —— 因为出现了
fetchUser.output和fetchProducts.output,依赖关系会被自动推断出来。 - 一个带重试和降级的授信检查节点 —— 韧性策略直接声明在节点上。
- 一个 branch —— 它不是写在 Java 代码里的
if,而是 graph 里的第一类路由结构。
关键认识: 你没有手写线程池,没有手写重试循环,也没有在业务代码里堆
try/catch去兜底。引擎负责这些事。
跟着一笔订单:移动责任,而不是移动方法
设想 OrderService.placeOrder() 最初只有五次清楚的方法调用。半年后,
它还要创建异步任 务、重试信用检查、给库存查询加超时、捕获多种异常、
记录指标,并决定创建还是拒绝订单。这个方法依然“能跑”,但业务故事和
所有运行机制已经挤在同一个责任边界里。
把图读成一次责任迁移
BLOGE 的价值不是给方法调用画几个方框,而是让每个问题都有一个可见的 owner:
| 问题 | 可见 owner |
|---|---|
| 有哪些工作? | fetchUser、calcPrice、createOrder 等 node |
| 哪些工作必须先完成? | 依赖边 |
| 瞬时失败后怎么办? | node 上的 retry 与 timeout 策略 |
| 哪条业务路径没有被选中? | 结果状态 SKIPPED |
| 业务操作实际上做了什么? | Operator 输出与外部证据 |
这种拆分会直接改变评审方式。增加库存检查,改的是 graph 形状;修改计价
公式,改的是 CalcPriceOperator;调整重试间隔,改的是 node 策略。评审者
不必再从一个控制流方法里重新发现三种不同决策。
先记住第一条边界
显式 graph 只能让已经声明的编排关系可检查;它不能证明 Operator 扣款 金额正确,也不能证明流程满足业务需求。第 18 章与第 23—32 章会逐层补上 更强证据。此时只要抓住一个具体收益:执行形状不再藏在偶然形成的 Java 控制流里。
模块地图 —— 现在你已经知道为什么要看它
订单故事给四层模块各自找到了工作:核心负责运行 graph;编写工具让声明 可读;扩展增加更大的交互模型;运行时集成保留并观察 execution。
第 1 层 —— 核心
| 模块 | 作用 |
|---|---|
bloge-core | graph model、调度器、listener/interceptor API 与 GraphEngine |
bloge-runtime-spi | execution、checkpoint、wait、timer、lease、audit 与 inbox store 合同 |
第 2 层 —— 编写
| 模块 | 作用 |
|---|---|
bloge-dsl | .bloge parser、compiler 与 code generator |
bloge-lang / bloge-lsp | 共享语言模型与编辑器诊断 |
bloge-lint / bloge-studio | 可复现规则与可视化 graph 编写 |
bloge-maven-plugin | Operator 元数据生成 |
第 3 层 —— 扩展
| 模块 | 作用 |
|---|---|
bloge-session-ext / bloge-state-ext | 多轮 session 与显式状态机 |
bloge-agent-ext | 具有能力边界的 model/tool loop |
bloge-event-journal | 只追加的 execution 与 agent 事件 |
第 4 层 —— 运行时与集成
| 模块 | 作用 |
|---|---|
bloge-durable / bloge-durable-mybatis | 恢复与持久执行状态 |
bloge-spring / bloge-spring-web | Boot wiring、Web 错误与运维控制台 |
bloge-metrics-otel / bloge-dispatch-kafka | telemetry 与远端 worker 派发 |
bloge-test / bloge-verification | graph 测试与业务正确性证据 |
先使用 bloge-core 与 bloge-dsl。只有当故事走到对应问题时才增加一层;
后面的章节会在这些时刻回到这张地图。
拆开来看
| 概念 | 含义 |
|---|---|
graph | 工作流容器——一个 DAG |
node | 一个处理步骤,会执行某个 operator |
input { … } | 把上游输出和上下文值装配为当前节点的输入 |
ctx.userId | 来自 graph context 的值——执行 graph 时传入的初始数据 |
fetchUser.output | 上游节点输出;只要引用它,就会形成隐式依赖 |
timeout | 节点的执行时限 |
retry | 失败时自动重试,并带退避策略 |
fallback | 在重试仍失败时使用的静态降级结果 |
branch on | 基于节点输出做条件路由,只激活其中一条下游路径 |
常见陷阱
❌ 把 node 当成普通函数调用来理解
// 不要这样想:
User user = fetchUser(ctx.getUserId());
List<Product> products = fetchProducts(ctx.getProductIds());
Price price = calcPrice(user, products);
在命令式代码里,代码的顺序 就是执行顺序。在 BLOGE 里,依赖图 才是执行顺序。如果两个节点互相不引用对方输出,引擎就有自由把它们并发起来。
要用依赖思维,而不是顺序思维。
引导式重写
看看
bff-dashboard.bloge
这个例子。它为一个仪表盘并行拉取五类数据:
graph bffDashboard {
node fetchProfile : FetchProfileOperator {
input { userId = ctx.userId }
timeout = 2s
retry = { attempts: 1, backoff: 100ms }
}
node fetchOrders : FetchOrdersOperator {
input { userId = ctx.userId }
timeout = 3s
fallback = { recentOrderIds: [], totalOrders: 0 }
}
// … fetchRecommendations, fetchNotifications, fetchLoyalty …
node aggregate : AggregateOperator {
depends_on = [fetchProfile, fetchOrders, fetchRecommendations, fetchNotifications, fetchLoyalty]
input {
profile = fetchProfile.output
orders = fetchOrders.output
recommendations = fetchRecommendations.output
notifications = fetchNotifications.output
loyalty = fetchLoyalty.output
}
}
}
可以思考下面几个问题:
- 有多少个节点会并行运行?(答案:五个 fetch 节点都会并行,因为它们之间没有依赖。)
- 如果
fetchOrders失败或超时会发生什么?(答案:fetchOrders没有重试策略,只有 fallback;因此会直接使用{ recentOrderIds: [], totalOrders: 0 }这份降级结果,graph 仍能继续执行。) - 如果你新增第六个 fetch 节点,并让它依赖
fetchProfile.output,它还能与其他 fetch 节点一起并行吗?(答案:不能,它必须先等fetchProfile完成。)
脑力检查
- 让 BLOGE graph 区别于临时编排代码的三个核心特征是什么?(可见、有结构、有韧性。)
- 两个节点是否并发执行,是由开发者逐行控制,还是由引擎决定?(由引擎根据依赖图决定。)
- 在
order-process例子里,如果checkCredit在三次重试后仍然失败,会发生什么?(会使用 fallback 值:{ approved: false, reason: "credit service unavailable" }。)
练习
- 新增一个
fetchInventory节点,让它与fetchUser、fetchProducts并行执行,并给它timeout = 2s。 - 把它的输出接入
calcPrice,为calcPrice增加一个新的输入绑定。 - 预测新的执行形状:哪些节点会并行?哪些节点会等待?
这里暂时不必急着编译,重点在于先把“如何读 DSL、如何改 DSL”的基本方式看清楚。真正的运行过程放在第 2 章。
实验验收卡
- 预期与观察: 依赖与韧性策略在 DSL 中可见,读者能标注执行形状。
- 失败与恢复: 让节点依赖不存在的输出;撤销错误边并重画 DAG。
- 证明边界: 只证明设计意图可审查,不证明可编译、可运行或业务正确。
- 练习合同: 订单 DSL;只加 inventory 节点和一条绑定;交付依赖图;每条边有来源且无环即停止。
回顾
- BLOGE 用一个可见、有结构、有韧性的 graph 替代手写编排。
- graph 是由 node 组成的 DAG,每个 node 运行一个 operator。
- 引擎根据依赖关系(显式声明,或通过
.output推断)来调度节点。 - 韧性策略(timeout、retry、fallback)声明在节点上,而不是埋在业务代码里。
- branch 根据节点输出进行控制流路由。
下一步
第 2 章 —— 你的第一个 Graph继续展开最小 graph 的编写、编译与执行过程。
参考链接
- 项目 README —— 模块概览与快速开始
- 入门指南 —— 完整环境与运行说明
- 核心架构 —— 引擎内部结构
order-process.bloge—— 完整示例源码bff-dashboard.bloge—— 完整示例源码
Coding Agent: Open the versioned task guide.