Solon v4.1.0

结构化输出与条件路由

</> markdown
2026年9月18日 上午11:53:08

LLM 可以判断语义,但关键业务路径不应直接匹配一段自由文本。推荐管线是:

分类 Agent
  -> 结构化解析和校验 Activity
  -> Exclusive
       ├─ 专项 Agent
       └─ 人工兜底

1、定义稳定的路由结果

例如分类 Agent 约定输出:

{
  "problemType": "TECHNICAL",
  "confidence": 0.92
}

业务允许的枚举:

public enum ProblemType {
    TECHNICAL,
    BILLING,
    COMPLAINT,
    MANUAL_REVIEW
}

不要直接用“看起来像技术问题”“大概是账单”之类文本作为 Graph 条件。

2、解析并校验

spec.addActivity("normalize_route")
        .task((ctx, node) -> {
            String raw = ctx.getAs("classifier.result");
            RouteDecision decision = parseRouteDecision(raw);

            ProblemType type = decision.getProblemType();
            if (decision.getConfidence() < 0.80D) {
                type = ProblemType.MANUAL_REVIEW;
            }

            ctx.put("route.problemType", type.name());
            ctx.put("route.confidence", decision.getConfidence());
        })
        .linkAdd("problem_route");

解析节点应处理:

  • JSON 无法解析;
  • 枚举未知;
  • 置信度缺失或越界;
  • Agent 空输出;
  • 必填字段缺失。

异常数据统一写成 MANUAL_REVIEW,而不是让 Graph 停在未知状态。

3、Exclusive 单选路由

spec.addExclusive("problem_route")
        .linkAdd(technical.name(), link -> link
                .priority(30)
                .when(ctx -> "TECHNICAL".equals(
                        ctx.getAs("route.problemType"))))
        .linkAdd(billing.name(), link -> link
                .priority(20)
                .when(ctx -> "BILLING".equals(
                        ctx.getAs("route.problemType"))))
        .linkAdd(complaint.name(), link -> link
                .priority(10)
                .when(ctx -> "COMPLAINT".equals(
                        ctx.getAs("route.problemType"))))
        .linkAdd("manual_review");

Exclusive 按优先级检查条件,只执行第一个命中的分支;没有条件命中时执行无条件连接。

每个 Exclusive 应只配置一个无条件默认连接。当前执行逻辑不会主动拒绝多个默认连接,多配属于应避免的错误配置。

4、分支 Agent 读取同一任务

Agent technical = SimpleAgent.of(chatModel)
        .name("technical_support")
        .role("技术支持")
        .instruction("处理用户原始问题,并给出技术排查建议。")
        .outputKey("result.technical")
        .build();

TeamAgent Graph 中,成员仍可收到团队原始 Prompt。若分支依赖解析节点生成的新数据,应在 instruction 中显式引用 Context,或改用适配 Activity。

5、统一收敛最终结果

spec.addActivity("finish")
        .task((ctx, node) -> {
            String type = ctx.getAs("route.problemType");
            String result;

            if ("TECHNICAL".equals(type)) {
                result = ctx.getAs("result.technical");
            } else if ("BILLING".equals(type)) {
                result = ctx.getAs("result.billing");
            } else if ("COMPLAINT".equals(type)) {
                result = ctx.getAs("result.complaint");
            } else {
                result = ctx.getAs("result.manual");
            }

            ctx.put("output.final", result);
            TeamTrace trace = TeamTrace.getCurrent(ctx);
            if (trace != null) {
                trace.setFinalAnswer(result);
            }
        })
        .linkAdd(Agent.ID_END);

显式设置 finalAnswer,可以避免最终结果意外依赖“最后执行的 Agent”。

6、验证路由,而不是比较整段文本

Assertions.assertEquals("TECHNICAL",
        session.getContext().get("route.problemType"));
Assertions.assertNotNull(
        session.getContext().get("result.technical"));
Assertions.assertNull(
        session.getContext().get("result.billing"));

还应使用节点拦截器验证命中的分支和未执行的分支。TeamTrace 只能验证 Agent 记录,不能替代完整 Graph 节点历史。

7、设计原则

  • Agent 产出结构化判断;
  • Activity 负责解析、校验和归一化;
  • Graph 负责选择路径;
  • 低置信度进入人工审核;
  • 未知枚举和空输出必须有兜底;
  • 高风险决策不能只依赖 Prompt 约束。