Spring Boot 整合 Spring AI 與 MCP 開發智能體工具指南
一、引言
隨著大語言模型(LLM)的普及,越來越多的開發者希望將其集成到自己的應用中。Spring AI 作為 Spring 生態下的 AI 集成框架,提供了便捷的方式來對接各種大模型。而 MCP(Model Context Protocol) 則是 Spring AI 中用于擴展模型能力的重要機制,允許我們通過自定義工具(Tool)增強模型的功能。
本文將詳細介紹如何在 Spring Boot 項目中整合 Spring AI 和 MCP,開發自定義工具,并通過一個完整的示例展示整個流程。
二、環境準備
首先創建一個 Spring Boot 項目,添加以下依賴:
<dependencies><!-- Spring Web --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- Spring AI --><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-core</artifactId><version>1.0.0-M6</version></dependency><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-openai</artifactId><version>1.0.0-M6</version></dependency><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-mcp-server-webmvc</artifactId><version>1.0.0-M6</version></dependency><!-- 工具庫 --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency>
</dependencies>
在 application.properties
中配置 OpenAI API 密鑰:
spring.ai.openai.api-key=your-openai-api-key
spring.ai.openai.model=gpt-3.5-turbo
三、理解工具(Tool)概念
在 Spring AI 中,工具是一個可以被大模型調用的功能單元。工具通常對應一個具體的函數或方法,用于執行特定的任務,如查詢數據庫、調用外部 API、執行計算等。
工具的核心特點:
- 有明確的功能和輸入輸出
- 可以被大模型理解和調用
- 可以組合使用,形成復雜的工作流
四、開發自定義工具
下面通過一個完整的例子,展示如何開發自定義工具。我們將創建一個簡單的智能助手,支持數學計算、天氣查詢和翻譯功能。
先給出整體流程圖:
1. 數學計算工具
package com.example.aiagent.tools;import org.springframework.ai.tool.Tool;
import org.springframework.stereotype.Component;@Component
public class CalculatorTool {@Tool(name = "calculator",description = "執行基本的數學計算,支持加(+), 減(-), 乘(*), 除(/)運算",parameters = {@ToolParam(name = "expression", description = "數學表達式,如 '2+3'", type = "string")})public String calculate(String expression) {try {// 簡單的表達式解析if (expression.contains("+")) {String[] parts = expression.split("\\+");double a = Double.parseDouble(parts[0].trim());double b = Double.parseDouble(parts[1].trim());return String.valueOf(a + b);} else if (expression.contains("-")) {String[] parts = expression.split("-");double a = Double.parseDouble(parts[0].trim());double b = Double.parseDouble(parts[1].trim());return String.valueOf(a - b);} else if (expression.contains("*")) {String[] parts = expression.split("\\*");double a = Double.parseDouble(parts[0].trim());double b = Double.parseDouble(parts[1].trim());return String.valueOf(a * b);} else if (expression.contains("/")) {String[] parts = expression.split("/");double a = Double.parseDouble(parts[0].trim());double b = Double.parseDouble(parts[1].trim());return String.valueOf(a / b);} else {return "不支持的表達式: " + expression;}} catch (Exception e) {return "計算錯誤: " + e.getMessage();}}
}
2. 天氣查詢工具
package com.example.aiagent.tools;import org.springframework.ai.tool.Tool;
import org.springframework.stereotype.Component;import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;@Component
public class WeatherTool {private static final String API_KEY = "your-weather-api-key";private static final String API_URL = "https://api.weatherapi.com/v1/current.json";@Tool(name = "weather",description = "查詢指定城市的當前天氣",parameters = {@ToolParam(name = "city", description = "城市名稱,如 '北京'", type = "string")})public String getWeather(String city) {try {URL url = new URL(API_URL + "?key=" + API_KEY + "&q=" + city);HttpURLConnection connection = (HttpURLConnection) url.openConnection();connection.setRequestMethod("GET");BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));StringBuilder response = new StringBuilder();String line;while ((line = reader.readLine()) != null) {response.append(line);}reader.close();return response.toString();} catch (Exception e) {return "獲取天氣失敗: " + e.getMessage();}}
}
3. 翻譯工具
package com.example.aiagent.tools;import org.springframework.ai.tool.Tool;
import org.springframework.stereotype.Component;@Component
public class TranslationTool {@Tool(name = "translator",description = "將文本從一種語言翻譯成另一種語言",parameters = {@ToolParam(name = "text", description = "需要翻譯的文本", type = "string"),@ToolParam(name = "targetLanguage", description = "目標語言代碼,如 'en' 表示英語,'zh' 表示中文", type = "string")})public String translate(String text, String targetLanguage) {try {// 這里使用簡單的模擬,實際應用中可以調用翻譯APIif ("en".equalsIgnoreCase(targetLanguage)) {return "This is a simulated translation result for: " + text;} else if ("zh".equalsIgnoreCase(targetLanguage)) {return "這是一個模擬的翻譯結果,原文是: " + text;} else {return "暫不支持的語言: " + targetLanguage;}} catch (Exception e) {return "翻譯失敗: " + e.getMessage();}}
}
五、自動掃描與注冊工具
Spring AI 提供了便捷的方式來自動掃描和注冊所有帶有 @Tool
注解的工具類,無需手動逐個注冊。
package com.example.aiagent.config;import org.springframework.ai.mcp.server.tool.MethodToolCallbackProvider;
import org.springframework.ai.mcp.server.tool.ToolCallbackProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
import org.springframework.ai.tool.Tool;@Configuration
// 自動掃描指定包下所有帶有 @Tool 注解的類
@ComponentScan(basePackages = "com.example.aiagent.tools",includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION,classes = Tool.class)
)
public class ToolAutoScanConfig {@Beanpublic ToolCallbackProvider toolCallbackProvider(Object[] toolBeans) {// 自動注冊所有掃描到的工具return MethodToolCallbackProvider.builder().toolObjects(toolBeans).build();}
}
六、配置AI客戶端與動態工具描述
動態生成系統提示,自動包含所有已注冊工具的信息:
package com.example.aiagent.config;import org.springframework.ai.chat.ChatClient;
import org.springframework.ai.mcp.server.tool.ToolCallback;
import org.springframework.ai.mcp.server.tool.ToolCallbackProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class AIClientConfig {@Beanpublic ChatClient chatClient(ChatClient.Builder builder, ToolCallbackProvider toolCallbackProvider) {// 動態生成系統提示,包含所有可用工具信息String systemPrompt = generateSystemPrompt(toolCallbackProvider);return builder.defaultSystem(systemPrompt).defaultTools(toolCallbackProvider).build();}private String generateSystemPrompt(ToolCallbackProvider toolCallbackProvider) {StringBuilder sb = new StringBuilder();sb.append("你是一個智能助手,可以使用以下工具來回答用戶問題:\n\n");// 動態添加所有工具描述toolCallbackProvider.getToolCallbacks().forEach(callback -> {sb.append("- ").append(callback.getName()).append(": ").append(callback.getDescription()).append("\n");});sb.append("\n如果你需要使用工具,請按照以下格式返回:\n");sb.append("[TOOL_CALL]\n");sb.append("{\n");sb.append(" \"name\": \"工具名稱\",\n");sb.append(" \"parameters\": {\n");sb.append(" \"參數名\": \"參數值\"\n");sb.append(" }\n");sb.append("}\n");sb.append("[/TOOL_CALL]\n\n");sb.append("如果不需要工具,直接回答用戶問題。");return sb.toString();}
}
七、智能工具調用處理器
創建一個服務來處理模型生成的工具調用:
package com.example.aiagent.service;import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.ai.mcp.server.tool.ToolCallback;
import org.springframework.ai.mcp.server.tool.ToolCallbackRegistry;
import org.springframework.stereotype.Service;import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;@Service
public class ToolInvocationService {private static final Pattern TOOL_CALL_PATTERN = Pattern.compile("\\[TOOL_CALL\\](.*?)\\[/TOOL_CALL\\]", Pattern.DOTALL);private final ToolCallbackRegistry toolCallbackRegistry;private final ObjectMapper objectMapper;public ToolInvocationService(ToolCallbackRegistry toolCallbackRegistry, ObjectMapper objectMapper) {this.toolCallbackRegistry = toolCallbackRegistry;this.objectMapper = objectMapper;}/*** 檢查并處理工具調用*/public String processToolCalls(String modelResponse) {Matcher matcher = TOOL_CALL_PATTERN.matcher(modelResponse);while (matcher.find()) {String toolCallJson = matcher.group(1).trim();try {// 解析工具調用JSONMap<String, Object> toolCall = objectMapper.readValue(toolCallJson, HashMap.class);String toolName = (String) toolCall.get("name");Map<String, Object> parameters = (Map<String, Object>) toolCall.get("parameters");// 調用對應的工具ToolCallback callback = toolCallbackRegistry.getToolCallback(toolName);if (callback != null) {Object result = callback.invoke(parameters);// 將工具調用結果替換回響應中modelResponse = modelResponse.replace("[TOOL_CALL]" + toolCallJson + "[/TOOL_CALL]","工具[" + toolName + "]執行結果: " + result);} else {modelResponse = modelResponse.replace("[TOOL_CALL]" + toolCallJson + "[/TOOL_CALL]","錯誤: 未找到工具[" + toolName + "]");}} catch (Exception e) {modelResponse = modelResponse.replace("[TOOL_CALL]" + toolCallJson + "[/TOOL_CALL]","工具調用錯誤: " + e.getMessage());}// 重新匹配,因為內容已被替換matcher = TOOL_CALL_PATTERN.matcher(modelResponse);}return modelResponse;}
}
八、創建控制器處理用戶請求
package com.example.aiagent.controller;import com.example.aiagent.model.ChatRequest;
import com.example.aiagent.model.ChatResponse;
import com.example.aiagent.service.ToolInvocationService;
import org.springframework.ai.chat.ChatClient;
import org.springframework.ai.chat.ChatResponse;
import org.springframework.ai.prompt.Prompt;
import org.springframework.ai.prompt.PromptTemplate;
import org.springframework.web.bind.annotation.*;import java.util.HashMap;
import java.util.Map;@RestController
@RequestMapping("/api/chat")
public class ChatController {private final ChatClient chatClient;private final ToolInvocationService toolInvocationService;public ChatController(ChatClient chatClient, ToolInvocationService toolInvocationService) {this.chatClient = chatClient;this.toolInvocationService = toolInvocationService;}@PostMappingpublic ChatResponse chat(@RequestBody ChatRequest request) {// 創建用戶提示Map<String, Object> modelParams = new HashMap<>();modelParams.put("userInput", request.getMessage());PromptTemplate promptTemplate = new PromptTemplate("{userInput}", modelParams);Prompt prompt = new Prompt(promptTemplate.create());// 調用AI模型ChatResponse response = chatClient.generate(prompt);String content = response.getGeneration().getContent();// 處理工具調用content = toolInvocationService.processToolCalls(content);return new ChatResponse(content);}
}
九、模型類
package com.example.aiagent.model;import lombok.Data;@Data
public class ChatRequest {private String message;
}@Data
public class ChatResponse {private String content;public ChatResponse(String content) {this.content = content;}
}
十、測試工具功能
完成上述代碼后,你可以通過發送 POST 請求到 /api/chat
測試工具功能。以下是幾個測試示例:
1. 數學計算測試
{"message": "3乘以5等于多少?"
}
預期模型可能會生成工具調用:
[TOOL_CALL]
{"name": "calculator","parameters": {"expression": "3*5"}
}
[/TOOL_CALL]
2. 天氣查詢測試
{"message": "今天北京的天氣如何?"
}
預期模型可能會生成工具調用:
[TOOL_CALL]
{"name": "weather","parameters": {"city": "北京"}
}
[/TOOL_CALL]
3. 翻譯測試
{"message": "請將'Hello World'翻譯成中文"
}
預期模型可能會生成工具調用:
[TOOL_CALL]
{"name": "translator","parameters": {"text": "Hello World","targetLanguage": "zh"}
}
[/TOOL_CALL]
十一、總結
通過以上步驟,我們完成了一個完整的 Spring Boot 項目,整合了 Spring AI 和 MCP 框架,并開發了自定義工具。主要關鍵點包括:
- 工具開發:使用
@Tool
注解標記工具方法,明確工具名稱、描述和參數 - 自動掃描:通過
@ComponentScan
和MethodToolCallbackProvider
自動注冊所有工具 - 動態系統提示:根據已注冊工具動態生成系統提示,無需手動維護
- 工具調用處理:統一處理模型生成的工具調用請求,執行對應工具并返回結果
這里的實現具有以下優勢:
- 簡化配置:無需手動注冊每個工具類,系統自動發現并注冊
- 可擴展性:隨時添加新工具,只需在工具包下創建帶
@Tool
注解的類 - 維護方便:工具描述與工具實現保持在一起,易于理解和修改
- 靈活性:工具調用處理邏輯集中管理,便于擴展更復雜的工具調用鏈
希望這篇教程能幫助你入門 Spring Boot 與 Spring AI 開發,讓你能夠輕松開發自己的智能體工具!