Solon v4.1.0

Inclusive 动态专家选择与反馈循环

</> markdown
2026年9月18日 上午11:56:06

Parallel 固定执行全部分支;实际系统常常需要“按任务动态选择多个专家”,或者“生成—审核—修改”循环。这两类拓扑分别适合 Inclusive 和带上限的反馈回路。

1、Inclusive 动态多选

classify
  -> Inclusive split
       ├─ legal_agent ──┐
       ├─ risk_agent  ──┼─> Inclusive join
       └─ cost_agent  ──┘
  -> aggregate

先由分类节点写入稳定标签:

ctx.put("route.needLegal", true);
ctx.put("route.needRisk", true);
ctx.put("route.needCost", false);

再选择多个分支:

spec.addInclusive("expert_select")
        .linkAdd(legal.name(), link -> link.when(ctx ->
                Boolean.TRUE.equals(ctx.getAs("route.needLegal"))))
        .linkAdd(risk.name(), link -> link.when(ctx ->
                Boolean.TRUE.equals(ctx.getAs("route.needRisk"))))
        .linkAdd(cost.name(), link -> link.when(ctx ->
                Boolean.TRUE.equals(ctx.getAs("route.needCost"))));

spec.addActivity(legal).linkAdd("expert_join");
spec.addActivity(risk).linkAdd("expert_join");
spec.addActivity(cost).linkAdd("expert_join");

spec.addInclusive("expert_join")
        .linkAdd("aggregate");

Inclusive 会执行所有条件命中的分支,汇聚端只等待本轮实际选中的分支数。

2、Inclusive 没有 Exclusive 式默认边

在 Inclusive 中,无条件连接按“始终为真”处理,会和其他命中分支一起执行,而不是“都没命中才执行”。

如果需要兜底,应在前置 Activity 明确计算:

boolean any = needLegal || needRisk || needCost;
ctx.put("route.needManual", !any);

然后为人工分支配置条件:

.linkAdd("manual_review", link -> link.when(ctx ->
        Boolean.TRUE.equals(ctx.getAs("route.needManual"))))

还应确保至少一个分支会命中,否则 Graph 将没有后续路径。

3、Exclusive、Inclusive、Parallel 对照

网关出边选择汇聚规则多线程
Exclusive第一个命中的条件;否则无条件边不负责多分支汇聚
Inclusive所有命中的条件等待本轮实际选中分支否;当前实现即使配置 Executor,也按命中连接依次调用
Parallel全部出边,不检查条件等待全部静态入边配 Executor 后分支可并发,但 eval() 仍等待汇聚完成

三个网关都可以挂 task;汇聚型网关会先完成流入等待,再执行自己的 task。

4、构建有上限的反馈循环

典型内容生产:

draft_agent
  -> review_agent
  -> route
       ├─ PASS -> finish
       ├─ REVISE 且未达上限 -> draft_agent
       └─ 达到上限 -> manual_review

每轮审核后增加计数:

spec.addActivity("review_result")
        .task((ctx, node) -> {
            int round = ctx.getOrDefault("loop.round", 0);
            ctx.put("loop.round", round + 1);

            String review = ctx.getAs("review.result");
            ctx.put("route.reviewDecision", parseDecision(review));
        })
        .linkAdd("review_route");

路由:

spec.addExclusive("review_route")
        .linkAdd("finish", link -> link.when(ctx ->
                "PASS".equals(ctx.getAs("route.reviewDecision"))))
        .linkAdd("prepare_revision", link -> link.when(ctx ->
                "REVISE".equals(ctx.getAs("route.reviewDecision"))
                        && ctx.<Integer>getAs("loop.round") < 3))
        .linkAdd("manual_review");

修订前显式构造下一轮输入:

spec.addActivity("prepare_revision")
        .task((ctx, node) -> {
            String draft = ctx.getAs("draft.result");
            String review = ctx.getAs("review.result");
            ctx.put("draft.input", "原稿:\n" + draft
                    + "\n\n审核意见:\n" + review);
        })
        .linkAdd("draft_step");

不要假设 Agent 会自动记住 Context 中上一轮结果。

5、循环必须有治理上限

至少限制:

  • 最大循环次数;
  • 总 Token;
  • 总耗时;
  • 工具调用次数;
  • 连续相同审核意见;
  • 达到上限后的人工路径。

每轮结果最好保留版本:

draft.round.1
review.round.1
draft.round.2
review.round.2

若只覆盖 draft.result,将失去问题诊断和审计信息。

6、恢复与测试

循环和 Inclusive 的恢复状态比线性 Graph 更复杂。测试至少覆盖:

  • Inclusive 无分支、单分支和多分支命中;
  • 无条件边不会被误当成兜底;
  • PASS 首轮结束;
  • REVISE 后进入下一轮;
  • 达到上限进入人工审核;
  • 恢复后轮次不重复增加;
  • 外部副作用不会因循环或恢复重复执行。