欢迎光临
我们一直在努力

给 Dify 应用装上“分岔路“:什么时候写规则,什么时候让模型判断 | Dify 系列 · EP3

城北 · 2026-09-04

一个只能回答正确问题的应用,离"可靠"还很远。

深夜书房里一张地图在暖光下分出两条路径,一条笔直规则、一条柔和弯曲,隐喻规则判断与模型判断两种分支方式

同一张地图,从这一集开始分出两条路:一条交给写死的规则,一条交给模型判断。

用户输入"法国",它知道首都是巴黎;可用户说"你好",它可能一本正经地介绍法国;用户让它"帮我写段代码",它也可能顺手接单。模型看起来无所不能,应用却因此失去了边界。

真正的产品不会假设每个用户都按照设计者预想的方式说话。它必须先判断:这句话属于我的职责范围吗?如果属于,应该走哪条处理路径?如果不属于,又该怎样明确拒绝,而不是让模型自由发挥?

从这一刻开始,我们不再只关心"模型怎样回答",而要开始设计"应用怎样决定下一步"。

前情提要 上一集我们做了一个能够回答国家首都的最小 Chatflow 应用,这一集要解决的是怎样识别不同类型的输入,并把它们送往不同处理路径。想看完整讲解可以回看 EP2《从零搭出第一个 Dify 应用:用云版完成一次 Chatflow 上线》 的「下一步:在这条骨架上增加真实复杂度」部分——不看也不影响读完这一集。

#主题一句话状态
1 AI 应用平台通识与选型 认知/选型判断力:这个领域是什么、要不要学、怎么定位 已发布
2 云版搭第一个 Chatflow 应用 5 种应用类型、Start/LLM/Answer、Prompt 分层、发布分发 已发布
3 给应用装上"分岔路" If/Else、问题分类器:写死的规则 vs 交给模型判断 ← 你在这里
4 变量,这个系列真正的地基 会话变量、环境变量、变量聚合器/赋值器 未发布
5 把外部世界接进来 HTTP Request、Code 节点、参数提取器 未发布
6 知识库与 RAG 实战 文档处理/分段/检索策略/Rerank 未发布
7 批量处理与循环 Iteration、Loop、列表操作 未发布
8 工具与 Agent 工具市场、自定义工具、Function Calling/ReAct、双向 MCP 未发布
9 从 Demo 到生产:运维篇 DSL 版本管理、执行日志、标注、可观测性、权限 未发布
10 私有化部署实战 Docker Compose、.env、报错排查 未发布

01一条直线,为什么不够用了

上一集的"国家信息小助手"只有三个节点:

开始 → LLM → 直接回复

用户输入一条消息,Start 接收输入,LLM 根据 System Prompt 生成答案,Answer 再把 LLM 的文本输出交付给用户。

这条流程很干净,也有一个明显的问题:无论用户输入什么,消息都会进入同一个 LLM 节点。

输入"法国",进入 LLM。

输入"你好",进入 LLM。

输入"帮我写一段 Python 爬虫",还是进入 LLM。

我们给 LLM 的 System Prompt 是:

你是一个国家小百科助手。用户会提到一个国家的名字,你需要用一句话说出这个国家的首都,语气亲切自然。

这段 Prompt 描述了正常输入,却没有建立流程层面的边界。遇到问候、乱码或无关要求时,模型只能自行揣测应该怎样处理。

当然,我们可以继续修改 Prompt:

如果用户没有输入国家名称,请不要回答其他问题……

这可能改善结果,但它仍然把"输入属于哪一类"和"这一类应该怎样回答"揉进了同一次模型调用。分类失败和回答失败混在一起,流程也无法对不同输入采取真正不同的动作。

更清楚的做法,是先把判断从回答中拆出来:

  • 先判断用户输入属于哪一类;
  • 再让不同类别进入不同路径;
  • 只有确实需要模型回答的问题,才进入原来的 LLM 节点。
  • 这就是控制流。

    所谓控制流,不是让应用多画几根线,而是让它拥有选择下一步的能力。原来的 Chatflow 像一条从入口直达出口的走廊;现在,我们要在走廊中间装上第一个岔路口。

    02先确定三条路,而不是先找节点

    在画布上添加分支以前,应该先用业务语言说清楚我们要区分什么。

    这次把用户输入分成三类。

    第一类是"国家名称"。

    只要用户明确提到一个国家,例如"法国""日本"或"德国的首都是什么?",就继续走上一集留下的主流程:

    LLM → 直接回复

    原有 LLM 的模型、System Prompt 和输出关系都保持不变。我们不是重做应用,而是在它前面增加一道分流。

    第二类是"问候或闲聊"。

    例如"你好""谢谢""在吗"。这些内容与国家知识无关,但表达的是正常的社交意图,不适合冷冰冰地报错。

    它们会进入一个新增的直接回复节点,返回固定引导语:

    你好!我是国家信息小助手,你可以告诉我任意一个国家的名字,我会告诉你它的首都。

    第三类是"无法识别"。

    这里包括乱码、明显无关的话题,以及超出应用职责的任务,例如"帮我写代码"。

    它们会进入另一个新增的直接回复节点:

    我目前只能回答国家首都相关的问题。你可以试试输入"法国"或"日本的首都是什么?"

    问候和无法识别都没有必要再调用负责回答首都的 LLM。固定回复更便宜、更稳定,也更容易控制措辞。

    于是,新的主结构变成:

    开始 → 问题分类器 → 三条独立路径

    • 国家名称 → If/Else 兜底检查 → 原有 LLM → 原有直接回复
    • 问候或闲聊 → 新增直接回复
    • 无法识别 → 新增直接回复

    这里有一个值得提前记住的设计习惯:

    先用自然语言定义每条路径的职责,再选择节点实现它。

    如果一开始只想着"这一集要学问题分类器,所以先拖一个问题分类器",很容易得到一张节点很多、边界却含糊的画布。

    03第一个岔路口:让问题分类器理解用户在说什么

    我们面对的第一个判断是:"这句话到底在表达什么?"

    这不是简单的字符串比较。

    "法国"是国家名称,"法国的首都是什么?"同样是在询问国家;"去法国旅行要准备什么"虽然提到了国家,却不是当前助手负责的首都问题;"你好呀"与"在吗"字面上完全不同,语义上却都属于日常问候。

    如果试图用关键词穷举这些表达,规则会迅速膨胀,而且永远追不上自然语言的变化。

    这正是问题分类器适合处理的场景。

    问题分类器不会只检查一句话里有没有某几个固定字符,而是借助 LLM 的语义理解,把输入归入预先定义的类别,然后把流程路由到对应分支。

    在"国家信息小助手"的 Start 节点之后插入一个问题分类器,并让 Start 的用户输入先进入它。

    改造后的国家信息小助手 Chatflow 完整画布:开始节点连接问题分类器,分类器分出国家名称、问候或闲聊、无法识别三条路径,国家名称分支再经条件分支连到原有的 LLM 和直接回复

    改造后的完整画布:一个入口,四条出口,各管各的职责。

    问题分类器的第一项关键配置,是待分类的输入变量。

    本例选择 Chatflow 的用户输入,也就是 sys.query。它代表用户本轮发来的消息。问题分类器也可以接收任何前置节点提供的文本变量,但这里没有必要绕路:我们要判断的就是用户刚刚说了什么。

    第二项关键配置,是用于分类的 LLM 模型。

    简单、边界清楚的分类任务,可以优先选择响应较快的模型;类别接近、语义差异细微时,再考虑能力更强的模型。模型越强不等于设计越好。若类别描述本身互相重叠,再强的模型也只能替含糊的产品规则猜答案。

    这个案例只有三个类别,重点不在追求复杂推理,而在把分类边界写清楚。

    问题分类器节点配置面板,模型选择 gpt-4o-mini,输入变量选择用户输入 sys.query

    先定两件事:谁来读这句话(模型),读的是哪句话(sys.query)。

    04分类器真正读的是"类描述"

    问题分类器中的每个类别,至少要处理好两个部分:类标题和类描述。

    类标题主要显示在画布上。把它改成"国家名称""问候或闲聊""无法识别",协作者看到连线时就能理解各分支的用途。

    真正影响判断的是类描述。LLM 会把用户输入与这些描述进行比较,因此描述不能只是标题的重复。

    我们的三个类别可以这样定义。

    类别一:国家名称

    类标题:

    国家名称

    类描述:

    用户明确提到一个国家,并希望获得该国家的首都信息。包括只输入国家名称,或用自然语言询问该国首都。排除旅行建议、历史介绍、代码编写等虽然可能提到国家、但并非询问首都的内容。

    这段描述有两个作用。

    一是扩大正常表达的覆盖范围。"法国"和"法国的首都是什么?"形式不同,但都应该进入同一路径。

    二是明确排除条件。仅仅出现国家名,不代表它就属于这个应用能够处理的问题。"帮我写一个介绍法国旅游的网站"提到了法国,却不该进入首都回答节点。

    类别二:问候或闲聊

    类标题:

    问候或闲聊

    类描述:

    用户正在进行简单问候、致谢或确认助手是否在线,例如"你好""谢谢""在吗"。这些内容不要求回答国家首都,也不包含其他明确任务。

    这里要把"正常社交表达"和"无法处理的任务"分开。

    两者都不进入原来的 LLM,但产品反馈应该不同。用户说"谢谢"时,友好引导比"无法识别"更自然;用户要求写代码时,则应该明确说明能力边界。

    类别三:无法识别

    类标题:

    无法识别

    类描述:

    用户输入的是乱码、含义不清的内容、与国家首都无关的话题,或要求助手完成其他任务,例如写代码、回答天气、提供旅行建议。排除明确的问候、致谢和在线确认。

    这个类别承担的是剩余边界,但不能只写"其他"。

    "其他"看似省事,实际上没有给模型足够的判断依据。告诉分类器哪些内容应该进入这里、哪些相近内容需要排除,通常比一个模糊的兜底标签更可靠。

    问题分类器的三个分类完整配置:国家名称、问候或闲聊、无法识别,每个分类都写明了包含范围和排除边界

    三个分类的完整描述——每一条都写了“是什么”,也写了“不是什么”。

    05用"指导说明"解决类别之间的灰色地带

    除了每一类自己的描述,问题分类器还提供"指导说明",用于补充整体分类规则和边界情况。

    这里可以写:

    优先判断用户是否明确询问某个国家的首都。仅仅提到国家名称,但实际要求是旅行建议、历史介绍、代码编写或其他任务时,不归入"国家名称"。简单问候、致谢和确认是否在线归入"问候或闲聊";其余不属于首都查询的内容归入"无法识别"。

    为什么还要加这一层?

    因为类别描述是分别解释每个抽屉里放什么,指导说明则是在告诉分类器:当一件东西似乎可以放进两个抽屉时,优先按什么原则处理。

    例如:

    法国有哪些好玩的地方?

    它明确提到了国家,却没有询问首都。如果"国家名称"只写成"任何包含国家名称的输入",这句话就很可能被送进原来的 LLM。可原 LLM 的职责是回答首都,它最后可能生硬地回复"法国的首都是巴黎",也可能违背 Prompt 开始介绍景点。

    把"是否在询问首都"写成优先判断标准,分类器才有机会把它送到"无法识别"。

    再看另一句:

    你好,日本的首都是什么?

    它既包含问候,也包含明确的首都问题。指导说明中的"优先判断是否明确询问首都",可以帮助分类器将其归入"国家名称",而不是被开头的"你好"截走。

    问题分类器的价值来自语义理解,但语义理解不等于读心。类别定义越清楚,模型越容易稳定执行你的意图。

    06给两个非业务分支固定回复

    问题分类器配置完成后,把"问候或闲聊"连接到一个新增的直接回复节点。

    回复内容可以固定为:

    你好!我是国家信息小助手,你可以告诉我任意一个国家的名字,我会告诉你它的首都。

    再把"无法识别"连接到另一个新增的直接回复节点:

    我目前只能回答国家首都相关的问题。你可以试试输入"法国"或"日本的首都是什么?"

    问候或闲聊分支连接到问候引导节点,无法识别分支连接到无法识别兜底节点,两个直接回复节点都已填好固定文案

    问候引导、无法识别兜底:两条不需要模型回答的路,各自挂了一句现成的话。

    这两个节点不需要引用 LLM 输出,因为它们返回的是设计者事先确定的产品文案。

    固定回复并不比模型生成"低级"。恰恰相反,在内容确定、变化价值很低的地方坚持使用固定文本,是一种更成熟的工程选择。

    它至少带来三点好处:

    • 输出措辞可预测,不会每次变化;
    • 不需要为这一步再次调用 LLM;
    • 产品边界清楚,不会因为模型热心而越权。

    AI 应用不是模型调用越多越智能。真正可靠的设计,是把不确定性留给确实需要理解和生成的地方。

    07第二个岔路口:用 If/Else 做确定性检查

    问题分类器解决的是语义问题。接下来,我们故意在"国家名称"分支后面再放一个 If/Else,演示另一种完全不同的分支方式。

    这次判断的问题是:

    sys.query 是否为空?

    它不需要理解用户想表达什么,也不需要模型判断"这算不算一个国家"。变量里有没有内容,是一个确定事实。

    因此,这一步更适合交给 If/Else。

    If/Else 节点会根据定义好的条件评估变量,再决定工作流进入哪条路径。它支持三种分支:

    • IF:首先检查的主条件;
    • ELIF:按顺序检查的附加条件,可以有多个;
    • ELSE:前面的条件都不匹配时进入的备选路径。

    本例只需要 IF 和 ELSE。

    在"国家名称"分支后插入 If/Else,选择 sys.query 作为待检查变量,把 IF 条件设为"为空"。

    如果条件成立,就进入一个兜底直接回复节点:

    我没有收到有效内容,请输入一个国家名称,例如"法国"。

    如果条件不成立,也就是输入非空,则从 ELSE 继续进入上一集原有的 LLM,再由原有直接回复节点返回首都答案。

    条件分支节点配置面板,IF 条件为用户输入 query 为空,ELSE 连接到 LLM 节点,IF 连接到空输入兜底节点

    If/Else 不需要理解语言,只需要一个能算清楚的条件。

    严格来说,这个检查放在问题分类器之后,正常情况下几乎不会触发。

    一条空输入如果已经被问题分类器判定为"国家名称",说明前面的分类结果本身就很反常。实际生产设计中,如果"空输入"是必须拦截的硬性规则,更自然的位置通常是问题分类器之前:先用 If/Else 拦掉空值,再把非空文本交给分类器。

    这里仍然把它放在"国家名称"之后,是为了在不改变本集既定主结构的前提下,清楚展示两件事:

    第一,语义分支与规则分支可以嵌套组合。

    第二,兜底不一定意味着它经常触发。它也可以用于保护下游节点,明确表达"只有满足这个前提,才允许继续"。

    但要警惕为了"看起来保险"而无限堆叠永远不可能触发的判断。教学中保留这个节点是为了理解机制;真正上线前,则应该结合执行日志决定哪些防线有实际价值。

    08If/Else 不只会判断空值

    这次只配置一个最小条件,但 If/Else 能表达的规则不止于此。

    面对文本变量,可以检查是否包含或不包含某段文字、是否以某段文字开头或结尾、是否精确匹配或不匹配。

    面对适用的变量和值,可以判断为空或非空,也可以进行大于、小于、等于和不等于等比较。

    多个条件还可以通过两种逻辑组合:

    • AND:所有条件都满足,分支才成立;
    • OR:任意一个条件满足,分支就成立。

    它引用的也不一定是用户输入。LLM 响应、API 调用结果,以及任何前置工作流节点提供的输出变量,都可以成为条件判断的依据。

    例如,一条更长的流程可以先调用外部接口,再根据返回结果是否为空决定继续生成回答还是进入错误提示;也可以检查某段文本是否包含指定标记,再决定下一步交给哪个节点。

    这些场景虽然不同,本质上都在做同一件事:

    把能够明确写成条件的判断,留在确定规则里。

    09同样叫"分支",背后其实是两种判断哲学

    问题分类器和 If/Else 在画布上都会拉出多条线,看起来像两种相近的节点。

    实际上,它们解决的是两类完全不同的问题。

    If/Else 问的是:

    一个明确条件是否成立?

    问题分类器问的是:

    这段自然语言在语义上更接近哪一类?

    "sys.query 是否为空"有唯一、可计算的答案。让 LLM 判断既浪费 Token,也引入了本不该存在的不确定性。

    "这句话是不是在询问国家首都"则没有一个可靠的固定字符串规则。用户可以说"日本""日本的首都是哪里""东京是哪国的首都",也可能说"推荐几个日本景点"。只靠包含、开头、结尾和精确匹配,很难正确覆盖自然语言。

    两者的分工可以归纳为:

    判断问题更合适的节点原因
    用户输入是否为空 If/Else 条件明确,可确定计算
    文本是否精确匹配某个固定值 If/Else 不需要语义理解
    数值是否超过阈值 If/Else 规则稳定、结果可预测
    用户是在问国家首都还是闲聊 问题分类器 表达方式多样,需要语义理解
    一段话属于投诉、咨询还是表扬 问题分类器 类别取决于整体含义
    无关任务是否应该被拦截 问题分类器 很难穷举所有自然语言形式

    还有一个成本差异不能忽略。

    If/Else 只是评估已经定义好的条件。问题分类器背后需要 LLM 进行语义判断,因此会带来模型调用所对应的 Token、延迟和不确定性。

    所以,节点选择不是"哪个更智能",而是判断本身需不需要智能。

    能写成稳定规则,就写成规则;只有无法用确定条件可靠表达时,才把判断交给模型。

    10把判断交给模型,也要保留产品责任

    问题分类器比关键词规则灵活,但它不是一个绝对正确的路由器。

    类别描述会影响结果,模型选择会影响结果,边界模糊的输入也可能在不同类别之间摇摆。应用设计者不能因为用了"智能分类",就把判断责任全部推给模型。

    至少应该准备一组覆盖正常输入和边界输入的测试样例。

    "国家名称"分支可以测试:

    • 法国
    • 日本的首都是什么?
    • 请告诉我德国的首都
    • 你好,日本的首都在哪里?

    "问候或闲聊"分支可以测试:

    • 你好
    • 在吗
    • 谢谢
    • 你是谁

    "无法识别"分支可以测试:

    • asdfgh
    • 帮我写一段 Python 代码
    • 法国有哪些旅游景点?
    • 今天天气怎么样?
    • 给我介绍一下罗马帝国

    If/Else 的空值兜底则应该单独验证:当 sys.query 没有有效内容时,它是否进入预期的固定回复路径;当输入非空时,是否继续进入原有 LLM。

    Dify 预览面板里连续测试三类输入:德国被问题分类器判定为国家名称并经 LLM 正确回复柏林;你好被判定为问候或闲聊并触发问候引导;帮我写一段 Python 代码被判定为无法识别并触发无法识别兜底

    三句话,三条路:分类器把“德国”“你好”“帮我写代码”分别送去了它们该去的地方。

    这组测试的重点不是看回答文案是否足够漂亮,而是检查路由是否正确。

    如果"法国有哪些旅游景点?"进入了国家名称分支,不要先修改原有 LLM 的 Prompt。问题发生在上游分类,就应该先检查"国家名称"的类描述是否太宽,指导说明是否缺少排除规则。

    如果"你好,日本的首都在哪里?"进入了问候分支,则需要明确多意图输入的优先级,而不是责怪模型没有理解你脑中的默认规则。

    沿着数据路径定位错误,仍然是上一集建立的排错方法。区别只在于,流程从一条直线变成了有多个出口的道路网。

    11这次改造,真正增加的不是三个节点

    改造前,"国家信息小助手"只有一种命运:任何输入都进入 LLM。

    改造后,应用开始拥有职责边界:

    • 明确的国家首都问题,交给 LLM 生成回答;
    • 正常问候,用固定文案友好引导;
    • 无关或无法识别的请求,用固定文案明确拒绝;
    • 确定性的空值检查,交给 If/Else 处理。

    这张画布第一次把"模型能力"和"产品逻辑"分开了。

    模型擅长理解自然语言,所以让问题分类器判断输入的语义类别;程序擅长执行明确规则,所以让 If/Else 判断变量是否为空;产品已经确定的提示语,则直接写进 Answer,不必每次请模型重新创作。

    这也是从 Demo 走向可靠应用时最重要的转变之一:

    不要把整套产品寄托在一段越来越长的 Prompt 上。先拆出哪些是语义判断,哪些是确定规则,哪些是固定交付,再让不同节点各自承担最合适的职责。

    12分岔路的终点,是更清楚的责任边界

    这一集只增加了一个核心能力:让同一条用户输入可以走向不同路径。

    我们用了两种做法。

    问题分类器依靠 LLM 的语义理解,适合处理"这句话属于哪一种意图"这类很难穷举表达方式的问题。它的关键不只是添加类别,而是写清楚每一类的包含范围、排除范围和冲突时的优先级。

    If/Else 根据变量和明确条件决定路径,适合处理空值、精确匹配、文本包含和数值比较等确定性问题。它更省 Token,也更容易预测和测试。

    两者不是竞争关系,而是可以嵌套使用的两层控制:先在需要理解语言的地方使用模型,再在能够写死规则的地方坚持使用规则。

    如果只记住一句话,可以记住这个判断标准:

    能准确写成"如果条件成立,就走这条路"的,用 If/Else;必须先理解"用户到底在说什么"的,用问题分类器。

    下一集,我们会继续向工作流内部走,专门讲"变量"——这个系列真正的地基:数据怎样在节点之间流动,会话变量和环境变量分别解决什么问题,变量聚合器与赋值器又为什么会决定复杂流程能不能维护。


    本文首发于我的个人站 chengbei.org,同步转载于此。

    赞(0)
    未经允许不得转载:171主机测评 » 给 Dify 应用装上“分岔路“:什么时候写规则,什么时候让模型判断 | Dify 系列 · EP3
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址