城北 · 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:
如果用户没有输入国家名称,请不要回答其他问题……
这可能改善结果,但它仍然把"输入属于哪一类"和"这一类应该怎样回答"揉进了同一次模型调用。分类失败和回答失败混在一起,流程也无法对不同输入采取真正不同的动作。
更清楚的做法,是先把判断从回答中拆出来:
这就是控制流。
所谓控制流,不是让应用多画几根线,而是让它拥有选择下一步的能力。原来的 Chatflow 像一条从入口直达出口的走廊;现在,我们要在走廊中间装上第一个岔路口。
02先确定三条路,而不是先找节点
在画布上添加分支以前,应该先用业务语言说清楚我们要区分什么。
这次把用户输入分成三类。
第一类是"国家名称"。
只要用户明确提到一个国家,例如"法国""日本"或"德国的首都是什么?",就继续走上一集留下的主流程:
LLM → 直接回复
原有 LLM 的模型、System Prompt 和输出关系都保持不变。我们不是重做应用,而是在它前面增加一道分流。
第二类是"问候或闲聊"。
例如"你好""谢谢""在吗"。这些内容与国家知识无关,但表达的是正常的社交意图,不适合冷冰冰地报错。
它们会进入一个新增的直接回复节点,返回固定引导语:
你好!我是国家信息小助手,你可以告诉我任意一个国家的名字,我会告诉你它的首都。
第三类是"无法识别"。
这里包括乱码、明显无关的话题,以及超出应用职责的任务,例如"帮我写代码"。
它们会进入另一个新增的直接回复节点:
我目前只能回答国家首都相关的问题。你可以试试输入"法国"或"日本的首都是什么?"
问候和无法识别都没有必要再调用负责回答首都的 LLM。固定回复更便宜、更稳定,也更容易控制措辞。
于是,新的主结构变成:
开始 → 问题分类器 → 三条独立路径
- 国家名称 → If/Else 兜底检查 → 原有 LLM → 原有直接回复
- 问候或闲聊 → 新增直接回复
- 无法识别 → 新增直接回复
这里有一个值得提前记住的设计习惯:
先用自然语言定义每条路径的职责,再选择节点实现它。
如果一开始只想着"这一集要学问题分类器,所以先拖一个问题分类器",很容易得到一张节点很多、边界却含糊的画布。
03第一个岔路口:让问题分类器理解用户在说什么
我们面对的第一个判断是:"这句话到底在表达什么?"
这不是简单的字符串比较。
"法国"是国家名称,"法国的首都是什么?"同样是在询问国家;"去法国旅行要准备什么"虽然提到了国家,却不是当前助手负责的首都问题;"你好呀"与"在吗"字面上完全不同,语义上却都属于日常问候。
如果试图用关键词穷举这些表达,规则会迅速膨胀,而且永远追不上自然语言的变化。
这正是问题分类器适合处理的场景。
问题分类器不会只检查一句话里有没有某几个固定字符,而是借助 LLM 的语义理解,把输入归入预先定义的类别,然后把流程路由到对应分支。
在"国家信息小助手"的 Start 节点之后插入一个问题分类器,并让 Start 的用户输入先进入它。

改造后的完整画布:一个入口,四条出口,各管各的职责。
问题分类器的第一项关键配置,是待分类的输入变量。
本例选择 Chatflow 的用户输入,也就是 sys.query。它代表用户本轮发来的消息。问题分类器也可以接收任何前置节点提供的文本变量,但这里没有必要绕路:我们要判断的就是用户刚刚说了什么。
第二项关键配置,是用于分类的 LLM 模型。
简单、边界清楚的分类任务,可以优先选择响应较快的模型;类别接近、语义差异细微时,再考虑能力更强的模型。模型越强不等于设计越好。若类别描述本身互相重叠,再强的模型也只能替含糊的产品规则猜答案。
这个案例只有三个类别,重点不在追求复杂推理,而在把分类边界写清楚。

先定两件事:谁来读这句话(模型),读的是哪句话(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/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。

三句话,三条路:分类器把“德国”“你好”“帮我写代码”分别送去了它们该去的地方。
这组测试的重点不是看回答文案是否足够漂亮,而是检查路由是否正确。
如果"法国有哪些旅游景点?"进入了国家名称分支,不要先修改原有 LLM 的 Prompt。问题发生在上游分类,就应该先检查"国家名称"的类描述是否太宽,指导说明是否缺少排除规则。
如果"你好,日本的首都在哪里?"进入了问候分支,则需要明确多意图输入的优先级,而不是责怪模型没有理解你脑中的默认规则。
沿着数据路径定位错误,仍然是上一集建立的排错方法。区别只在于,流程从一条直线变成了有多个出口的道路网。
11这次改造,真正增加的不是三个节点
改造前,"国家信息小助手"只有一种命运:任何输入都进入 LLM。
改造后,应用开始拥有职责边界:
- 明确的国家首都问题,交给 LLM 生成回答;
- 正常问候,用固定文案友好引导;
- 无关或无法识别的请求,用固定文案明确拒绝;
- 确定性的空值检查,交给 If/Else 处理。
这张画布第一次把"模型能力"和"产品逻辑"分开了。
模型擅长理解自然语言,所以让问题分类器判断输入的语义类别;程序擅长执行明确规则,所以让 If/Else 判断变量是否为空;产品已经确定的提示语,则直接写进 Answer,不必每次请模型重新创作。
这也是从 Demo 走向可靠应用时最重要的转变之一:
不要把整套产品寄托在一段越来越长的 Prompt 上。先拆出哪些是语义判断,哪些是确定规则,哪些是固定交付,再让不同节点各自承担最合适的职责。
12分岔路的终点,是更清楚的责任边界
这一集只增加了一个核心能力:让同一条用户输入可以走向不同路径。
我们用了两种做法。
问题分类器依靠 LLM 的语义理解,适合处理"这句话属于哪一种意图"这类很难穷举表达方式的问题。它的关键不只是添加类别,而是写清楚每一类的包含范围、排除范围和冲突时的优先级。
If/Else 根据变量和明确条件决定路径,适合处理空值、精确匹配、文本包含和数值比较等确定性问题。它更省 Token,也更容易预测和测试。
两者不是竞争关系,而是可以嵌套使用的两层控制:先在需要理解语言的地方使用模型,再在能够写死规则的地方坚持使用规则。
如果只记住一句话,可以记住这个判断标准:
能准确写成"如果条件成立,就走这条路"的,用 If/Else;必须先理解"用户到底在说什么"的,用问题分类器。
下一集,我们会继续向工作流内部走,专门讲"变量"——这个系列真正的地基:数据怎样在节点之间流动,会话变量和环境变量分别解决什么问题,变量聚合器与赋值器又为什么会决定复杂流程能不能维护。
本文首发于我的个人站 chengbei.org,同步转载于此。

