Skip to main content

知识库答不了的问题,交给MCP工具去查

开篇引言

上一篇拆解了后处理流水线——三个通道返回的约 30 条原始 Chunk,经过去重、Cross-Encoder 精排、topK 截断,最终只留 5 条高质量上下文喂给大模型。到那里为止,KB 检索链路就完整收尾了:意图识别告诉系统该查哪个知识库,多通道检索把文档捞出来,后处理流水线把结果精炼到 5 条。

但 KB 检索不是万能的。

还是电商客服的场景。用户问“AirPods Pro 怎么退货”——知识库里有退货政策文档,KB 检索能搞定。但如果用户问的是“我上周提的退货申请现在到哪一步了”呢?这条退货申请的实时状态不会写在任何知识库文档里,它在订单系统的数据库里,每小时都可能变。类似的场景还有很多:查物流轨迹、查优惠券余额、查维修工单进度……这些数据不在知识库里,而是在企业的业务系统里。

要回答这类问题,得让系统实时去业务系统查。这就是 MCP(Model Context Protocol,模型上下文协议)工具调用要解决的事。

本篇不讲 MCP 协议本身是什么——基础系列已经讲过 Function Call 和 MCP 的概念,MCP 系列也有专门的协议规范和 SDK 架构拆解。本篇只聚焦一件事:在 Ragent 这个系统里,一次 MCP 工具调用从被识别到被执行再到结果回流,完整链路是怎么跑的。

MCP 在八阶段里的位置

1. 回到全景地图

先回到第 1 篇的全景地图,定位一下 MCP 在整个流水线中的位置:

StreamChatPipeline.execute(ctx)
阶段 1:loadMemory — 加载会话记忆
阶段 2:rewriteQuery — 查询改写与子问题拆分
阶段 3:resolveIntents — 意图识别
阶段 4:handleGuidance — 歧义引导 [短路点 #1]
阶段 5:handleSystemOnly — 系统直答 [短路点 #2]
阶段 6:retrieve — 多通道检索(KB + MCP) ← MCP 在这里
阶段 7:handleEmptyRetrieval — 空结果兜底 [短路点 #3]
阶段 8:streamRagResponse — Prompt 组装 + 流式生成

MCP 不是一条独立的流水线。它嵌在阶段 6 retrieve 内部,和 KB 检索并行执行。意图识别阶段(阶段 3)已经判断出哪些子问题需要查知识库、哪些需要调工具,到了阶段 6 就各走各的路——KB 意图走多通道检索 + 后处理流水线(第 10~11 篇的内容),MCP 意图走工具注册表查找 + 参数提取 + 远程执行。两条路的结果最终汇合到同一个 RetrievalContext 里,一起传给阶段 8 做 Prompt 组装。

2. 阶段 6 内部的分流

下面这张图把 RetrievalEngine.retrieve() 内部的 KB/MCP 并行分流画出来:

KB 路径在第 10~11 篇已经完整拆过了。接下来按 MCP 路径的顺序,一步步拆解。

意图分流:KB 和 MCP 怎么分开的

分流发生在 RetrievalEngine.buildSubQuestionContext() 方法的前两行——第 9 篇讲意图到检索映射时已经见过这段代码,这里只做简要回顾:

private SubQuestionContext buildSubQuestionContext(SubQuestionIntent intent, int topK) {
// 按 IntentKind 把 KB 和 MCP 意图分开
List<NodeScore> kbIntents = NodeScoreFilters.kb(intent.nodeScores());
List<NodeScore> mcpIntents = NodeScoreFilters.mcp(intent.nodeScores());

KbResult kbResult = retrieveAndRerank(intent, kbIntents, topK);

String mcpContext = CollUtil.isNotEmpty(mcpIntents)
? executeMcpAndMerge(intent.subQuestion(), mcpIntents)
: "";

return new SubQuestionContext(
intent.subQuestion(), kbResult.groupedContext(), mcpContext, kbResult.intentChunks()
);
}

NodeScoreFilters.mcp() 的过滤条件很明确——节点非空、kind 等于 IntentKind.MCPmcpToolId 非空:

public static List<NodeScore> mcp(List<NodeScore> scores) {
return scores.stream()
.filter(ns -> ns.getNode() != null && ns.getNode().isMCP())
.filter(ns -> StrUtil.isNotBlank(ns.getNode().getMcpToolId()))
.toList();
}

分流的关键在于 IntentNode 上的几个字段。每个意图节点在配置时就声明了自己的类型:

public enum IntentKind {
KB(0), // 知识库类,走 RAG 检索
SYSTEM(1), // 系统交互类,如欢迎语
MCP(2); // MCP 工具调用,查实时数据
}

当一个意图节点的 kind 被配置为 MCP 时,它还会携带几个 MCP 专属字段:

字段含义示例
mcpToolId工具 ID,关联注册表中的执行器order_query
paramPromptTemplate自定义参数提取提示词(可选)针对特定工具的定制提取逻辑
promptSnippet回答规则片段(可选)回答时附上订单编号和物流公司信息

继续用电商客服的场景。假设意图树里有一个节点叫订单查询,配置长这样:

字段
idbiz-order-query
name订单查询
kindMCP
mcpToolIdorder_query
description查询用户的订单状态、物流轨迹、退货进度等实时信息

当用户问“我上周提的退货申请到哪一步了”,意图识别命中了这个节点,NodeScoreFilters.mcp() 就会把它过滤出来。后续流程知道:这不是去知识库搜文档,而是去调用 ID 为 order_query 的 MCP 工具。

order_query 这个字符串怎么对应到一个可以真正执行的工具?

工具注册表:mcpToolId 怎么对应到可执行的工具

1. McpToolRegistry 接口

McpToolRegistry(MCP 工具注册表)定义了工具注册和查找的核心能力:

public interface McpToolRegistry {

void register(McpToolExecutor executor); // 注册工具执行器
void unregister(String toolId); // 注销工具
Optional<McpToolExecutor> getExecutor(String toolId); // 按 ID 查找执行器
List<Tool> listAllTools(); // 列出所有工具定义
boolean contains(String toolId); // 检查工具是否已注册
int size(); // 已注册工具数量
}

接口很直接——核心就是 register()getExecutor()。注册时传入一个执行器,查找时传入一个 toolId

2. DefaultMcpToolRegistry 实现

默认实现 DefaultMcpToolRegistry 的内部结构就是一个 Map<String, McpToolExecutor>

@Slf4j
@Component
@RequiredArgsConstructor
public class DefaultMcpToolRegistry implements McpToolRegistry {

// 核心存储:toolId → 执行器
private final Map<String, McpToolExecutor> executorMap = new HashMap<>();

// Spring 容器中所有 McpToolExecutor Bean(自动注入)
private final List<McpToolExecutor> autoDiscoveredExecutors;

@PostConstruct
public void init() {
for (McpToolExecutor executor : autoDiscoveredExecutors) {
register(executor);
}
log.info("MCP 工具自动注册完成, 共注册 {} 个工具", autoDiscoveredExecutors.size());
}

@Override
public void register(McpToolExecutor executor) {
String toolId = executor.getToolId();
McpToolExecutor existing = executorMap.put(toolId, executor);
if (existing != null) {
log.warn("工具 {} 已存在,已覆盖", toolId);
} else {
log.info("MCP 工具注册成功, toolId: {}", toolId);
}
}

@Override
public Optional<McpToolExecutor> getExecutor(String toolId) {
return Optional.ofNullable(executorMap.get(toolId));
}
// ...
}

@PostConstruct 在应用启动时自动执行,把 Spring 容器中所有实现了 McpToolExecutor 接口的 Bean 注册进 Map。运行时通过 getExecutor(toolId) 查表就能找到对应的执行器。

但这些执行器从哪来?注册表自己只管存,不管造。执行器的创建靠另一个组件——McpClientAutoConfiguration

3. 远程工具怎么注册进来的

解锁付费内容,👉 戳