Spring AI 利用 MCP 让AI实现联网搜索(webflux方式实现)
MCP 是一个开放协议,它为应用程序向 LLM 提供上下文的方式进行了标准化。你可以将 MCP 想象成 AI 应用程序的 USB-C 接口。就像 USB-C 为设备连接各种外设和配件提供了标准化的方式一样,MCP 为 AI 模型连接各种数据源和工具提供了标准化的接口。Spring AI 不断的与时俱进,也支持了MCP协议,这使得我们这些普通程序员也能够快速搭建和引入MCP服务。
前言
MCP 是一个开放协议,它为应用程序向 LLM 提供上下文的方式进行了标准化。你可以将 MCP 想象成 AI 应用程序的 USB-C 接口。就像 USB-C 为设备连接各种外设和配件提供了标准化的方式一样,MCP 为 AI 模型连接各种数据源和工具提供了标准化的接口。
Spring AI 不断的与时俱进,也支持了MCP协议,这使得我们这些普通程序员也能够快速搭建和引入MCP服务。
Spring AI 集成 MCP的几种方式
可以通过引入一些依赖,直接让 Spring AI 和 MCP 进行集成,在 Spring Boot 项目中轻松使用。下面是实现的几种方式:
(1)客户端启动器
-
spring-ai-starter-mcp-client:核心启动器,提供 STDIO 和基于 HTTP 的 SSE 支持
-
spring-ai-starter-mcp-client-webflux:基于 WebFlux 的 SSE 流式传输实现
(2)服务器启动器
-
spring-ai-starter-mcp-server:核心服务器,具有 STDIO 传输支持
-
spring-ai-starter-mcp-server-webmvc:基于 Spring MVC 的 SSE 流式传输实现
-
spring-ai-starter-mcp-server-webflux:基于 WebFlux 的 SSE 流式传输实现
本文所采用的是webflux,因为这种方式后续部署和维护最方便。
服务端
1. 引入依赖
<!--mcp服务器核心依赖— 响应式-->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp-server-webflux-spring-boot-starter</artifactId>
<version>1.0.0-M6</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>
2. 配置文件
server:
port: 8090
spring:
application:
name: mcp-server
ai:
mcp:
server:
name: mcp-server # MCP服务器名称
version: 0.0.1 # 服务器版本号
3. 代码
McpServerApplication.java
@SpringBootApplication
public class McpServerApplication {
public static void main(String[] args) {
SpringApplication.run(McpServerApplication.class, args);
}
@Bean
public ToolCallbackProvider serverTools(MCPService MCPService) {
return MethodToolCallbackProvider.builder().toolObjects(MCPService).build();
}
}
MCPService.java
/**
* 使用Tavily进行AI搜索
* @param query 搜索关键词
* @param maxResults 最大返回结果数
* @return JSON格式的搜索结果(失败时返回错误信息)
*/
@Tool(name = "tavilySearch", description = "执行AI驱动的互联网搜索")
public String tavilySearch(
@ToolParam(description = "搜索关键词", required = true) String query,
@ToolParam(description = "最大返回结果数") int maxResults
) {
try {
TavilySearch tavilySearch = new TavilySearch();
List<Map<String, String>> results = tavilySearch.tavilySearch(query, maxResults);
log.debug("Tavily search completed for query: {}, results: {}", query, results.size());
return new ObjectMapper().writeValueAsString(results);
} catch (Exception e) {
log.error("Tavily search failed for query: {}", query, e);
return "{\"error\":\"Search failed: " + e.getMessage() + "\"}";
}
}
TavilySearch.java
// 搜索引擎
private String baseUrl = "https://api.tavily.com/search";
// apikey
private String apiKey = "tvly-XXX"; //Tavily的apikey需要替换为自己的
private final OkHttpClient client;
private final ObjectMapper objectMapper;
public TavilySearch() {
this.client = new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build();
this.objectMapper = new ObjectMapper();
}
public List<Map<String, String>> tavilySearch(String query, int maxResults) {
List<Map<String, String>> results = new ArrayList<>();
try {
Map<String,Object> requestBody = new HashMap<>();
requestBody.put("query", query);
// requestBody.put("search_depth", searchDepth);
requestBody.put("max_results", maxResults);
// requestBody.put("time_range", timeRange);
Request request = new Request.Builder()
.url(baseUrl)
.post(RequestBody.create(MediaType.parse("application/json"), objectMapper.writeValueAsString(requestBody)))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer" + apiKey)
.build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) throw new IOException("请求失败: " + response);
JsonNode jsonNode = objectMapper.readTree(response.body().string()).get("results");
if (!jsonNode.isEmpty()) {
jsonNode.forEach(data -> {
Map<String, String> processedResult = new HashMap<>();
processedResult.put("title", data.get("title").toString());
processedResult.put("url", data.get("url").toString());
processedResult.put("content", data.get("content").toString());
results.add(processedResult);
});
}
}
} catch (Exception e) {
System.err.println("搜索时发生错误: " + e.getMessage());
}
return results;
}
这里Tavily的apikey需要替换为自己的,申请地址:
https://tavily.com/
客户端
1. 引入依赖
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp-client-webflux-spring-boot-starter</artifactId>
<version>1.0.0-M6</version>
</dependency>
2. 配置文件
spring:
ai:
mcp:
client:
enabled: true
name: mcp-client
version: 1.0.0
request-timeout: 120s
type: ASYNC # 类型同步或者异步
sse:
connections:
server1:
url: http://localhost:8090
3. 代码
@RestController
@RequestMapping("/mcp")
@Tag(name = "MCP")
@Slf4j
public class MCPController {
@Autowired
private ChatClient chatClient;
@PostMapping(value = "/client", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> client(String message) {
log.info("正在连接MCP服务器: {}", mcpServerUrl);
var transport = new WebFluxSseClientTransport(WebClient.builder().baseUrl(mcpServerUrl));
return Flux.using(
() -> McpClient.sync(transport).build(),
client -> {
client.initialize();
try {
List<McpSchema.Tool> tools = client.listTools().tools();
String name = "tavilySearch";
log.info("Available Tools = {}", tools);
McpSchema.CallToolResult mcpResult = null;
//无参数调用mcp
// mcpResult = client.callTool(new McpSchema.CallToolRequest(name, Map.of()));
//有参数调用mcp
mcpResult = client.callTool(new McpSchema.CallToolRequest(name, Map.of("query", message, "maxResults", 5)));
String content = mcpResult.content().toString();
String mcpResultString = "{" + name + " : " + content + "}, ";
log.info("最终请求信息{}", mcpResultString + message);
// 先返回MCP的结果,然后再返回chatClient的流式响应
return Flux.concat(
Flux.just(mcpResultString),
chatClient.prompt()
.user(mcpResultString + message)
.stream()
.content()
);
} catch (Exception e) {
log.error("MCP调用出错: ", e);
return Flux.error(new RuntimeException("MCP调用出错: " + e.getMessage()));
}
}
);
}
}
测试
由结果图可以看到大模型回答前都会先调用mcp的联网搜索然后再回答,这样就解决大模型语料库落后的问题了。
下面是实际应用的项目:
https://yiyongai.cn/
更多推荐
所有评论(0)