MCP(Model Context Protocol)提供了标准化的外部工具接入协议,让AI Agent可以统一对接各类外部服务。在基于 Spring AI Alibaba 构建 MCP Agent 时,MCP Server 连接失败、工具列表为空、调用超时、鉴权失败等场景下,大模型极易产生幻觉,编造虚假返回结果。MCP 协议本身仅定义工具通信规范,不具备幻觉治理能力。本文结合 Spring AI 原生 MCP 同步客户端 McpSyncClient,给出宿主Host前置硬拦截 + 提示词软约束的组合方案,在工具链路异常时阻断模型脑补,附带适配生产的可运行 Java 代码、Mermaid 时序图,规避只依赖 System Prompt 带来的规则逃逸风险。
前言
随着 AI Agent 开发热度上升,MCP(Model Context Protocol)正在成为 Agent 对接外部能力的标准方案。MCP 采用三层架构划分职责:
- MCP Host(宿主):AI 应用本体,本文为 SpringBoot + Spring AI Alibaba 应用,是整个交互流程的控制方,掌握 MCP 连接真实状态;
- MCP Client :内嵌在 Host 内部,基于 JSON-RPC 协议和远端 MCP Server 通信,负责拉取工具列表、转发工具调用请求;工具清单不是静态配置,必须远程调用
list_toolsRPC 获取; - MCP Server :对外暴露工具与资源,例如 GitHub MCP Server、文件服务 MCP Server,提供
list_tools、call_tool标准 RPC 方法。
原生标准 MCP 调用链路:
用户提问 → Host 驱动 MCP Client 请求 list_tools → MCP Server 返回工具定义 → Client 聚合工具集合交给 Host → Host 将工具定义送入大模型 → 模型判断是否发起工具调用 → Host 驱动 Client 执行 call_tool → Server 执行业务并返回结果 → 模型基于真实工具结果整理回答。
业务痛点:两类高频幻觉场景
很多开发者接入 MCP 之后,会遇到一个共性问题:协议只规范通信,不负责控幻觉。
- 场景一: MCP Server 连接失败,
list_tools返回空列表,无任何可用工具。当用户询问依赖外部资源的问题,模型会无视"没有工具"的事实,编造不存在的数据; - **场景二:**工具列表拉取成功,模型正常下发工具调用指令,但 MCP Server 超时、鉴权报错、返回空值。如果直接把异常信息丢给大模型,模型经常忽略报错信息,脑补一份虚假结果返回用户。
单纯依靠 System Prompt 约束是不可靠的:大模型属于概率生成,存在规则逃逸。所以我们的核心思路是:Host 作为管控方,持有 MCP 真实状态,前置硬拦截优先,Prompt 仅作为兜底软约束。Host 的判断优先级高于模型自主决策。
一、环境与依赖说明
- SpringBoot:3.2.x
- Spring AI Alibaba:1.0.0-M1
- 底层复用 Spring AI 官方 MCP 同步客户端
McpSyncClient,自动处理 stdio / SSE、JSON-RPC 通信
pom.xml 核心依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>springai-mcp-anti-hallucination</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springai-mcp-anti-hallucination</name>
<properties>
<java.version>17</java.version>
<spring-ai.version>1.0.0-M1</spring-ai.version>
</properties>
<dependencies>
<!-- Spring AI Alibaba 核心 -->
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter</artifactId>
<version>${spring-ai.version}</version>
</dependency>
<!-- Spring AI MCP Client -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp</artifactId>
</dependency>
<!-- Web接口 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- JSON序列化 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>
二、MCP 状态定义 & 会话上下文
Host 侧维护 MCP 状态,状态由 MCP Client 和远端 Server 交互后更新,模型无法篡改这个状态。
package com.example.springaimcp.context;
import lombok.Data;
/**
* MCP工具可用状态枚举
*/
public enum McpAvailableStatus {
/**
* 存在可用MCP工具
*/
HAS_AVAILABLE_TOOLS,
/**
* 无任何可用MCP工具(Server连接失败 / list_tools返回空)
*/
NO_AVAILABLE_TOOLS
}
/**
* MCP会话上下文,由Host持有,作为幻觉拦截判断依据
*/
@Data
public class McpSessionContext {
// MCP工具可用状态
private McpAvailableStatus mcpAvailableStatus;
// 当前用户问题是否依赖外部资源查询
private boolean requireExternalResource;
// 工具调用异常信息
private String toolErrorMsg;
}
三、核心业务服务:Spring AI 原生 MCP + 幻觉拦截逻辑
重点说明:摒弃伪代码
parseModelWantCallTool,使用 Spring AI 原生 API:
ChatResponse.getResult().getOutput().getToolCalls()判断模型是否需要调用工具;MCP 工具自动包装为标准
ToolCallback,和 Spring AI 工具体系完全打通。
package com.example.springaimcp.service;
import com.example.springaimcp.context.McpAvailableStatus;
import com.example.springaimcp.context.McpSessionContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.messages.ToolResponseMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.mcp.client.sync.McpSyncClient;
import org.springframework.ai.mcp.tool.McpToolUtils;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.ToolResponse;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.util.List;
@Slf4j
@Service
public class McpAgentService {
private final ChatClient chatClient;
private final McpSyncClient mcpSyncClient;
private final List<ToolCallback> mcpToolCallbacks;
/**
* 系统提示词:软约束兜底,禁止模型编造外部资源数据
*/
private static final String SYSTEM_PROMPT = """
你是MCP AI助手,请严格遵守规则:
1. 如果没有可用MCP工具,你绝对不能编造任何外部数据、文件内容、仓库信息。不知道就如实告知用户,禁止猜测。
2. 如果调用MCP工具收到错误、空结果,禁止脑补返回数据,直接告知用户工具调用失败。
3. 你只能使用MCP工具返回的真实结果回答用户,不允许虚构工具返回值。
""";
public McpAgentService(ChatClient.Builder builder, McpSyncClient mcpSyncClient) {
this.chatClient = builder
.defaultSystem(SYSTEM_PROMPT)
.build();
this.mcpSyncClient = mcpSyncClient;
// 从MCP同步客户端拉取远端所有工具,转为Spring AI标准ToolCallback
this.mcpToolCallbacks = McpToolUtils.getToolCallbacksFromSyncClients(List.of(mcpSyncClient));
}
public String chat(String userInput) {
// 1. 构建会话上下文,更新MCP可用状态
McpSessionContext sessionContext = buildSessionContext();
// ========= Host层硬拦截核心逻辑 =========
boolean needExternalData = judgeNeedExternalResource(userInput);
if (needExternalData && sessionContext.getMcpAvailableStatus() == McpAvailableStatus.NO_AVAILABLE_TOOLS) {
return "【宿主拦截】当前MCP无可用工具,该问题依赖外部资源,禁止模型编造结果。";
}
UserMessage userMessage = new UserMessage(userInput);
Prompt prompt = new Prompt(List.of(userMessage));
// 把MCP工具注册给ChatClient,模型感知远端MCP Server工具定义
ChatResponse resp = chatClient.prompt(prompt)
.tools(mcpToolCallbacks)
.call()
.chatResponse();
// Spring AI原生方式读取模型下发的工具调用指令
var toolCalls = resp.getResult().getOutput().getToolCalls();
if (!CollectionUtils.isEmpty(toolCalls)) {
for (var toolCall : toolCalls) {
String toolName = toolCall.name();
String argumentsJson = toolCall.arguments();
ToolCallback targetCallback = mcpToolCallbacks.stream()
.filter(cb -> cb.getToolDefinition().name().equals(toolName))
.findFirst()
.orElse(null);
if (targetCallback == null) {
log.warn("模型请求调用不存在的MCP工具:{}", toolName);
return "MCP工具【" + toolName + "】不存在,无法获取数据,请稍后重试,禁止编造结果";
}
try {
// 底层自动走MCP JSON-RPC,调用远端MCP Server call_tool
String toolResult = targetCallback.call(argumentsJson);
// 将工具真实结果回传给模型,整理自然语言回答
return chatClient.prompt()
.messages(userMessage, new ToolResponseMessage(List.of(new ToolResponse(toolCall.id(), toolResult, toolName))))
.call()
.content();
} catch (Exception e) {
log.error("MCP工具调用异常,toolName:{}", toolName, e);
// 超时、鉴权失败、服务宕机统一在这里拦截,不把异常交给模型解读
return "MCP工具调用失败,无法获取数据,请稍后重试,禁止编造结果";
}
}
}
// 无需调用工具,直接返回模型回答
return resp.getResult().getOutput().getText();
}
/**
* 构建MCP会话上下文,同步远端工具状态
*/
private McpSessionContext buildSessionContext() {
McpSessionContext ctx = new McpSessionContext();
if (CollectionUtils.isEmpty(mcpToolCallbacks)) {
ctx.setMcpAvailableStatus(McpAvailableStatus.NO_AVAILABLE_TOOLS);
log.warn("MCP Client拉取远端工具列表为空,MCP Server不可用");
} else {
ctx.setMcpAvailableStatus(McpAvailableStatus.HAS_AVAILABLE_TOOLS);
log.info("成功加载MCP工具列表 size:{}", mcpToolCallbacks.size());
}
return ctx;
}
/**
* 简易分类:判断问题是否依赖外部资源
* 生产环境可替换为轻量分类模型,提升准确率
*/
private boolean judgeNeedExternalResource(String query) {
String[] keywords = {"github", "仓库", "issue", "pr", "搜索代码", "slack消息", "查询文件"};
for (String kw : keywords) {
if (query.contains(kw)) {
return true;
}
}
return false;
}
}
四、Controller 接口层
package com.example.springaimcp.controller;
import com.example.springaimcp.service.McpAgentService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequiredArgsConstructor
public class McpChatController {
private final McpAgentService mcpAgentService;
@GetMapping("/ai/mcp/chat")
public String chat(@RequestParam String query) {
return mcpAgentService.chat(query);
}
}
五、启动类 + 配置文件
启动类
package com.example.springaimcp;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringAiMcpApplication {
public static void main(String[] args) {
SpringApplication.run(SpringAiMcpApplication.class, args);
}
}
application.yml(DashScope 通义大模型)
spring:
application:
name: springai-mcp-anti-hallucination
ai:
dashscope:
api-key: sk-xxx你的密钥xxx
六、测试用例说明
测试1:MCP无可用工具,查询外部资源问题
请求:GET http://127.0.0.1:8080/ai/mcp/chat?query=查询github仓库最近issue
逻辑:judgeNeedExternalResource=true + MCP状态 NO_AVAILABLE_TOOLS → Host 直接拦截,请求不送入大模型
返回:【宿主拦截】当前MCP无可用工具,该问题依赖外部资源,禁止模型编造结果。
测试2:MCP无可用工具,纯常识问题
请求:GET http://127.0.0.1:8080/ai/mcp/chat?query=1+1等于几
逻辑:不需要外部资源,放行给模型,正常返回答案 2
测试3:MCP工具列表正常,但是call_tool超时/鉴权失败
逻辑:模型下发工具调用指令,执行 targetCallback.call() 抛出异常,代码捕获异常,直接返回固定提示,异常信息不交给模型二次处理,杜绝模型脑补。
返回:MCP工具调用失败,无法获取数据,请稍后重试,禁止编造结果
七、Mermaid 时序图
时序1:MCP Server连接失败/空工具列表,宿主前置拦截
MCPServer LLM MCPClient Host User MCPServer LLM MCPClient Host User #mermaid-svg-tjXkzzDoC6CS9Vio{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-tjXkzzDoC6CS9Vio .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-tjXkzzDoC6CS9Vio .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-tjXkzzDoC6CS9Vio .error-icon{fill:#552222;}#mermaid-svg-tjXkzzDoC6CS9Vio .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-tjXkzzDoC6CS9Vio .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-tjXkzzDoC6CS9Vio .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-tjXkzzDoC6CS9Vio .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-tjXkzzDoC6CS9Vio .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-tjXkzzDoC6CS9Vio .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-tjXkzzDoC6CS9Vio .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-tjXkzzDoC6CS9Vio .marker{fill:#333333;stroke:#333333;}#mermaid-svg-tjXkzzDoC6CS9Vio .marker.cross{stroke:#333333;}#mermaid-svg-tjXkzzDoC6CS9Vio svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-tjXkzzDoC6CS9Vio p{margin:0;}#mermaid-svg-tjXkzzDoC6CS9Vio .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-tjXkzzDoC6CS9Vio text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-tjXkzzDoC6CS9Vio .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-tjXkzzDoC6CS9Vio .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-tjXkzzDoC6CS9Vio .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-tjXkzzDoC6CS9Vio .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-tjXkzzDoC6CS9Vio #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-tjXkzzDoC6CS9Vio .sequenceNumber{fill:white;}#mermaid-svg-tjXkzzDoC6CS9Vio #sequencenumber{fill:#333;}#mermaid-svg-tjXkzzDoC6CS9Vio #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-tjXkzzDoC6CS9Vio .messageText{fill:#333;stroke:none;}#mermaid-svg-tjXkzzDoC6CS9Vio .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-tjXkzzDoC6CS9Vio .labelText,#mermaid-svg-tjXkzzDoC6CS9Vio .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-tjXkzzDoC6CS9Vio .loopText,#mermaid-svg-tjXkzzDoC6CS9Vio .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-tjXkzzDoC6CS9Vio .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-tjXkzzDoC6CS9Vio .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-tjXkzzDoC6CS9Vio .noteText,#mermaid-svg-tjXkzzDoC6CS9Vio .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-tjXkzzDoC6CS9Vio .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-tjXkzzDoC6CS9Vio .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-tjXkzzDoC6CS9Vio .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-tjXkzzDoC6CS9Vio .actorPopupMenu{position:absolute;}#mermaid-svg-tjXkzzDoC6CS9Vio .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-tjXkzzDoC6CS9Vio .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-tjXkzzDoC6CS9Vio .actor-man circle,#mermaid-svg-tjXkzzDoC6CS9Vio line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-tjXkzzDoC6CS9Vio :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 用户提问 查询github issue list_tools 拉取远端工具 RPC list_tools 返回空列表或连接异常 更新状态 NO_AVAILABLE_TOOLS 判断问题依赖外部资源 宿主拦截 无MCP工具 禁止编造回答
时序2:工具列表拉取成功,工具执行调用异常
MCPServer LLM MCPClient Host User MCPServer LLM MCPClient Host User #mermaid-svg-P65dh7YFOqV0324W{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-P65dh7YFOqV0324W .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-P65dh7YFOqV0324W .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-P65dh7YFOqV0324W .error-icon{fill:#552222;}#mermaid-svg-P65dh7YFOqV0324W .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-P65dh7YFOqV0324W .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-P65dh7YFOqV0324W .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-P65dh7YFOqV0324W .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-P65dh7YFOqV0324W .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-P65dh7YFOqV0324W .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-P65dh7YFOqV0324W .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-P65dh7YFOqV0324W .marker{fill:#333333;stroke:#333333;}#mermaid-svg-P65dh7YFOqV0324W .marker.cross{stroke:#333333;}#mermaid-svg-P65dh7YFOqV0324W svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-P65dh7YFOqV0324W p{margin:0;}#mermaid-svg-P65dh7YFOqV0324W .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-P65dh7YFOqV0324W text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-P65dh7YFOqV0324W .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-P65dh7YFOqV0324W .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-P65dh7YFOqV0324W .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-P65dh7YFOqV0324W .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-P65dh7YFOqV0324W #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-P65dh7YFOqV0324W .sequenceNumber{fill:white;}#mermaid-svg-P65dh7YFOqV0324W #sequencenumber{fill:#333;}#mermaid-svg-P65dh7YFOqV0324W #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-P65dh7YFOqV0324W .messageText{fill:#333;stroke:none;}#mermaid-svg-P65dh7YFOqV0324W .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-P65dh7YFOqV0324W .labelText,#mermaid-svg-P65dh7YFOqV0324W .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-P65dh7YFOqV0324W .loopText,#mermaid-svg-P65dh7YFOqV0324W .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-P65dh7YFOqV0324W .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-P65dh7YFOqV0324W .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-P65dh7YFOqV0324W .noteText,#mermaid-svg-P65dh7YFOqV0324W .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-P65dh7YFOqV0324W .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-P65dh7YFOqV0324W .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-P65dh7YFOqV0324W .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-P65dh7YFOqV0324W .actorPopupMenu{position:absolute;}#mermaid-svg-P65dh7YFOqV0324W .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-P65dh7YFOqV0324W .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-P65dh7YFOqV0324W .actor-man circle,#mermaid-svg-P65dh7YFOqV0324W line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-P65dh7YFOqV0324W :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 用户提问 list_tools 拉取远端工具 RPC list_tools 返回工具定义 工具集合转为ToolCallback Prompt和工具定义送入模型 返回ToolCall调用指令 执行call_tool RPC call_tool 超时或鉴权报错 捕获异常 工具调用失败禁止编造结果
八、生产落地要点与优化方向
-
硬拦截优先级高于Prompt
Prompt只是软约束,大模型概率生成天然存在突破规则的可能性。Host作为应用控制层,持有真实的MCP连接状态,在请求送入模型之前阻断,是可靠性最高的防线。
-
当前方案局限与优化方向
- 资源依赖判断:示例是关键词匹配,生产可以接入小模型分类,识别用户意图是否需要外部工具;
- 多MCP Server聚合:
McpToolUtils支持聚合多个McpSyncClient的工具集合; - 返回结果校验:增加后置校验,扫描模型输出是否包含虚构外部资源信息;
- 状态缓存与刷新:
list_tools不需要每次请求都拉取,可以增加定时刷新、失效机制,降低RPC开销。
-
避坑清单
- ❌ 只依靠 System Prompt 限制幻觉:MCP不可用时模型依然编造外部数据;
- ❌ 将原始异常堆栈直接透传给大模型:模型容易解读报错信息、虚构返回值;
- ✅ Host维护独立MCP状态,前置拦截优先;Prompt作为兜底;
- ✅ 工具调用异常由Host接管返回固定提示,不交给模型自由解读错误。
结语
MCP 协议解决了工具接入标准化的问题,但幻觉治理不在协议能力范围内。在 Spring AI Alibaba + MCP 构建 Agent 的场景中,宿主状态管控前置拦截 + 提示词兜底这套组合方案,能够很好抑制工具不可用场景下的模型编造行为。