很多意图路由代码之所以混乱,是因为把“分类”和“路由”写在了同一个方法里。本文只聚焦两件事:分类器如何只做意图识别,路由如何只做服务分发。并用 Spring AI 给出最小可理解的解耦示例。核心结论:分类器负责“用户想干什么”,路由负责“该送到哪个服务”。
一、先看一段常见的耦合代码
很多系统一开始都会写成这样:
if (query.contains("退款")) {
return refundService.call(query);
} else if (query.contains("物流")) {
return logisticsService.call(query);
} else if (query.contains("投诉")) {
return complaintService.call(query);
}这段代码能跑,但问题很明显:
- 分类逻辑和转发逻辑揉在一起;
- 新增一个服务,要改分类代码;
- 替换分类模型,要动路由逻辑;
- 如果让 LLM 同时输出“意图”和“目标服务”,一旦幻觉,路由直接错乱。
所以第一件事,是把两个概念拆开:
- 分类器 Classifier:只识别意图,输出
intent、confidence、参数; - 路由 Router:只根据分类结果,决定目标服务。
一句话:
分类 = 识别是什么
路由 = 决定送到哪
分类器不感知下游有哪些服务,路由不负责理解用户文本。
二、分类器:只回答“用户想干什么”
分类器的职责非常单一:输入用户 query,输出意图标签和置信度。
先定义结果对象和接口。这里不用record,用传统 Java 类:
import java.util.Collections;
import java.util.Map;
public class ClassifierResult {
private final String intent;
private final double confidence;
private final Map<String, Object> slots;
public ClassifierResult(String intent, double confidence, Map<String, Object> slots) {
this.intent = intent;
this.confidence = confidence;
this.slots = slots == null ? Collections.emptyMap() : slots;
}
public String getIntent() {
return intent;
}
public double getConfidence() {
return confidence;
}
public Map<String, Object> getSlots() {
return slots;
}
}public interface IntentClassifier {
ClassifierResult classify(String query);
}注意:ClassifierResult里没有targetService。
分类器不应该知道下游有哪些服务,更不应该决定调用谁。
分类器通常有三种形态:
- 规则分类器;
- 轻量模型分类器;
- LLM 显式分类器。
它们输出格式一致,都是intent + confidence + slots。
1. 规则分类器
适合强规则、关键词明确的场景,比如“转人工”“退款”“物流”。
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.regex.Pattern;
@Component
public class RuleIntentClassifier implements IntentClassifier {
private static class Rule {
private final Pattern pattern;
private final String intent;
public Rule(Pattern pattern, String intent) {
this.pattern = pattern;
this.intent = intent;
}
public Pattern getPattern() {
return pattern;
}
public String getIntent() {
return intent;
}
}
private static final List<Rule> RULES = Arrays.asList(
new Rule(Pattern.compile(".*(转人工|人工客服).*"), "manual_service"),
new Rule(Pattern.compile(".*(物流|快递|到哪).*"), "logistics_query"),
new Rule(Pattern.compile(".*(退款|退货|退钱).*"), "refund_apply"),
new Rule(Pattern.compile(".*(投诉|差评).*"), "complaint")
);
@Override
public ClassifierResult classify(String query) {
for (Rule rule : RULES) {
if (rule.getPattern().matcher(query).matches()) {
return new ClassifierResult(rule.getIntent(), 1.0, Collections.emptyMap());
}
}
return new ClassifierResult(null, 0.0, Collections.emptyMap());
}
}规则分类器只输出意图,不输出服务。
2. 轻量模型分类器
轻量模型分类器介于规则和 LLM 之间。
它通常使用 TextCNN、FastText、蒸馏 BERT、ONNX 模型等做本地推理,成本低、延迟低,适合高频且意图相对稳定的场景。
它和 LLM 分类器的边界一样:只输出意图,不输出服务。
先定义模型输出对象和模型客户端:
import java.util.Collections;
import java.util.Map;
public class ModelOutput {
private final String intent;
private final double confidence;
private final Map<String, Object> slots;
public ModelOutput(String intent, double confidence, Map<String, Object> slots) {
this.intent = intent;
this.confidence = confidence;
this.slots = slots == null ? Collections.emptyMap() : slots;
}
public String getIntent() {
return intent;
}
public double getConfidence() {
return confidence;
}
public Map<String, Object> getSlots() {
return slots;
}
}public interface IntentModelClient {
ModelOutput predict(String query);
}然后实现IntentClassifier:
import org.springframework.stereotype.Component;
@Component
public class LightweightIntentClassifier implements IntentClassifier {
private final IntentModelClient modelClient;
public LightweightIntentClassifier(IntentModelClient modelClient) {
this.modelClient = modelClient;
}
@Override
public ClassifierResult classify(String query) {
ModelOutput output = modelClient.predict(query);
return new ClassifierResult(
output.getIntent(),
output.getConfidence(),
output.getSlots()
);
}
}这里的IntentModelClient可以是:
- ONNX Runtime 加载 BERT 蒸馏模型;
- DJL 加载本地文本分类模型;
- TensorFlow Serving / Triton 远程推理;
- 自研小模型 HTTP 服务。
但无论底层是什么,它对外只做一件事:
query -> intent / confidence / slots它不知道下游有refundService还是logisticsService。
目标服务仍然由ServiceRouter决定。
3. LLM 显式分类器
用 Spring AI 调 LLM,也只让它输出意图,不让它选服务。
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Component;
@Component
public class LlmIntentClassifier implements IntentClassifier {
private final ChatClient chatClient;
private final ObjectMapper objectMapper;
public LlmIntentClassifier(ChatClient.Builder builder, ObjectMapper objectMapper) {
this.chatClient = builder.build();
this.objectMapper = objectMapper;
}
@Override
public ClassifierResult classify(String query) {
String prompt = "你是意图分类器,只输出 JSON,不要输出目标服务。\n"
+ "可选意图:manual_service, logistics_query, refund_apply, complaint, order_query, unknown。\n"
+ "输出格式:{\"intent\":\"xxx\",\"confidence\":0.0,\"slots\":{}}\n"
+ "用户输入:%s";
prompt = String.format(prompt, query);
try {
String content = chatClient.prompt().user(prompt).call().content();
JsonNode node = objectMapper.readTree(content);
return new ClassifierResult(
node.path("intent").asText("unknown"),
node.path("confidence").asDouble(0.0),
objectMapper.convertValue(node.path("slots"), Map.class)
);
} catch (Exception e) {
return new ClassifierResult("unknown", 0.0, null);
}
}
}关键点:LLM 分类器的 Prompt 里不要写服务列表。
它只需要知道有哪些意图,不需要知道下游有refundService还是logisticsService。
4. 三种分类器对比
| 分类器 | 原理 | 成本 | 适合场景 |
|---|---|---|---|
| 规则分类器 | 正则、关键词、字典 | 极低 | 强规则、明确关键词 |
| 轻量模型分类器 | 本地小模型推理 | 低 | 高频、意图稳定、有标注数据 |
| LLM 显式分类器 | Prompt + 大模型 | 高 | 语义复杂、长尾表达 |
它们的共同点是:只输出 intent,不输出 targetService。
三、路由:只回答“送到哪个服务”
路由的职责也很单一:拿到分类结果,决定目标服务。
定义路由结果和接口:
import java.util.Collections;
import java.util.Map;
public class RouteDecision {
private final String targetService;
private final Map<String, Object> params;
private final boolean fallback;
private final String reason;
public RouteDecision(String targetService, Map<String, Object> params,
boolean fallback, String reason) {
this.targetService = targetService;
this.params = params == null ? Collections.emptyMap() : params;
this.fallback = fallback;
this.reason = reason;
}
public String getTargetService() {
return targetService;
}
public Map<String, Object> getParams() {
return params;
}
public boolean isFallback() {
return fallback;
}
public String getReason() {
return reason;
}
}public interface ServiceRouter {
RouteDecision route(String query, ClassifierResult classifierResult);
}这里query传进来,是为了兼容“模型路由”这种特殊实现。
对于规则路由来说,它根本不需要读query。
规则路由
规则路由只做一件事:查表。
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
@Component
public class RuleServiceRouter implements ServiceRouter {
private static final Map<String, String> INTENT_TO_SERVICE = new HashMap<>();
static {
INTENT_TO_SERVICE.put("manual_service", "humanCustomerService");
INTENT_TO_SERVICE.put("logistics_query", "logisticsService");
INTENT_TO_SERVICE.put("refund_apply", "refundService");
INTENT_TO_SERVICE.put("complaint", "complaintService");
INTENT_TO_SERVICE.put("order_query", "orderService");
}
@Override
public RouteDecision route(String query, ClassifierResult result) {
if (result == null || result.getIntent() == null || result.getConfidence() < 0.5) {
return new RouteDecision("fallbackService", Collections.emptyMap(), true, "low_confidence");
}
String target = INTENT_TO_SERVICE.get(result.getIntent());
if (target == null) {
return new RouteDecision("fallbackService", Collections.emptyMap(), true, "no_mapping");
}
return new RouteDecision(target, result.getSlots(), false, "rule_router");
}
}注意:规则路由里没有query.contains("退款")。
它不读文本,不懂语义,只根据分类结果查表。
这是分类和路由解耦的关键:
- 分类器负责理解文本;
- 路由负责映射意图到服务。
四、模型路由:它是路由,不是分类器
有一种路由实现叫“模型路由”。
它把“理解 query + 选择服务”都交给 LLM,直接输出targetService,不要求外部先有intent。
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Component;
import java.util.Map;
@Component
public class LlmModelRouter implements ServiceRouter {
private final ChatClient chatClient;
private final ObjectMapper objectMapper;
public LlmModelRouter(ChatClient.Builder builder, ObjectMapper objectMapper) {
this.chatClient = builder.build();
this.objectMapper = objectMapper;
}
@Override
public RouteDecision route(String query, ClassifierResult result) {
String prompt = "你是路由决策器。根据用户问题,从下面服务中选择一个:\n"
+ "humanCustomerService, logisticsService, refundService,\n"
+ "complaintService, orderService, fallbackService。\n\n"
+ "严格输出 JSON:\n"
+ "{\"targetService\":\"xxx\",\"params\":{},\"fallback\":false}\n\n"
+ "用户问题:%s";
prompt = String.format(prompt, query);
try {
String content = chatClient.prompt().user(prompt).call().content();
JsonNode node = objectMapper.readTree(content);
return new RouteDecision(
node.path("targetService").asText("fallbackService"),
objectMapper.convertValue(node.path("params"), Map.class),
node.path("fallback").asBoolean(false),
"llm_model_router"
);
} catch (Exception e) {
return new RouteDecision("fallbackService", null, true, "llm_error");
}
}
}模型路由内部其实也发生了语义理解,但它不对外暴露intent,而是直接输出路由指令。
所以一定要区分:
| 实现 | 是否输出 intent | 输出 | 谁决定目标服务 |
|---|---|---|---|
| LLM 显式分类 + 规则路由 | 是 | ClassifierResult | 路由 |
| 模型路由 | 否 | RouteDecision | LLM 自己 |
结论:
模型路由是路由的一种实现,不是分类器。
它在内部隐式分类,但外部拿不到意图标签。
五、最小组合:先分类,再路由
把分类器和路由串起来,代码可以非常简单:
public class IntentDispatchService {
private final IntentClassifier classifier;
private final ServiceRouter router;
public IntentDispatchService(IntentClassifier classifier, ServiceRouter router) {
this.classifier = classifier;
this.router = router;
}
public RouteDecision dispatch(String query) {
ClassifierResult result = classifier.classify(query);
return router.route(query, result);
}
}这样拆开之后:
- 想换分类器:规则分类器换成 LLM 分类器,路由不用动;
- 想换路由:规则路由换成模型路由,分类器不用动;
- 想新增服务:只改路由映射,不改分类逻辑。
这就是解耦的价值。
六、什么时候用哪种?
不需要复杂化,只需要按场景选。
分类器怎么选
| 场景 | 推荐 |
|---|---|
| 关键词明确,比如“转人工”“退款” | 规则分类器 |
| 高频、意图稳定、有标注数据 | 轻量模型分类器 |
| 表达多样、语义复杂 | LLM 显式分类器 |
| 需要可观测、需要 intent 标签 | 轻量模型分类器或 LLM 显式分类器 |
路由怎么选
| 场景 | 推荐 |
|---|---|
| 意图到服务映射稳定 | 规则路由 |
| 想让 LLM 端到端直接选服务 | 模型路由 |
| 需要可观测、可测试 | 规则路由 |
| 服务多、映射复杂、实验性质 | 模型路由 |
但无论怎么选,边界不要乱:
- 分类器不要输出
targetService; - 规则路由不要读
query做关键词判断; - LLM 分类器的 Prompt 里不要写服务列表;
- 模型路由不是分类器,它只是路由的另一种实现。
七、总结
分类器和路由的职责其实非常清楚:
- 规则分类器:根据关键词、正则、字典识别意图;
- 轻量模型分类器:本地小模型推理,输出标准
intent; - LLM 显式分类器:用大模型理解语义,仍然只输出
intent; - 规则路由:根据分类结果查表转发,不读文本,不懂语义;
- 模型路由:LLM 内部隐式分类,直接输出路由决策,它属于路由,不是分类器。