LangChain4j Java AI 应用开发实战(二十二):条件分支 - 智能路由与动态决策

目录

  • 前言
  • 一、条件分支的核心概念
    • [1.1 三大核心组件](#1.1 三大核心组件)
      • [① 子代理列表(Sub Agents)](#① 子代理列表(Sub Agents))
      • [② 路由条件(Routing Condition)](#② 路由条件(Routing Condition))
      • [③ Fallback 策略](#③ Fallback 策略)
  • 二、实战案例:简历评审智能决策
    • [2.1 业务场景](#2.1 业务场景)
    • [2.2 定义 Agent 接口](#2.2 定义 Agent 接口)
      • [① 邮件助手(EmailAssistant)](#① 邮件助手(EmailAssistant))
      • [② 面试组织者(InterviewOrganizer)](#② 面试组织者(InterviewOrganizer))
      • [③ 信息请求器(InfoRequester)](#③ 信息请求器(InfoRequester))
    • [2.3 工具类与 RAG 支持](#2.3 工具类与 RAG 支持)
      • [① 组织工具(OrganizingTools)](#① 组织工具(OrganizingTools))
      • [② RAG 提供者(RagProvider)](#② RAG 提供者(RagProvider))
    • [2.4 构建条件分支工作流(基础版)](#2.4 构建条件分支工作流(基础版))
      • 完整代码
      • 代码解析
        • [① 创建子代理](#① 创建子代理)
        • [② 构建条件工作流](#② 构建条件工作流)
        • [③ 调用工作流](#③ 调用工作流)
    • [2.5 运行效果](#2.5 运行效果)
      • [场景 1:评分 >= 0.8(安排面试)](#场景 1:评分 >= 0.8(安排面试))
      • [场景 2:评分 < 0.8(发送拒绝邮件)](#场景 2:评分 < 0.8(发送拒绝邮件))
  • 三、高级用法:异步条件执行
    • [3.1 问题:如何实现多条件并行检查?](#3.1 问题:如何实现多条件并行检查?)
    • [3.2 解决方案:异步代理](#3.2 解决方案:异步代理)
  • 四、路由策略详解
    • [4.1 策略 1:硬编码条件(Hard-coded Conditions)](#4.1 策略 1:硬编码条件(Hard-coded Conditions))
    • [4.2 策略 2:分类模型(Classification Model)](#4.2 策略 2:分类模型(Classification Model))
    • [4.3 策略 3:LLM 自主决策(LLM-based Routing)](#4.3 策略 3:LLM 自主决策(LLM-based Routing))
    • [4.4 三种策略对比](#4.4 三种策略对比)
  • [五、Fallback 策略](#五、Fallback 策略)
    • [5.1 问题:当所有条件都不满足怎么办?](#5.1 问题:当所有条件都不满足怎么办?)
    • [5.2 解决方案:添加默认路由](#5.2 解决方案:添加默认路由)
      • [方案 1:总是匹配的条件](#方案 1:总是匹配的条件)
      • [方案 2:最后一个作为默认](#方案 2:最后一个作为默认)
      • [方案 3:友好的错误提示](#方案 3:友好的错误提示)
  • 六、常见问题与避坑指南
    • [6.1 问题 1:条件顺序导致某些分支永远无法执行](#6.1 问题 1:条件顺序导致某些分支永远无法执行)
    • [6.2 问题 2:多个条件同时满足,只执行第一个](#6.2 问题 2:多个条件同时满足,只执行第一个)
    • [6.3 问题 3:AgenticScope 中读取到 null 值](#6.3 问题 3:AgenticScope 中读取到 null 值)
    • [6.4 问题 4:条件判断逻辑复杂导致难以维护](#6.4 问题 4:条件判断逻辑复杂导致难以维护)
  • 七、适用场景与最佳实践
    • [7.1 适用场景](#7.1 适用场景)
    • [7.2 不适用场景](#7.2 不适用场景)
    • [7.3 最佳实践](#7.3 最佳实践)
      • [① 设计互斥的条件](#① 设计互斥的条件)
      • [② 始终提供 Fallback](#② 始终提供 Fallback)
      • [③ 记录路由决策](#③ 记录路由决策)
      • [④ 定期评估路由规则](#④ 定期评估路由规则)
  • 八、与其他工作流的组合
    • [8.1 组合 1:条件分支 + 并行工作流](#8.1 组合 1:条件分支 + 并行工作流)
    • [8.2 组合 2:条件分支 + 循环工作流](#8.2 组合 2:条件分支 + 循环工作流)
    • [8.3 组合 3:条件分支 + 顺序工作流](#8.3 组合 3:条件分支 + 顺序工作流)
  • 结语

前言

在前面的文章中,我们学习了顺序工作流 (固定顺序执行)、循环工作流 (迭代执行)和并行工作流 (并发执行)。但在实际业务场景中,往往需要根据条件选择不同的执行路径

场景 1:智能客服

复制代码
用户问题:
- "怎么退款?" ─→ 路由到「售后 Agent」
- "这个产品怎么样?" ─→ 路由到「销售 Agent」
- "系统报错了" ─→ 路由到「技术支持 Agent」

场景 2:简历评审决策

复制代码
评审结果:
- 评分 >= 0.8 ─→ 安排现场面试
- 评分 < 0.8 ─→ 发送拒绝邮件
- 缺失关键信息 ─→ 请求补充材料

场景 3:内容审核

复制代码
内容类型:
- 文本 ─→ 文本审核 Agent
- 图片 ─→ 图片审核 Agent
- 视频 ─→ 视频审核 Agent

这就是条件分支工作流的价值所在!

什么是条件分支工作流?

条件分支工作流(Conditional Workflow)是一种基于条件的路由模式 ,它根据运行时状态动态选择要执行的 Agent。

bash 复制代码
┌─────────────┐
│  输入参数    │
└──────┬──────┘
       │
       ▼
┌─────────────┐
│  条件判断    │
└──┬──────┬───┘
   │      │
  YES    NO
   │      │
   ▼      ▼
┌──────┐ ┌──────┐
│AgentA│ │AgentB│
└──────┘ └──────┘

核心优势

智能决策 :根据输入内容自动选择最合适的 Agent

专业化分工 :每个 Agent 专注于特定领域,提高回答质量

灵活扩展 :新增路由规则无需修改现有代码

降级容错:无法匹配时使用 Fallback 策略


一、条件分支的核心概念

1.1 三大核心组件

① 子代理列表(Sub Agents)

条件分支由多个 Agent 组成,但每次只执行其中一个

java 复制代码
EmailAssistant emailAssistant = AgenticServices.agentBuilder(EmailAssistant.class)
    .chatModel(openAiChatModel)
    .tools(new OrganizingTools())
    .build();

InterviewOrganizer interviewOrganizer = AgenticServices.agentBuilder(InterviewOrganizer.class)
    .chatModel(openAiChatModel)
    .tools(new OrganizingTools())
    .contentRetriever(RagProvider.loadHouseRulesRetriever())
    .build();

// 根据条件选择执行其中一个

执行流程

bash 复制代码
条件判断:cvReview.score >= 0.8
- TRUE  → 执行 InterviewOrganizer(安排面试)
- FALSE → 执行 EmailAssistant(发送拒绝邮件)

② 路由条件(Routing Condition)

路由条件是一个布尔表达式,决定执行哪个 Agent:

java 复制代码
.subAgents(agenticScope -> {
    CvReview review = (CvReview) agenticScope.readState("cvReview");
    return review.score >= 0.8;  // 返回 true 或 false
}, interviewOrganizer)

.subAgents(agenticScope -> {
    CvReview review = (CvReview) agenticScope.readState("cvReview");
    return review.score < 0.8;
}, emailAssistant)

常见路由策略

  • 硬编码条件:基于数值阈值、关键词匹配
  • 分类模型:使用机器学习模型分类
  • LLM 自主决策:让 LLM 判断应该调用哪个 Agent

③ Fallback 策略

当所有条件都不满足时,使用默认路由

java 复制代码
// 方式 1:添加默认条件
.subAgents(agenticScope -> true, fallbackAgent)  // 总是匹配,作为兜底

// 方式 2:不指定条件(最后一个作为默认)
.subAgents(fallbackAgent)

Fallback 的作用

  • 🛡️ 防止无法路由导致任务失败
  • 🛡️ 提供友好的错误提示
  • 🛡️ 转人工处理特殊情况

二、实战案例:简历评审智能决策

2.1 业务场景

我们要构建一个智能招聘决策系统

bash 复制代码
输入:
- 候选人简历(tailored_cv.txt)
- 候选人联系方式(candidate_contact.txt)
- 职位描述(job_description_backend.txt)
- HR 评审结果(评分 + 反馈)

处理流程:
1. 检查评审评分
   - 如果评分 >= 0.8:调用 InterviewOrganizer 安排现场面试
   - 如果评分 < 0.8:调用 EmailAssistant 发送拒绝邮件
   - 如果反馈包含"缺失信息":调用 InfoRequester 请求补充材料

2. 被选中的 Agent 执行相应操作
   - 发送邮件
   - 创建日历邀请
   - 更新申请状态

输出:
- 操作结果(邮件 ID、日历邀请、状态更新)

2.2 定义 Agent 接口

① 邮件助手(EmailAssistant)

java 复制代码
package com.langchain4j.agentic._05_conditional_workflow;

import dev.langchain4j.agentic.Agent;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.V;

/**
 * 邮件助手接口 - 向未通过评审的候选人发送拒绝邮件
 */
public interface EmailAssistant {

    @Agent("向未通过的候选人发送拒绝邮件,返回已发送邮件 ID,如果无法发送则返回 0")
    @SystemMessage("""
            你向未通过首轮评审的候选申请人发送一封友好的邮件。
            你还将申请状态更新为"已拒绝"。
            你返回已发送邮件的 ID。
            """)
    @UserMessage("""
            被拒绝的候选人:{{candidateContact}}
            
            申请职位:{{jobDescription}}
            """)
    int send(@V("candidateContact") String candidateContact, 
             @V("jobDescription") String jobDescription);
}

关键点

  • 角色:HR 助手,负责发送拒绝邮件
  • 工具:使用 OrganizingTools 发送邮件、更新状态
  • 返回值:邮件 ID(用于追踪)

② 面试组织者(InterviewOrganizer)

java 复制代码
package com.langchain4j.agentic._05_conditional_workflow;

import dev.langchain4j.agentic.Agent;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.V;

/**
 * 面试组织者接口 - 组织候选人的现场面试
 */
public interface InterviewOrganizer {

    @Agent("组织申请人的现场面试")
    @SystemMessage("""
            你通过向所有相关员工发送日历邀请来组织现场会议,
            安排在当前日期一周后的上午进行 3 小时面试。
            这是相关的职位空缺:{{jobDescription}}
            你还向候选人发送一封祝贺邮件,包含面试详细信息
            以及他在来现场之前应该了解的任何事项。
            最后,你将申请状态更新为"已邀请现场面试"。
            """)
    @UserMessage("""
            为此候选人组织一场现场面试(适用外部访客政策):{{candidateContact}}
            """)
    String organize(@V("candidateContact") String candidateContact, 
                    @V("jobDescription") String jobDescription);
}

关键点

  • 角色:面试协调员,负责安排面试
  • 工具:使用 OrganizingTools 创建日历、发送邮件
  • RAG:加载办公室规则(house_rules.txt),告知候选人注意事项

③ 信息请求器(InfoRequester)

java 复制代码
package com.langchain4j.agentic._05_conditional_workflow;

import com.langchain4j.domain.CvReview;
import dev.langchain4j.agentic.Agent;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.V;

/**
 * 信息请求器接口 - 向候选人发送邮件以获取额外信息
 */
public interface InfoRequester {

    @Agent("向候选人发送邮件以获取额外信息")
    @SystemMessage("""
            你向候选人发送一封友好的邮件,请求公司评审申请所需的额外信息。
            要明确说明他们的申请仍在考虑中。
            """)
    @UserMessage("""
            HR 评审及缺失信息描述:{{cvReview}}
            
            候选人联系信息:{{candidateContact}}
            
            职位描述:{{jobDescription}}
            """)
    String send(@V("candidateContact") String candidateContact, 
                @V("jobDescription") String jobDescription, 
                @V("cvReview") CvReview hrReview);
}

关键点

  • 角色:信息收集员,负责请求补充材料
  • 触发条件:评审反馈中包含"缺失信息"
  • 语气:友好,强调申请仍在考虑中

2.3 工具类与 RAG 支持

① 组织工具(OrganizingTools)

java 复制代码
package com.langchain4j.agentic._05_conditional_workflow;

import dev.langchain4j.agent.tool.P;
import dev.langchain4j.agent.tool.Tool;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * 组织工具类 - 提供邮件发送、日历安排等组织功能的工具方法
 */
public class OrganizingTools {

    @Tool
    public Date getCurrentDate(){
        return new Date();
    }

    @Tool("查找给定职位描述 ID 需要参加现场面试的人员的邮箱地址和姓名")
    public List<String> getInvolvedEmployeesForInterview(@P("职位描述 ID") String jobDescriptionId){
        System.out.println("*** 职位描述 ID:" + jobDescriptionId);
        // 演示用的虚拟实现
        return new ArrayList<>(List.of(
                "Anna Bolena: hiring.manager@company.com",
                "Chris Durue: near.colleague@company.com",
                "Esther Finnigan: vp@company.com"));
    }

    @Tool("根据邮箱地址为员工创建日历条目")
    public void createCalendarEntry(
        @P("员工邮箱地址列表") List<String> emailAddress, 
        @P("会议主题") String topic, 
        @P("开始日期和时间,格式为 yyyy-mm-dd hh:mm") String start, 
        @P("结束日期和时间,格式为 yyyy-mm-dd hh:mm") String end){
        System.out.println("*** 已创建日历条目 ***");
        System.out.println("主题:" + topic);
        System.out.println("开始时间:" + start);
        System.out.println("结束时间:" + end);
    }

    @Tool
    public int sendEmail(
        @P("收件人邮箱地址列表") List<String> to, 
        @P("抄送人邮箱地址列表") List<String> cc, 
        @P("邮件主题") String subject, 
        @P("邮件正文") String body){
        System.out.println("*** 已发送邮件 ***");
        System.out.println("收件人:" + to);
        System.out.println("抄送人:" + cc);
        System.out.println("主题:" + subject);
        System.out.println("正文:" + body);
        return 1234; // 虚拟邮件 ID
    }

    @Tool
    public void updateApplicationStatus(
        @P("职位描述 ID") String jobDescriptionId, 
        @P("候选人姓名(名,姓)") String candidateName, 
        @P("新的申请状态") String newStatus){
        System.out.println("*** 申请状态已更新 ***");
        System.out.println("职位描述 ID:" + jobDescriptionId);
        System.out.println("候选人姓名:" + candidateName);
        System.out.println("新状态:" + newStatus);
    }
}

工具说明

  • getCurrentDate():获取当前日期,用于计算面试时间
  • getInvolvedEmployeesForInterview():查找需要参加面试的员工
  • createCalendarEntry():创建日历邀请
  • sendEmail():发送邮件
  • updateApplicationStatus():更新申请状态

② RAG 提供者(RagProvider)

java 复制代码
package com.langchain4j.agentic._05_conditional_workflow;

import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.splitter.DocumentSplitters;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.rag.content.retriever.ContentRetriever;
import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever;
import dev.langchain4j.store.embedding.EmbeddingStoreIngestor;
import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore;
import dev.langchain4j.model.embedding.onnx.bgesmallenv15q.BgeSmallEnV15QuantizedEmbeddingModel;

import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;

import static dev.langchain4j.data.document.loader.FileSystemDocumentLoader.loadDocument;

public class RagProvider {

    public static ContentRetriever loadHouseRulesRetriever() {
        Document doc = loadDocument(toPath("documents/house_rules.txt"));
        EmbeddingModel embeddingModel = new BgeSmallEnV15QuantizedEmbeddingModel();
        InMemoryEmbeddingStore<TextSegment> store = new InMemoryEmbeddingStore<>();

        EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
                .documentSplitter(DocumentSplitters.recursive(200, 10))
                .embeddingModel(embeddingModel)
                .embeddingStore(store)
                .build();

        ingestor.ingest(List.of(doc));

        return EmbeddingStoreContentRetriever.builder()
                .embeddingStore(store)
                .embeddingModel(embeddingModel)
                .maxResults(2)
                .minScore(0.8)
                .build();
    }

    public static Path toPath(String relativePath) {
        try {
            URL fileUrl = Utils.class.getClassLoader().getResource(relativePath);
            return Paths.get(fileUrl.toURI());
        } catch (URISyntaxException e) {
            throw new RuntimeException(e);
        }
    }
}

作用

  • 加载办公室规则文档(house_rules.txt)
  • 创建 RAG 检索器,供 InterviewOrganizer 使用
  • 面试组织者可以检索办公室规则,告知候选人注意事项

2.4 构建条件分支工作流(基础版)

完整代码

java 复制代码
package com.langchain4j;

import com.langchain4j.agentic._05_conditional_workflow.*;
import com.langchain4j.domain.CvReview;
import com.langchain4j.util.StringLoader;
import dev.langchain4j.agentic.AgenticServices;
import dev.langchain4j.agentic.UntypedAgent;
import dev.langchain4j.model.openai.OpenAiChatModel;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.Map;

@SpringBootTest
public class _05_ConditionalWorkflowTest {

    @Autowired
    private OpenAiChatModel openAiChatModel;

    @Test
    public void testConditionalWorkflow() throws Exception {
        
        // 1. 创建子代理
        EmailAssistant emailAssistant = AgenticServices.agentBuilder(EmailAssistant.class)
                .chatModel(openAiChatModel)
                .tools(new OrganizingTools())  // 使用组织工具
                .build();
        
        InterviewOrganizer interviewOrganizer = AgenticServices.agentBuilder(InterviewOrganizer.class)
                .chatModel(openAiChatModel)
                .tools(new OrganizingTools())
                .contentRetriever(RagProvider.loadHouseRulesRetriever())  // 添加 RAG 支持
                .build();

        // 2. 构建条件工作流
        UntypedAgent candidateResponder = AgenticServices
                .conditionalBuilder()
                .subAgents(agenticScope -> {
                    CvReview review = (CvReview) agenticScope.readState("cvReview");
                    return review.score >= 0.8;  // 评分 >= 0.8 安排面试
                }, interviewOrganizer)
                .subAgents(agenticScope -> {
                    CvReview review = (CvReview) agenticScope.readState("cvReview");
                    return review.score < 0.8;  // 评分 < 0.8 发送拒绝邮件
                }, emailAssistant)
                .build();

        // 3. 加载输入数据
        String candidateCv = StringLoader.loadFromResource("/documents/tailored_cv.txt");
        String candidateContact = StringLoader.loadFromResource("/documents/candidate_contact.txt");
        String jobDescription = StringLoader.loadFromResource("/documents/job_description_backend.txt");
        
        // 测试通过的情况
        CvReview cvReviewPass = new CvReview(0.9, "简历优秀,符合后端职位的所有要求。");

        // 4. 准备参数
        Map<String, Object> arguments = Map.of(
                "candidateCv", candidateCv,
                "candidateContact", candidateContact,
                "jobDescription", jobDescription,
                "cvReview", cvReviewPass  // 改为 cvReviewFail 以查看另一个分支
        );

        // 5. 调用条件工作流
        candidateResponder.invoke(arguments);
        
        // 6. 观察控制台输出
        // 工具会打印执行的操作(发送邮件、更新状态等)
    }
}

代码解析

关键步骤拆解

① 创建子代理
java 复制代码
EmailAssistant emailAssistant = AgenticServices.agentBuilder(EmailAssistant.class)
    .chatModel(openAiChatModel)
    .tools(new OrganizingTools())  // 代理可以使用那里定义的所有工具
    .build();

InterviewOrganizer interviewOrganizer = AgenticServices.agentBuilder(InterviewOrganizer.class)
    .chatModel(openAiChatModel)
    .tools(new OrganizingTools())
    .contentRetriever(RagProvider.loadHouseRulesRetriever())  // 这是我们为代理添加 RAG 的方式
    .build();

作用

  • 两个 Agent 都使用 OrganizingTools 工具集
  • InterviewOrganizer 额外添加了 RAG 支持,可以检索办公室规则
② 构建条件工作流
java 复制代码
UntypedAgent candidateResponder = AgenticServices
    .conditionalBuilder()
    .subAgents(agenticScope -> {
        CvReview review = (CvReview) agenticScope.readState("cvReview");
        return review.score >= 0.8;  // 条件 1:评分 >= 0.8
    }, interviewOrganizer)  // 执行面试组织者
    .subAgents(agenticScope -> {
        CvReview review = (CvReview) agenticScope.readState("cvReview");
        return review.score < 0.8;  // 条件 2:评分 < 0.8
    }, emailAssistant)  // 执行邮件助手
    .build();

重要说明

  • 条件按顺序依次检查:先检查条件 1,如果不满足再检查条件 2
  • 第一个匹配的条件会被执行:如果多个条件都满足,只有第一个会执行
  • 条件是硬编码的:直接在代码中定义路由逻辑
③ 调用工作流
java 复制代码
Map<String, Object> arguments = Map.of(
    "candidateCv", candidateCv,
    "candidateContact", candidateContact,
    "jobDescription", jobDescription,
    "cvReview", cvReviewPass  // 评分 0.9,会触发面试安排
);

candidateResponder.invoke(arguments);

注意

  • cvReview 是路由决策的关键参数
  • 修改为 cvReviewFail(评分 0.6)会触发拒绝邮件

2.5 运行效果

场景 1:评分 >= 0.8(安排面试)

bash 复制代码
*** 已创建日历条目 ***
主题:后端工程师现场面试 - 张伟
开始时间:2026-06-11 09:00
结束时间:2026-06-11 12:00

*** 已发送邮件 ***
收件人:[zhangwei.dev@gmail.com]
抄送人:[]
主题:恭喜!您已获得现场面试资格
正文:尊敬的张伟先生,恭喜您通过首轮评审...

*** 申请状态已更新 ***
职位描述 ID:backend_engineer_2026
候选人姓名:张伟
新状态:已邀请现场面试

场景 2:评分 < 0.8(发送拒绝邮件)

bash 复制代码
*** 已发送邮件 ***
收件人:[zhangwei.dev@gmail.com]
抄送人:[]
主题:感谢您申请后端工程师职位
正文:尊敬的张伟先生,非常感谢您申请我们的职位...

*** 申请状态已更新 ***
职位描述 ID:backend_engineer_2026
候选人姓名:张伟
新状态:已拒绝

三、高级用法:异步条件执行

3.1 问题:如何实现多条件并行检查?

在基础版中,条件是按顺序检查 的。但如果我们有三个或更多条件,可能需要同时检查以提高效率。

3.2 解决方案:异步代理

java 复制代码
@Test
public void testConditionalWorkflowAsync() throws Exception {
    
    // 1. 创建异步代理
    ManagerCvReviewer managerCvReviewer = AgenticServices.agentBuilder(ManagerCvReviewer.class)
            .chatModel(openAiChatModel)
            .async(true)  // ⭐ 设置为异步代理
            .outputKey("managerReview")
            .build();
    
    EmailAssistant emailAssistant = AgenticServices.agentBuilder(EmailAssistant.class)
            .chatModel(openAiChatModel)
            .async(true)  // ⭐ 设置为异步代理
            .tools(new OrganizingTools())
            .outputKey("sentEmailId")
            .build();
    
    InfoRequester infoRequester = AgenticServices.agentBuilder(InfoRequester.class)
            .chatModel(openAiChatModel)
            .async(true)  // ⭐ 设置为异步代理
            .tools(new OrganizingTools())
            .outputKey("sentEmailId")
            .build();

    // 2. 构建异步条件工作流
    UntypedAgent candidateResponder = AgenticServices
            .conditionalBuilder()
            .subAgents(scope -> {
                CvReview hrReview = (CvReview) scope.readState("cvReview");
                return hrReview.score >= 0.8;  // 如果 HR 通过,发送给经理评审
            }, managerCvReviewer)
            .subAgents(scope -> {
                CvReview hrReview = (CvReview) scope.readState("cvReview");
                return hrReview.score < 0.8;  // 如果 HR 未通过,发送拒绝邮件
            }, emailAssistant)
            .subAgents(scope -> {
                CvReview hrReview = (CvReview) scope.readState("cvReview");
                return hrReview.feedback.toLowerCase().contains("缺失信息:");  // 如果需要更多信息
            }, infoRequester)
            .output(agenticScope ->
                    (agenticScope.readState("managerReview", new CvReview(0, "无需经理评审"))).toString() +
                    "\n" + agenticScope.readState("sentEmailId", 0)
            )
            .build();

    // 3. 准备参数
    String candidateCv = StringLoader.loadFromResource("/documents/tailored_cv.txt");
    String candidateContact = StringLoader.loadFromResource("/documents/candidate_contact.txt");
    String jobDescription = StringLoader.loadFromResource("/documents/job_description_backend.txt");
    
    CvReview hrReview = new CvReview(
            0.85,
            """
            候选人扎实,薪资期望在范围内,能够在期望的时间框架内开始工作。
            缺失信息:比利时的工授权状态详情。
            """
    );

    Map<String, Object> arguments = Map.of(
            "candidateCv", candidateCv,
            "candidateContact", candidateContact,
            "jobDescription", jobDescription,
            "cvReview", hrReview
    );

    // 4. 运行异步条件工作流
    candidateResponder.invoke(arguments);

    System.out.println("=== 异步条件工作流执行完成 ===");
}

关键改进

  • .async(true):将 Agent 设置为异步执行
  • ✅ 三个条件可以同时检查(虽然最终只执行一个 Agent)
  • .output():自定义输出处理器,提取结果

四、路由策略详解

4.1 策略 1:硬编码条件(Hard-coded Conditions)

直接在代码中定义路由逻辑:

java 复制代码
.subAgents(agenticScope -> {
    CvReview review = (CvReview) agenticScope.readState("cvReview");
    return review.score >= 0.8;
}, interviewOrganizer)

适用场景

  • ✅ 路由规则明确且稳定
  • ✅ 基于数值阈值或简单逻辑
  • ✅ 性能要求高(无需额外调用 LLM)

优点

  • 简单直接
  • 执行速度快
  • 易于调试

缺点

  • 灵活性差
  • 修改规则需要重新部署
  • 无法处理复杂语义

4.2 策略 2:分类模型(Classification Model)

使用机器学习模型进行分类:

java 复制代码
.subAgents(agenticScope -> {
    String userInput = (String) agenticScope.readState("userInput");
    
    // 使用分类模型判断意图
    String intent = classifier.predict(userInput);
    
    return "refund".equals(intent);  // 退款意图
}, refundAgent)

.subAgents(agenticScope -> {
    String userInput = (String) agenticScope.readState("userInput");
    String intent = classifier.predict(userInput);
    return "technical_support".equals(intent);  // 技术支持意图
}, techSupportAgent)

适用场景

  • ✅ 文本分类任务
  • ✅ 意图识别
  • ✅ 已有训练好的分类模型

优点

  • 准确性高
  • 可处理复杂语义
  • 支持多分类

缺点

  • 需要训练数据
  • 维护成本高
  • 新增类别需要重新训练

4.3 策略 3:LLM 自主决策(LLM-based Routing)

让 LLM 判断应该调用哪个 Agent:

java 复制代码
// 方式 1:使用 Supervisor Agent
SupervisorAgent supervisor = AgenticServices.supervisorBuilder()
    .chatModel(openAiChatModel)
    .subAgents(refundAgent, salesAgent, techSupportAgent)
    .build();

// 方式 2:使用 AiServices + Tools
public interface RouterService {
    @Tool("处理退款相关问题")
    String handleRefund(@P("用户问题") String question);
    
    @Tool("处理产品销售相关问题")
    String handleSales(@P("用户问题") String question);
    
    @Tool("处理技术支持相关问题")
    String handleTechSupport(@P("用户问题") String question);
}

RouterService router = AiServices.builder(RouterService.class)
    .chatModel(openAiChatModel)
    .tools(refundExpert, salesExpert, techSupportExpert)
    .build();

适用场景

  • ✅ 路由规则复杂且多变
  • ✅ 需要理解自然语言
  • ✅ 新增 Agent 频繁

优点

  • 灵活性极高
  • 无需硬编码规则
  • 支持自然语言路由

缺点

  • 额外消耗 Token
  • 增加延迟
  • 可能路由错误

4.4 三种策略对比

特性 硬编码条件 分类模型 LLM 自主决策
灵活性
准确性 高(规则明确时) 高(有训练数据时) 中(依赖 LLM)
执行速度
维护成本
适用场景 简单规则 文本分类 复杂语义
Token 消耗

五、Fallback 策略

5.1 问题:当所有条件都不满足怎么办?

java 复制代码
// ❌ 危险示例:没有 Fallback
.subAgents(scope -> review.score >= 0.8, interviewOrganizer)
.subAgents(scope -> review.score < 0.8, emailAssistant)

// 如果 review 为 null 或 score 为 NaN,两个条件都不满足
// 工作流会静默失败,没有任何提示

5.2 解决方案:添加默认路由

方案 1:总是匹配的条件

java 复制代码
.subAgents(scope -> review.score >= 0.8, interviewOrganizer)
.subAgents(scope -> review.score < 0.8, emailAssistant)
.subAgents(scope -> true, fallbackAgent)  // ⭐ 兜底路由,总是匹配

方案 2:最后一个作为默认

java 复制代码
.subAgents(scope -> review.score >= 0.8, interviewOrganizer)
.subAgents(emailAssistant)  // ⭐ 不指定条件,作为默认路由

方案 3:友好的错误提示

java 复制代码
.subAgents(scope -> review.score >= 0.8, interviewOrganizer)
.subAgents(scope -> review.score < 0.8, emailAssistant)
.subAgents(scope -> true, agenticScope -> {
    System.err.println("⚠️ 无法路由:评审结果异常");
    return "抱歉,系统暂时无法处理您的申请,请稍后重试或联系人工客服。";
})

六、常见问题与避坑指南

6.1 问题 1:条件顺序导致某些分支永远无法执行

症状

java 复制代码
.subAgents(scope -> true, agentA)  // 总是匹配
.subAgents(scope -> review.score >= 0.8, agentB)  // 永远不会执行 ❌

原因

  • 条件按顺序检查
  • 第一个匹配的条件会被执行
  • true 总是匹配,后续条件不会被检查

解决方案

java 复制代码
// ✅ 正确顺序:具体条件在前,通用条件在后
.subAgents(scope -> review.score >= 0.8, agentB)
.subAgents(scope -> review.score < 0.8, agentC)
.subAgents(scope -> true, fallbackAgent)  // 兜底放在最后

6.2 问题 2:多个条件同时满足,只执行第一个

症状

java 复制代码
// 评分 0.9,且反馈包含"缺失信息"
.subAgents(scope -> review.score >= 0.8, interviewOrganizer)  // ✅ 执行这个
.subAgents(scope -> review.feedback.contains("缺失信息"), infoRequester)  // ❌ 不执行

说明

  • 这是正常行为,条件分支只执行第一个匹配的 Agent
  • 如果需要执行多个 Agent,应该使用并行工作流

解决方案

java 复制代码
// ✅ 方案 1:调整条件优先级,确保互斥
.subAgents(scope -> review.score >= 0.8 && !review.feedback.contains("缺失信息"), interviewOrganizer)
.subAgents(scope -> review.feedback.contains("缺失信息"), infoRequester)
.subAgents(scope -> review.score < 0.8, emailAssistant)

// ✅ 方案 2:使用并行工作流同时执行
.parallelBuilder()
    .subAgents(interviewOrganizer, infoRequester)
    .build();

6.3 问题 3:AgenticScope 中读取到 null 值

症状

bash 复制代码
NullPointerException at line: review.score

原因

  • 参数未正确传入
  • outputKey 配置错误

解决方案

java 复制代码
.subAgents(agenticScope -> {
    CvReview review = (CvReview) agenticScope.readState("cvReview");
    
    // ✅ 添加 null 检查
    if (review == null) {
        System.err.println("⚠️ 评审结果为空,使用默认路由");
        return false;  // 路由到 Fallback
    }
    
    return review.score >= 0.8;
}, interviewOrganizer)

6.4 问题 4:条件判断逻辑复杂导致难以维护

症状

java 复制代码
.subAgents(scope -> {
    CvReview review = (CvReview) scope.readState("cvReview");
    String jobType = (String) scope.readState("jobType");
    Integer yearsOfExperience = (Integer) scope.readState("yearsOfExperience");
    
    // 复杂的判断逻辑
    return (review.score >= 0.8 && "senior".equals(jobType) && yearsOfExperience >= 5) ||
           (review.score >= 0.9 && "junior".equals(jobType));
}, interviewOrganizer)

解决方案

java 复制代码
// ✅ 方案 1:提取路由逻辑到独立方法
private boolean shouldScheduleInterview(AgenticScope scope) {
    CvReview review = (CvReview) scope.readState("cvReview");
    String jobType = (String) scope.readState("jobType");
    Integer yearsOfExperience = (Integer) scope.readState("yearsOfExperience");
    
    if (review == null) return false;
    
    if ("senior".equals(jobType)) {
        return review.score >= 0.8 && yearsOfExperience >= 5;
    } else if ("junior".equals(jobType)) {
        return review.score >= 0.9;
    }
    
    return false;
}

.subAgents(this::shouldScheduleInterview, interviewOrganizer)

// ✅ 方案 2:使用 LLM 自主决策(见 4.3 节)

七、适用场景与最佳实践

7.1 适用场景

场景 说明 示例
智能客服 根据问题类型路由到不同专家 退款、销售、技术支持
内容审核 根据内容类型选择审核策略 文本、图片、视频
招聘决策 根据评分决定下一步操作 安排面试、拒绝、请求补充
多语言支持 根据语言选择翻译 Agent 中文、英文、日文
权限控制 根据用户角色路由到不同流程 管理员、普通用户、访客

7.2 不适用场景

所有条件都需要执行 :应使用顺序工作流

条件之间有依赖关系 :应使用顺序工作流

需要同时执行多个 Agent :应使用并行工作流

路由规则极其简单:直接使用 if-else 即可

7.3 最佳实践

① 设计互斥的条件

原则

  • 📌 每个条件应该是互斥的,避免多个条件同时满足
  • 📌 如果无法完全互斥,明确优先级
java 复制代码
// ✅ 互斥条件
.subAgents(scope -> review.score >= 0.8, agentA)
.subAgents(scope -> review.score < 0.8 && review.score >= 0.5, agentB)
.subAgents(scope -> review.score < 0.5, agentC)

// ❌ 重叠条件
.subAgents(scope -> review.score >= 0.7, agentA)
.subAgents(scope -> review.score >= 0.5, agentB)  // 0.7 以上会同时匹配两个

② 始终提供 Fallback

原则

  • 📌 永远不要假设所有情况都被覆盖
  • 📌 Fallback 应该提供友好的错误提示
java 复制代码
.subAgents(scope -> review.score >= 0.8, interviewOrganizer)
.subAgents(scope -> review.score < 0.8, emailAssistant)
.subAgents(scope -> true, agenticScope -> {
    // Fallback:记录日志并返回友好提示
    System.err.println("⚠️ 未知路由情况:" + agenticScope.readState("cvReview"));
    return "抱歉,系统暂时无法处理您的申请,请联系人工客服。";
})

③ 记录路由决策

价值

  • 📊 分析路由分布
  • 🐛 调试路由错误
  • 📈 优化路由规则
java 复制代码
.subAgents(agenticScope -> {
    CvReview review = (CvReview) agenticScope.readState("cvReview");
    boolean shouldSchedule = review.score >= 0.8;
    
    // 记录路由决策
    System.out.println("路由决策:评分=" + review.score + ", 安排面试=" + shouldSchedule);
    
    return shouldSchedule;
}, interviewOrganizer)

④ 定期评估路由规则

方法

  • 📊 统计各路由的执行次数
  • 🔍 分析 Fallback 触发的原因
  • 📈 评估路由准确性(用户满意度)
java 复制代码
// 在 Fallback 中记录原因
.subAgents(scope -> true, agenticScope -> {
    System.err.println("⚠️ Fallback 触发原因:");
    System.err.println("  - 评审结果:" + agenticScope.readState("cvReview"));
    System.err.println("  - 其他参数:" + agenticScope.readState("otherParam"));
    
    return "已转人工处理";
})

八、与其他工作流的组合

8.1 组合 1:条件分支 + 并行工作流

java 复制代码
// 伪代码示例
// 根据条件触发不同的并行工作流

ParallelAgent technicalReview = AgenticServices.parallelBuilder()
    .subAgents(hrReviewer, managerReviewer, teamMemberReviewer)
    .build();

ParallelAgent managementReview = AgenticServices.parallelBuilder()
    .subAgents(directorReviewer, ceoReviewer)
    .build();

.conditionalBuilder()
    .subAgents(scope -> isTechnicalRole(scope), technicalReview)
    .subAgents(scope -> isManagementRole(scope), managementReview)
    .build();

8.2 组合 2:条件分支 + 循环工作流

java 复制代码
// 伪代码示例
// 在循环内部使用条件分支

LoopAgent optimizationLoop = AgenticServices.loopBuilder()
    .subAgents(
        conditionBranch,  // 根据质量决定是否需要优化
        optimizer  // 执行优化
    )
    .exitCondition(scope -> qualityMet(scope))
    .build();

8.3 组合 3:条件分支 + 顺序工作流

java 复制代码
// 伪代码示例
// 条件分支的每个分支都是顺序工作流

SequenceAgent technicalProcess = AgenticServices.sequenceBuilder()
    .subAgents(initialScreening, technicalInterview, backgroundCheck)
    .build();

SequenceAgent managementProcess = AgenticServices.sequenceBuilder()
    .subAgents(initialScreening, managementInterview, executiveApproval)
    .build();

.conditionalBuilder()
    .subAgents(scope -> isTechnicalRole(scope), technicalProcess)
    .subAgents(scope -> isManagementRole(scope), managementProcess)
    .build();

结语

本文深入讲解了条件分支工作流的三大核心组件------子代理列表、路由条件与 Fallback 策略,通过简历评审后智能决策(安排面试/拒绝/请求补充)的完整实战,演示了从硬编码条件到 LLM 自主决策的三种路由策略演进。条件分支的精髓在于让系统根据运行时状态做出"选择题" :条件按优先级依次匹配,首个命中即执行,Fallback 兜底防止静默失败。至此,你已经掌握了顺序、循环、并行、条件分支四种基础编排模式,下一篇我们将学习主管编排与组合工作流,看 Supervisor Agent 如何自主调度子 Agent 并将多种模式嵌套组合成复杂系统。敬请期待!


🎯 更多专栏系列文章:LangChain4j Java AI应用开发实战、🔥 其他专栏可以查看博客主页

🔔 关于作者 :资深程序老猿,10年+架构经验,现专注 AIGC 探索与实践。

👍 若文章对你有所触动,恳请点赞 ⭐ 关注 ⭐ 收藏!AI 浪潮已至,愿与你同行。