做Agent设计模式这个系列做到第五篇,条件工作流算是我在真实项目里用得最多、也最容易翻车的部分。这期基于 LangChain4j 把“分支决策”这件事拆开聊:为什么简单的 if-else 不够用、怎么让模型自己决定走向哪个分支、以及循环和重试怎么落到代码里。适合那些已经能用 LangChain4j 跑通基础对话,想进一步把 Agent 流程设计成复杂业务系统的朋友。1. 条件工作流到底解决什么问题1.1 一个多场景助手的分支需求先看一个很常见的场景:你要写一个客服助手,用户进来之后可能问天气、可能设置提醒、可能只是打个招呼。三个任务如果放在同一个系统提示词里,让一个模型接口直接回答,会出现两个典型问题:第一,提示词里要同时塞入不同业务的规则和输出要求,整体会变得很长,模型在长上下文中更容易忽略其中某一条;第二,不同任务可能需要调用不同的工具、返回不同的字段结构,强行在一个“万能助手”里做,代码会变成一堆 if-else 嵌套,而且每加一个业务都要重新调整提示词,测试成本翻倍。条件工作流解决的就是这件事:把“用户进来之后到底走哪条处理链路”这个决策单独抽出来,先把意图分清楚,再让不同的小助手各管一段。你可以把整个流程想成公司前台:前台先问“办什么业务”,然后引导你去对应窗口,而不是所有人都挤在一个柜台后面处理所有问题。1.2 条件工作流的核心是流程编排而不是分支语法很多人一听到“条件工作流”,第一反应就是代码里的 switch、if-else。语法层面确实如此,但真正要设计的不是“怎么判断”,而是“在哪个环节判断、判断结果怎么影响后续流程”。在 Agent 设计模式里,条件工作流通常出现在两个层次:业务规则层:根据输入内容、上下文状态、用户参数做判断,比如“消息里包含城市和时间就查天气”,这种判断不依赖模型,速度快、结果稳定。模型推导层:机器无法用规则覆盖所有表达,比如“明天会不会下雨”和“明天适合带伞吗”在表达上完全不同,但意图可能接近。这种语义判断需要让模型参与决策。条件工作流的价值在于把这两层组合起来:先用规则拦截能明确判断的请求,再把模糊的请求交给模型路由,最后根据路由结果选择不同的执行分支。这里的“条件”可以是静态规则、模型输出、工具执行结果,甚至可以跨越多个节点形成一个循环,直到某个终止条件满足才退出。与顺序工作流相比,条件工作流最大的变化是:执行路径不是预编译好的单选题,而是运行时依赖输入内容和中间结果动态生成的。这也意味着,你的代码一旦写成,就要面对“模型判断错了怎么办”的局面。2. 条件工作流的实现路线怎么选2.1 规则预检:先拦截,再问模型我习惯在任何条件工作流的最前面加一道规则预检,而不是把所有判断都丢给模型。原因很简单:规则预检免费、确定、无延迟。比如用户消息里包含“天气”并且包含一个城市名,或者包含“提醒我”和时间短语,这些特征可以通过一个很小的正则表达式或者关键词表命中,根本不需要调用模型。规则预检适合处理高频且表达相对固定的场景。它的缺点也明显:自然语言变化太多,完全靠规则覆盖所有情况不现实。比如用户说“今天出门穿什么?”,这句话里没有“天气”两个字,但本质上是天气查询。这时规则会漏掉,必须让模型介入。所以我的设计原则是:规则预检负责“能确定的就确定”,模型路由负责“规则拿不准的再兜底”。两者结合,能在成本、延迟和准确率之间取得一个比较平衡的位置。2.2 模型路由:让 LLM 决策意图模型路由的核心思想是:把路由本身当成一个 Agent 任务,让模型输出一个结构化结果,然后用这个结果驱动代码分支。常见的做法是让模型返回一个枚举值或一个 JSON 对象,比如{"action":"WEATHER_QUERY","reason":"用户询问明天天气"}。LangChain4j 里可以用接口方法和返回对象的方式让模型直接映射到 Java 类,省去手写 JSON 解析的麻烦。模型路由的优势是覆盖面广、语义理解强,几乎不用维护规则表。但代价也很现实:每次路由多一次 LLM 调用,增加延迟和成本;如果返回格式不稳定,下游直接解析失败;如果枚举值太多、划分太细,模型也容易混淆。后面我会专门讲怎么避免这几个坑。另外要注意,模型路由不只是用在大入口做意图分类,还可以用在流程中间。比如“根据当前工具的返回结果决定是否再做一次检索”,这就是循环条件的一种体现。2.3 状态机与循环:处理更复杂的状态转移条件工作流并不只有“一个入口 + 几个分支”这种形态。还有一种更复杂的形态是状态机:整个 Agent 会话会经历多个状态,每个状态都是一个独立的节点,在不同节点间根据条件转移。比如一个售后工单助手,状态可能是收集问题 -> 确认诉求 -> 给方案 -> 回访结束。用户在“确认诉求”状态下输入的内容,处理方式和“收集问题”状态下完全不一样。这种场景如果只靠入口路由一次,无法解决,因为后续每一步都需要根据状态选择行为。LangChain4j 本身没有专门的工作流引擎,但状态机的逻辑可以自己用 Java 代码维护:定义一个状态枚举,维护状态转移表,在每次接收用户输入时先判断当前状态,再路由到对应处理节点。循环也可以理解为一种特殊的状态机——“状态”在同一个节点反复执行,直到满足退出条件。3. 基于 LangChain4j 实现一个条件工作流3.1 依赖准备与模型接入我使用的版本是 LangChain4j 1.x,依赖比较简单。如果你用 Maven,在pom.xml里加核心包和应用框架包即可:<dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j</artifactId> <version>1.0.0</version> </dependency> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-core</artifactId> <version>1.0.0</version> </dependency>模型接入我习惯写一个工厂方法,把具体模型服务的配置和环境变量绑在一起,这样切换模型服务时不用改业务代码。下面是示意:public class ModelFactory { public static ChatLanguageModel createChatModel() { String baseUrl = System.getenv("MODEL_BASE_URL"); String apiKey = System.getenv("MODEL_API_KEY"); String modelName = System.getenv("MODEL_NAME"); // 这里使用兼容主流协议的消息模型实现,具体厂商类放在基础设施层 return ChatLanguageModel.builder() .baseUrl(baseUrl) .apiKey(apiKey) .modelName(modelName) .build(); } }依赖注入或者单例管理都可以,核心是业务代码只面对ChatLanguageModel接口,不会因为底层模型变动而跟着改动。3.2 定义路由结果与分支动作路由结果最好用一个枚举加一个数据类来表达。枚举控制分支走向,数据类可以携带模型给出的理由,便于日志排查。public enum WorkflowAction { WEATHER_QUERY, REMINDER_SET, GREETING, UNKNOWN } public class WorkflowDecision { private WorkflowAction action; private String reason; public WorkflowAction getAction() { return action; } public void setAction(WorkflowAction action) { this.action = action; } public String getReason() { return reason; } public void setReason(String reason) { this.reason = reason; } }枚举数量一定要克制。我见过有人把路由枚举拆成二十多个分支,结果模型经常分错。一般情况下,一个大入口的顶层路由控制在五到七个动作以内会比较稳。如果业务确实很多,可以考虑先粗分再细分,用多级路由。3.3 编写路由接口及分支 AgentLangChain4j 的AiServices可以让我们通过接口定义 Agent,非常直观。我先定义一个路由接口:public interface DecisionRouter { @SystemMessage(""" 你是任务路由引擎。根据用户消息判断意图,只输出JSON格式。 可选动作:WEATHER_QUERY、REMINDER_SET、GREETING、UNKNOWN。 JSON格式:{"action":"动作枚举","reason":"选择理由"} 不要输出其他文字。 """) WorkflowDecision route(@UserMessage String userMessage); }这里让返回类型直接是WorkflowDecision,LangChain4j 会尝试把模型输出解析成 Java 对象。返回类型设计成 POJO,比返回一个字符串再手工解析省事得多。接下来定义分支 Agent,也就是真正干活的小助手:public interface WeatherAgent { @SystemMessage("你是天气问答助手,负责根据用户问题提供简洁的天气建议。") String handle(@UserMessage String userMessage); } public interface ReminderAgent { @SystemMessage("你是提醒助手,负责把用户请求整理成提醒信息并确认。") String handle(@UserMessage String userMessage); }这样设计的好处是每个分支的提示词互相独立,互不干扰。后续改天气助手的提示词,完全不会影响提醒助手。3.4 主流程编排代码实现有了路由接口和分支 Agent,就可以写主流程了。我把整个流程放在一个类里,叫ConditionalWorkflow。入口方法先做规则预检,再走模型路由,最后分发到具体分支。public class ConditionalWorkflow { private final DecisionRouter router; private final WeatherAgent weatherAgent; private final ReminderAgent reminderAgent; public ConditionalWorkflow(ChatLanguageModel model) { this.router = AiServices.builder(DecisionRouter.class) .chatLanguageModel(model) .build(); this.weatherAgent = AiServices.builder(WeatherAgent.class) .chatLanguageModel(model) .build(); this.reminderAgent = AiServices.builder(ReminderAgent.class) .chatLanguageModel(model) .build(); } public String execute(String userMessage) { // 第一步:规则预检,能确定的直接分流 WorkflowAction preAction = PreRule.match(userMessage); if (preAction != null) { return dispatch(preAction, userMessage); } // 第二步:模型路由 WorkflowDecision decision = router.route(userMessage); WorkflowAction action = decision.getAction(); if (action == null || action == WorkflowAction.UNKNOWN) { return fallback(userMessage); } return dispatch(action, userMessage); } private String dispatch(WorkflowAction action, String userMessage) { switch (action) { case WEATHER_QUERY: return weatherAgent.handle(userMessage); case REMINDER_SET: return reminderAgent.handle(userMessage); case GREETING: return greetingReply(userMessage); default: return fallback(userMessage); } } private String greetingReply(String userMessage) { return "你好,有需要随时告诉我。"; } private String fallback(String userMessage) { return "抱歉,我还没有完全理解你的需求,可以换个说法试试。"; } }这段代码最关键的是把“判断”和“执行”拆开。execute方法里没有业务判断逻辑,它只知道“决定完了就分发”;真正的判断逻辑分散在规则预检和模型路由里。这样后续加一个新分支,只需要新增一个枚举值、一个分支接口、一个 case 分支,改动集中且可控。PreRule.match是我抽出来的规则预检工具,内部就是关键词和正则,这里不展开。实际项目中我建议把它做成可配置的规则表,而不是硬编码在代码里。3.5 循环重试与终态控制条件工作流中还有一个高频场景:循环。比如“生成方案 -> 自动评估 -> 不合格则重写”,直到评估通过或者达到最大次数。这个循环本质上也是条件判断驱动的,只不过判断对象是模型输出结果。我用一个简化例子说明:写一段文案,然后用评测模型打一个合格/不合格标记,循环三次。public interface WriterAgent { String write(@UserMessage String instruction); } public interface JudgeAgent { @SystemMessage("你是质检员,判断文案是否符合要求,只输出JSON:{\"passed\":true,\"reason\":\"...\"}") JudgeResult evaluate(@UserMessage String instructionAndText); } public static class JudgeResult { private boolean passed; private String reason; // getters/setters }循环流程:public String generateWithQualityCheck(String instruction) { String answer = null; for (int i = 0; i < 3; i++) { answer = writerAgent.write(instruction); JudgeResult result = judgeAgent.evaluate("需求:" + instruction + "\n文案:" + answer); if (result.isPassed()) { return answer; } System.out.println("第" + (i + 1) + "次未通过,原因:" + result.getReason()); } return answer; }这里有两个必须注意的点。第一,必须有最大迭代次数,否则模型一直不合格就会无限循环,既浪费 token 又拖垮服务。第二,最后一次循环即使不合格也应返回结果,不要返回空字符串。业务上“拿一个不够完美的结果”比“什么都没有”更好兜底。4. 实操中值得注意的细节与排查经验4.1 不要让模型输出“自由格式”的意图我第一次做模型路由时,直接在系统提示词里写“请先判断用户的意图,然后给出回答”,把路由和回答混在一个接口里。结果模型输出一大堆解释文本,我很难稳定地拿到意图字段。后来改成“路由专用接口只输出 JSON”,情况立刻稳定了很多。LangChain4j 在把模型输出映射到 POJO 时,要求模型输出尽量贴近目标结构。你在系统提示词里给一个两个范例,效果会更好。还有一点:WorkflowDecision里的reason字段不要省略,它不影响分支逻辑,但能让排查问题的人立刻看到模型为什么选了这条路。没有reason,出问题时只能瞎猜。4.2 路由 Prompt 越短,越不容易翻车路由接口的系统提示词不需要写太多业务背景。它只需要知道“有哪些动作可选、各自代表什么含义、输出成什么格式”。写多了反而容易让模型花更多 token 去理解无关上下文。我给路由提示词定过三条原则:动作枚举必须在提示词里原样列出,并且说明一定是 JSON 字段值。枚举不超过七个,否则模型混淆率明显上升。每个动作配一个极简解释,但不要贴大段示例数据。如果你的路由需求确实复杂,不要硬塞到一段提示词里。拆成多级路由,先用一个粗粒度接口判断大类,再在各自分支里用更细的提示词判断小类,整体准确率通常更高。4.3 排查条件工作流问题的日志套路条件工作流出问题时,最难的是不知道在哪一环断的。我现在的习惯是给每个关键节点加结构化日志,至少记录四个信息:用户输入原文、路由决策、决策理由、分支执行耗时。实际日志长这样:input=明天上海有雨吗 preRule=null decision=WEATHER_QUERY reason=用户询问天气情况 branch=weatherAgent costMs=842有了这类日志,定位问题就快很多。如果预检命中了但结果不对,说明规则写太宽;如果预检没命中,模型路由选错,那就去看 reason;如果分支输出不对,问题往往在对应 Agent 的提示词而不是路由。这条经验帮我省过很多次时间,建议你把日志从一开始就埋好,不要等上线后出问题再补。4.4 常见问题速查表现象可能原因解决思路模型路由返回中文而非枚举系统提示词约束不够强强化“只输出 JSON”和枚举取值,给一个输出范例分支 Agent 回答跑题路由正确但分支提示词与任务不匹配检查对应分支 Agent 的系统提示词JSON 解析频繁失败模型输出中混入了多余文本改用接口返回 POJO;增加一次重试解析循环不结束缺少最大迭代次数或退出条件增加计数器,最多迭代 N 次后返回当前结果路由枚举太多,准确率下降顶层分支划分过细拆成多级路由,或把枚举数量控制在七个以内条件工作流整体响应变慢每次路由都额外调用一次模型前面加规则预检,能确定的请求不走模型路由模型选错动作但 reason 正确提示词语义模糊调整动作解释,增加 positive/negative few-shot 示例一个关于条件粒度的小经验这个系列写到现在,我个人最大的体会是:条件工作流的复杂度和模型能力成反比。模型能力越强,越可以用更粗的粒度让它自己决策;模型能力弱,就越需要把条件写显式,多依赖规则预检和状态机兜底。另外,不管条件工作流设计得多漂亮,一定要给每个分支留一个容错出口。我见过太多次因为模型在某个分支上输出异常,导致整条链路崩掉的情况。后续如果想继续扩展,可以顺着这个条件路由的思路,把缓存、超时、熔断这些稳定性策略加进来,那又是另一个主题了。
做Agent设计模式这个系列做到第五篇,条件工作流算是我在真实项目里用得最多、也最容易翻车的部分。这期基于 LangChain4j 把“分支决策”这件事拆开聊:为什么简单的 if-else 不够用、怎么让模型自己决定走向哪个分支、以及循环和重试怎么落到代码里。适合那些已经能用 LangChain4j 跑通基础对话,想进一步把 Agent 流程设计成复杂业务系统的朋友。1. 条件工作流到底解决什么问题1.1 一个多场景助手的分支需求先看一个很常见的场景:你要写一个客服助手,用户进来之后可能问天气、可能设置提醒、可能只是打个招呼。三个任务如果放在同一个系统提示词里,让一个模型接口直接回答,会出现两个典型问题:第一,提示词里要同时塞入不同业务的规则和输出要求,整体会变得很长,模型在长上下文中更容易忽略其中某一条;第二,不同任务可能需要调用不同的工具、返回不同的字段结构,强行在一个“万能助手”里做,代码会变成一堆 if-else 嵌套,而且每加一个业务都要重新调整提示词,测试成本翻倍。条件工作流解决的就是这件事:把“用户进来之后到底走哪条处理链路”这个决策单独抽出来,先把意图分清楚,再让不同的小助手各管一段。你可以把整个流程想成公司前台:前台先问“办什么业务”,然后引导你去对应窗口,而不是所有人都挤在一个柜台后面处理所有问题。1.2 条件工作流的核心是流程编排而不是分支语法很多人一听到“条件工作流”,第一反应就是代码里的 switch、if-else。语法层面确实如此,但真正要设计的不是“怎么判断”,而是“在哪个环节判断、判断结果怎么影响后续流程”。在 Agent 设计模式里,条件工作流通常出现在两个层次:业务规则层:根据输入内容、上下文状态、用户参数做判断,比如“消息里包含城市和时间就查天气”,这种判断不依赖模型,速度快、结果稳定。模型推导层:机器无法用规则覆盖所有表达,比如“明天会不会下雨”和“明天适合带伞吗”在表达上完全不同,但意图可能接近。这种语义判断需要让模型参与决策。条件工作流的价值在于把这两层组合起来:先用规则拦截能明确判断的请求,再把模糊的请求交给模型路由,最后根据路由结果选择不同的执行分支。这里的“条件”可以是静态规则、模型输出、工具执行结果,甚至可以跨越多个节点形成一个循环,直到某个终止条件满足才退出。与顺序工作流相比,条件工作流最大的变化是:执行路径不是预编译好的单选题,而是运行时依赖输入内容和中间结果动态生成的。这也意味着,你的代码一旦写成,就要面对“模型判断错了怎么办”的局面。2. 条件工作流的实现路线怎么选2.1 规则预检:先拦截,再问模型我习惯在任何条件工作流的最前面加一道规则预检,而不是把所有判断都丢给模型。原因很简单:规则预检免费、确定、无延迟。比如用户消息里包含“天气”并且包含一个城市名,或者包含“提醒我”和时间短语,这些特征可以通过一个很小的正则表达式或者关键词表命中,根本不需要调用模型。规则预检适合处理高频且表达相对固定的场景。它的缺点也明显:自然语言变化太多,完全靠规则覆盖所有情况不现实。比如用户说“今天出门穿什么?”,这句话里没有“天气”两个字,但本质上是天气查询。这时规则会漏掉,必须让模型介入。所以我的设计原则是:规则预检负责“能确定的就确定”,模型路由负责“规则拿不准的再兜底”。两者结合,能在成本、延迟和准确率之间取得一个比较平衡的位置。2.2 模型路由:让 LLM 决策意图模型路由的核心思想是:把路由本身当成一个 Agent 任务,让模型输出一个结构化结果,然后用这个结果驱动代码分支。常见的做法是让模型返回一个枚举值或一个 JSON 对象,比如{"action":"WEATHER_QUERY","reason":"用户询问明天天气"}。LangChain4j 里可以用接口方法和返回对象的方式让模型直接映射到 Java 类,省去手写 JSON 解析的麻烦。模型路由的优势是覆盖面广、语义理解强,几乎不用维护规则表。但代价也很现实:每次路由多一次 LLM 调用,增加延迟和成本;如果返回格式不稳定,下游直接解析失败;如果枚举值太多、划分太细,模型也容易混淆。后面我会专门讲怎么避免这几个坑。另外要注意,模型路由不只是用在大入口做意图分类,还可以用在流程中间。比如“根据当前工具的返回结果决定是否再做一次检索”,这就是循环条件的一种体现。2.3 状态机与循环:处理更复杂的状态转移条件工作流并不只有“一个入口 + 几个分支”这种形态。还有一种更复杂的形态是状态机:整个 Agent 会话会经历多个状态,每个状态都是一个独立的节点,在不同节点间根据条件转移。比如一个售后工单助手,状态可能是收集问题 -> 确认诉求 -> 给方案 -> 回访结束。用户在“确认诉求”状态下输入的内容,处理方式和“收集问题”状态下完全不一样。这种场景如果只靠入口路由一次,无法解决,因为后续每一步都需要根据状态选择行为。LangChain4j 本身没有专门的工作流引擎,但状态机的逻辑可以自己用 Java 代码维护:定义一个状态枚举,维护状态转移表,在每次接收用户输入时先判断当前状态,再路由到对应处理节点。循环也可以理解为一种特殊的状态机——“状态”在同一个节点反复执行,直到满足退出条件。3. 基于 LangChain4j 实现一个条件工作流3.1 依赖准备与模型接入我使用的版本是 LangChain4j 1.x,依赖比较简单。如果你用 Maven,在pom.xml里加核心包和应用框架包即可:<dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j</artifactId> <version>1.0.0</version> </dependency> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-core</artifactId> <version>1.0.0</version> </dependency>模型接入我习惯写一个工厂方法,把具体模型服务的配置和环境变量绑在一起,这样切换模型服务时不用改业务代码。下面是示意:public class ModelFactory { public static ChatLanguageModel createChatModel() { String baseUrl = System.getenv("MODEL_BASE_URL"); String apiKey = System.getenv("MODEL_API_KEY"); String modelName = System.getenv("MODEL_NAME"); // 这里使用兼容主流协议的消息模型实现,具体厂商类放在基础设施层 return ChatLanguageModel.builder() .baseUrl(baseUrl) .apiKey(apiKey) .modelName(modelName) .build(); } }依赖注入或者单例管理都可以,核心是业务代码只面对ChatLanguageModel接口,不会因为底层模型变动而跟着改动。3.2 定义路由结果与分支动作路由结果最好用一个枚举加一个数据类来表达。枚举控制分支走向,数据类可以携带模型给出的理由,便于日志排查。public enum WorkflowAction { WEATHER_QUERY, REMINDER_SET, GREETING, UNKNOWN } public class WorkflowDecision { private WorkflowAction action; private String reason; public WorkflowAction getAction() { return action; } public void setAction(WorkflowAction action) { this.action = action; } public String getReason() { return reason; } public void setReason(String reason) { this.reason = reason; } }枚举数量一定要克制。我见过有人把路由枚举拆成二十多个分支,结果模型经常分错。一般情况下,一个大入口的顶层路由控制在五到七个动作以内会比较稳。如果业务确实很多,可以考虑先粗分再细分,用多级路由。3.3 编写路由接口及分支 AgentLangChain4j 的AiServices可以让我们通过接口定义 Agent,非常直观。我先定义一个路由接口:public interface DecisionRouter { @SystemMessage(""" 你是任务路由引擎。根据用户消息判断意图,只输出JSON格式。 可选动作:WEATHER_QUERY、REMINDER_SET、GREETING、UNKNOWN。 JSON格式:{"action":"动作枚举","reason":"选择理由"} 不要输出其他文字。 """) WorkflowDecision route(@UserMessage String userMessage); }这里让返回类型直接是WorkflowDecision,LangChain4j 会尝试把模型输出解析成 Java 对象。返回类型设计成 POJO,比返回一个字符串再手工解析省事得多。接下来定义分支 Agent,也就是真正干活的小助手:public interface WeatherAgent { @SystemMessage("你是天气问答助手,负责根据用户问题提供简洁的天气建议。") String handle(@UserMessage String userMessage); } public interface ReminderAgent { @SystemMessage("你是提醒助手,负责把用户请求整理成提醒信息并确认。") String handle(@UserMessage String userMessage); }这样设计的好处是每个分支的提示词互相独立,互不干扰。后续改天气助手的提示词,完全不会影响提醒助手。3.4 主流程编排代码实现有了路由接口和分支 Agent,就可以写主流程了。我把整个流程放在一个类里,叫ConditionalWorkflow。入口方法先做规则预检,再走模型路由,最后分发到具体分支。public class ConditionalWorkflow { private final DecisionRouter router; private final WeatherAgent weatherAgent; private final ReminderAgent reminderAgent; public ConditionalWorkflow(ChatLanguageModel model) { this.router = AiServices.builder(DecisionRouter.class) .chatLanguageModel(model) .build(); this.weatherAgent = AiServices.builder(WeatherAgent.class) .chatLanguageModel(model) .build(); this.reminderAgent = AiServices.builder(ReminderAgent.class) .chatLanguageModel(model) .build(); } public String execute(String userMessage) { // 第一步:规则预检,能确定的直接分流 WorkflowAction preAction = PreRule.match(userMessage); if (preAction != null) { return dispatch(preAction, userMessage); } // 第二步:模型路由 WorkflowDecision decision = router.route(userMessage); WorkflowAction action = decision.getAction(); if (action == null || action == WorkflowAction.UNKNOWN) { return fallback(userMessage); } return dispatch(action, userMessage); } private String dispatch(WorkflowAction action, String userMessage) { switch (action) { case WEATHER_QUERY: return weatherAgent.handle(userMessage); case REMINDER_SET: return reminderAgent.handle(userMessage); case GREETING: return greetingReply(userMessage); default: return fallback(userMessage); } } private String greetingReply(String userMessage) { return "你好,有需要随时告诉我。"; } private String fallback(String userMessage) { return "抱歉,我还没有完全理解你的需求,可以换个说法试试。"; } }这段代码最关键的是把“判断”和“执行”拆开。execute方法里没有业务判断逻辑,它只知道“决定完了就分发”;真正的判断逻辑分散在规则预检和模型路由里。这样后续加一个新分支,只需要新增一个枚举值、一个分支接口、一个 case 分支,改动集中且可控。PreRule.match是我抽出来的规则预检工具,内部就是关键词和正则,这里不展开。实际项目中我建议把它做成可配置的规则表,而不是硬编码在代码里。3.5 循环重试与终态控制条件工作流中还有一个高频场景:循环。比如“生成方案 -> 自动评估 -> 不合格则重写”,直到评估通过或者达到最大次数。这个循环本质上也是条件判断驱动的,只不过判断对象是模型输出结果。我用一个简化例子说明:写一段文案,然后用评测模型打一个合格/不合格标记,循环三次。public interface WriterAgent { String write(@UserMessage String instruction); } public interface JudgeAgent { @SystemMessage("你是质检员,判断文案是否符合要求,只输出JSON:{\"passed\":true,\"reason\":\"...\"}") JudgeResult evaluate(@UserMessage String instructionAndText); } public static class JudgeResult { private boolean passed; private String reason; // getters/setters }循环流程:public String generateWithQualityCheck(String instruction) { String answer = null; for (int i = 0; i < 3; i++) { answer = writerAgent.write(instruction); JudgeResult result = judgeAgent.evaluate("需求:" + instruction + "\n文案:" + answer); if (result.isPassed()) { return answer; } System.out.println("第" + (i + 1) + "次未通过,原因:" + result.getReason()); } return answer; }这里有两个必须注意的点。第一,必须有最大迭代次数,否则模型一直不合格就会无限循环,既浪费 token 又拖垮服务。第二,最后一次循环即使不合格也应返回结果,不要返回空字符串。业务上“拿一个不够完美的结果”比“什么都没有”更好兜底。4. 实操中值得注意的细节与排查经验4.1 不要让模型输出“自由格式”的意图我第一次做模型路由时,直接在系统提示词里写“请先判断用户的意图,然后给出回答”,把路由和回答混在一个接口里。结果模型输出一大堆解释文本,我很难稳定地拿到意图字段。后来改成“路由专用接口只输出 JSON”,情况立刻稳定了很多。LangChain4j 在把模型输出映射到 POJO 时,要求模型输出尽量贴近目标结构。你在系统提示词里给一个两个范例,效果会更好。还有一点:WorkflowDecision里的reason字段不要省略,它不影响分支逻辑,但能让排查问题的人立刻看到模型为什么选了这条路。没有reason,出问题时只能瞎猜。4.2 路由 Prompt 越短,越不容易翻车路由接口的系统提示词不需要写太多业务背景。它只需要知道“有哪些动作可选、各自代表什么含义、输出成什么格式”。写多了反而容易让模型花更多 token 去理解无关上下文。我给路由提示词定过三条原则:动作枚举必须在提示词里原样列出,并且说明一定是 JSON 字段值。枚举不超过七个,否则模型混淆率明显上升。每个动作配一个极简解释,但不要贴大段示例数据。如果你的路由需求确实复杂,不要硬塞到一段提示词里。拆成多级路由,先用一个粗粒度接口判断大类,再在各自分支里用更细的提示词判断小类,整体准确率通常更高。4.3 排查条件工作流问题的日志套路条件工作流出问题时,最难的是不知道在哪一环断的。我现在的习惯是给每个关键节点加结构化日志,至少记录四个信息:用户输入原文、路由决策、决策理由、分支执行耗时。实际日志长这样:input=明天上海有雨吗 preRule=null decision=WEATHER_QUERY reason=用户询问天气情况 branch=weatherAgent costMs=842有了这类日志,定位问题就快很多。如果预检命中了但结果不对,说明规则写太宽;如果预检没命中,模型路由选错,那就去看 reason;如果分支输出不对,问题往往在对应 Agent 的提示词而不是路由。这条经验帮我省过很多次时间,建议你把日志从一开始就埋好,不要等上线后出问题再补。4.4 常见问题速查表现象可能原因解决思路模型路由返回中文而非枚举系统提示词约束不够强强化“只输出 JSON”和枚举取值,给一个输出范例分支 Agent 回答跑题路由正确但分支提示词与任务不匹配检查对应分支 Agent 的系统提示词JSON 解析频繁失败模型输出中混入了多余文本改用接口返回 POJO;增加一次重试解析循环不结束缺少最大迭代次数或退出条件增加计数器,最多迭代 N 次后返回当前结果路由枚举太多,准确率下降顶层分支划分过细拆成多级路由,或把枚举数量控制在七个以内条件工作流整体响应变慢每次路由都额外调用一次模型前面加规则预检,能确定的请求不走模型路由模型选错动作但 reason 正确提示词语义模糊调整动作解释,增加 positive/negative few-shot 示例一个关于条件粒度的小经验这个系列写到现在,我个人最大的体会是:条件工作流的复杂度和模型能力成反比。模型能力越强,越可以用更粗的粒度让它自己决策;模型能力弱,就越需要把条件写显式,多依赖规则预检和状态机兜底。另外,不管条件工作流设计得多漂亮,一定要给每个分支留一个容错出口。我见过太多次因为模型在某个分支上输出异常,导致整条链路崩掉的情况。后续如果想继续扩展,可以顺着这个条件路由的思路,把缓存、超时、熔断这些稳定性策略加进来,那又是另一个主题了。