Skip to main content

意图分数出来了,该查哪个库、查多少条

开篇引言

上一篇讲了歧义引导——用户问退货政策,3C、家电和服装三个品类都举手了,系统停下来让用户选。handleGuidance 检测到歧义后短路输出引导文案,Pipeline 直接 return,不走检索。

歧义引导走完了——要么用户被问了一次做出了选择,要么意图本身明确不需要引导。不管哪种情况,到这一步意图分类的结果已经确定了。每个子问题手里拿着的意图列表是板上钉钉的:可能是一个 KB 意图(清关流程,score=0.92),可能是一个 MCP 意图(订单查询,score=0.90),也可能是一个 SYSTEM 意图(欢迎与问候,score=0.88)。

现在摆在面前的问题是:这些意图分数怎么翻译成实际的动作?

不同类型的意图走完全不同的路径——KB 意图要去向量数据库的某个 Collection 做向量检索,MCP 意图要调订单系统的 API 拿实时数据,SYSTEM 意图压根不用查任何东西直接回复就行。而且分数的高低还会影响检索策略——高置信度走定向检索就够了,低置信度还得兜底搜全库给 Reranker 更多候选。

本篇是意图识别子系列的第 5 篇也是收尾篇。前四篇解决了怎么知道用户想问什么——树结构(第 5 篇)→ 打分(第 6 篇)→ 封顶(第 7 篇)→ 歧义引导(第 8 篇),本篇补上最后一环:知道了之后怎么做。

用电商客服的四个典型问题来跑一遍:

用户问题命中意图类型接下来该做什么
你好欢迎与问候(0.88)SYSTEM直接回复,不走检索
帮我查一下订单 2024112801 的物流进度订单查询(0.90)MCP调工具,不走向量检索
跨境包裹清关一般要多久?清关流程(0.92)KB去对应 Collection 做定向检索
退货规则是什么清关流程(0.55)KB分数低于置信度阈值,定向 + 全局兜底检索

四个问题,四条路径,本篇逐个拆解。

总览:从意图到动作的分发地图

1. Pipeline 中的位置

先回到 StreamChatPipelineexecute() 方法,定位本篇涉及的阶段:

public void execute(StreamChatContext ctx) {
loadMemory(ctx); // 第 2 篇
rewriteQuery(ctx); // 第 4 篇
resolveIntents(ctx); // 第 5~7 篇

if (handleGuidance(ctx)) {
return; // 第 8 篇:歧义引导短路
}
if (handleSystemOnly(ctx)) {
return; // ← 本篇重点:SYSTEM 意图短路
}

RetrievalContext retrievalCtx = retrieve(ctx); // ← 本篇重点:KB/MCP 分流入口
if (handleEmptyRetrieval(ctx, retrievalCtx)) {
return;
}

streamRagResponse(ctx, retrievalCtx);
}

上一篇讲到 handleGuidance 返回 false(没有歧义或者引导完成了),Pipeline 继续往下走。接下来就是本篇的两个核心位置:

  • handleSystemOnly:SYSTEM 意图的短路分支,不走检索直接回复
  • retrieve:KB 意图和 MCP 意图的分流入口,委托给 RetrievalEngine 处理

2. 三条路径一张图

用一张表汇总三种意图类型的处理方式:

意图类型处理方式关键代码入口是否走向量检索备注
SYSTEM短路直接回复handleSystemOnly()可配自定义 Prompt,Temperature=0.7
MCP工具调用executeMcpTools()LLM 提取参数 + 工具执行
KB定向 / 全局检索retrieveAndRerank()走多通道检索引擎,可能两个通道同时激活

SYSTEM 意图:不查库,直接回复

1. 什么场景走这条路

用户发了一句“你好”。意图分类命中了 系统交互 > 欢迎与问候 节点(kind=SYSTEM,score=0.88)。

这种问题去向量库搜 chunk 纯属浪费——答案不在任何知识库文档里,而是系统自己该有的社交应答能力。搜出来的 chunk 不但没用,还可能干扰 LLM 的回复(比如搜到一段退货政策的文档片段,LLM 莫名其妙地在问好回复里塞进一句退货相关的话)。

最合理的做法就是跳过检索,让 LLM 直接根据会话历史和系统提示词回复。

2. handleSystemOnly 的判定条件

private boolean handleSystemOnly(StreamChatContext ctx) {
List<SubQuestionIntent> subIntents = ctx.getSubIntents();
boolean allSystemOnly = subIntents.stream()
.allMatch(si -> intentResolver.isSystemOnly(si.nodeScores()));
if (!allSystemOnly) {
return false;
}
// ... 短路处理
return true;
}

判定的核心是 allMatch——必须所有子问题都是 SYSTEM-only 才短路。

isSystemOnly 的定义也很严格:

public boolean isSystemOnly(List<NodeScore> nodeScores) {
return nodeScores.size() == 1
&& nodeScores.get(0).getNode() != null
&& nodeScores.get(0).getNode().getKind() == SYSTEM;
}

恰好只有一个意图,且 kind 是 SYSTEM,才算 SYSTEM-only。

为什么这么谨慎?看两个场景:

场景子问题意图是否短路原因
用户说“你好”[欢迎与问候(SYSTEM,0.88)]✅ 短路唯一意图是 SYSTEM
用户说“你好,顺便帮我查一下退货政策”子问题 1:[欢迎与问候(SYSTEM,0.85)]
子问题 2:[退货政策(KB,0.78)]
❌ 不短路子问题 2 不是 SYSTEM-only
用户说“谢谢”[情感反馈(SYSTEM,0.82)]✅ 短路唯一意图是 SYSTEM

第二个场景很典型——用户打了个招呼顺带问了个问题,查询改写把它拆成两个子问题。虽然打招呼那个子问题是 SYSTEM,但退货政策那个子问题是 KB,allMatch 不满足,走正常流程。这样退货政策的问题不会被跳过。

3. 自定义 Prompt 和直接回复

判定通过后的处理逻辑:

String customPrompt = subIntents.stream()
.flatMap(si -> si.nodeScores().stream())
.map(ns -> ns.getNode().getPromptTemplate())
.filter(StrUtil::isNotBlank)
.findFirst()
.orElse(null);
StreamCancellationHandle handle = streamSystemResponse(
ctx.getRewriteResult().rewrittenQuestion(),
ctx.getHistory(),
customPrompt,
ctx.getCallback()
);

两个设计点值得注意。

自定义 Prompt 优先。 SYSTEM 节点上可以配 promptTemplate。比如欢迎与问候节点配了一段用活泼亲切的语气和用户打招呼,介绍自己的能力范围——这比默认的通用 System Prompt 更贴合场景。findFirst 找到第一个非空的自定义模板就用,没配就退回到全局默认。

会话历史照常带入。 streamSystemResponse 的实现里把 history 完整传入了消息列表:

private StreamCancellationHandle streamSystemResponse(String question, List<ChatMessage> history,
String customPrompt, StreamCallback callback) {
String systemPrompt = StrUtil.isNotBlank(customPrompt)
? customPrompt
: promptTemplateLoader.load(CHAT_SYSTEM_PROMPT_PATH);

List<ChatMessage> messages = new ArrayList<>();
messages.add(ChatMessage.system(systemPrompt));
if (CollUtil.isNotEmpty(history)) {
messages.addAll(history);
}
messages.add(ChatMessage.user(question));

ChatRequest req = ChatRequest.builder()
.messages(messages)
.temperature(0.7D)
.thinking(false)
.build();
return llmService.streamChat(req, callback);
}

不走检索不代表没有上下文。用户连续说了三句“你好”“你是谁”“你能做什么”,每次都短路到 SYSTEM 分支,但会话历史带着前面的对话,LLM 能理解上下文,不会每次都从头自我介绍。

4. Temperature 的差异

注意 temperature=0.7D。和后面 KB 检索场景的 temperature=0D 形成了鲜明对比。

场景Temperature原因
SYSTEM 直接回复0.7闲聊需要自然度,“你好”不能每次回一模一样的话
KB 检索回复0.0必须严格忠实于检索到的 chunk,不能编造细节
MCP 工具回复0.3工具返回结构化数据,需要一点灵活性来组织自然语言回答

5. 短路的成本节省

解锁付费内容,👉 戳