## 现状 ```java // MiddlewareBase —— 未覆写即参与全部扩展点(含未来新增的点) default Set<ExtensionPoint> activePoints() { return EnumSet.allOf(ExtensionPoint.class); } ``` ```java // ReActAgent#groupMiddlewares —— null 折算为全集 Objects.requireNonNullElse(mw.activePoints(), EnumSet.allOf(...)); ``` javadoc 约定 `null` 视为全集、空集表示全点不参与。**不覆写、返回 `null`、显式返回集合三种写法中,前两种语义相同——"没说"与"明确要全部"不可区分。** 六个现有扩展点的 default 实现均为直通或 no-op,默认参与只是空转,因此问题一直未暴露。 ## 问题 1. **参与集合不是可靠判据。** `middlewaresAt(point)` 返回的成员里混着大量被默认卷入、在该点没有任何逻辑的中间件;任何按参与集合决策的框架能力(激活标志、观测短路、按点统计)拿到的都是被污染的集合。 2. **开发者被默认卷入新增的扩展点。** 未覆写 `activePoints()` 的中间件在新点上线后自动参与,无意图也无告警;想精确表达需在覆写 hook 之外再罗列清单,双重记账,漏写即静默失效。 3. **副作用型扩展点不可用。** 设想一个激活即改变全局行为的扩展点:例如终局输出审查钩子,要求回答在审查裁决前不得进入公共事件流——激活即牺牲逐 token 流式。激活判定取 `!middlewaresAt(point).isEmpty()` 时,链上任何一个带默认 `activePoints()` 的中间件都会点亮它,与该点毫无关系的 agent 全体受影响。任何"激活即全局生效"的未来能力都会撞上同样的问题。 原则:**参与必须是意图的表达,不能是缺省的结果。** ## 方案 `activePoints()` 去掉 `default`,改为抽象方法;参与集只认显式返回的集合。 ```java // 现状 default Set<ExtensionPoint> activePoints() { return EnumSet.allOf(ExtensionPoint.class); } // 改为 /** * 参与集的唯一来源。空集 = 保留注册、全点不参与;null = 编程错误, * 构建期抛出;覆写 hook 而未声明对应点 = 永不调用。 */ Set<ExtensionPoint> activePoints(); ``` | `activePoints()` 返回 | 参与集 | | ------------------------------- | ---------------- | | 未实现 | 编译失败 | | `null` | 构建期 fail-fast | | 空集合 | 无(保留注册) | | 具体集合(含显式 `allOf(...)`) | 恰好这些点 | 配套:`groupMiddlewares` 删除 null→全集折算;javadoc 重写,并确立新增扩展点的评审原则——副作用型激活必须显式注册,不能默认参与。 ## 迁移影响(直观且代价很小) 对升级框架的开发者影响很小:未覆写 `activePoints()` 的中间件升级后编译报错,直接定位到要修改的类;补一个方法返回自己实现的点集合即可——一行 `EnumSet.of(...)`,想维持原行为则显式 `allOf(...)`。方法语义单一(声明激活的点),**无理解成本,顺手即可完成**。 其余:已显式覆写的中间件不变;预编译的第三方中间件 jar 需随升级重编译(否则运行期 `AbstractMethodError`);`middlewaresAt()` 只含显式声明者,属公开 API 行为变化。属 source + binary 破坏性变更,不需要过渡版本或废弃周期——编译错误即迁移信号 **(合理无隐患)**。
Activity