
C# 與 Windows API 交互的“秘密武器”:結構體和聯合體
可以發現 SpringAI 把 FunctionCall 已經標記為廢棄狀態,真的讓人大吃一驚,仔細深入查看文檔發現如今的 Spring 更為推薦使用 Tool Calling 感興趣的讀者可以閱讀下這篇文章 docs.spring.io/spring-ai/r…[2] ?簡單來說,Tool Calling 和 FunctionCall 的作用大致相同,只不過換用了一套注解,使用 Tool Calling 之后 可以像下面的代碼這樣去綁定到 ChatClient 上面
class DateTimeTools {
@Tool(description = "Get the current date and time in the user's timezone")
String getCurrentDateTime() {
return LocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId()).toString();
}
}
// 注冊工具到大模型上面
ChatModel chatModel = ...
ToolCallback[] dateTimeTools = ToolCallbacks.from(new DateTimeTools());
ChatOptions chatOptions = ToolCallingChatOptions.builder()
.toolCallbacks(dateTimeTools)
.build();
Prompt prompt = new Prompt("What day is tomorrow?", chatOptions);
chatModel.call(prompt);
那為什么好好的 FunctionCall 需要改為 ToolCall,我猜測背后的原因主要是 SpringAI 想去支持 MCP 協議,MCP 協議中規定了服務端程序需要具備 Prompt、Resource、Tools 三個要素(后文會有詳細講解),所以把 FunctionCall 的 API 全部修改為 ToolCall,讀者可以參考:docs.spring.io/spring-ai/r…[3] ?有兩種 API 的使用對比,改造的規模還是挺大的,不得不說以后升級 SpringAI 大版本又不知道會讓多少研發陷入 API 改造的深淵。
MCP(Model Context Protocol)又被稱之為模型上下文協議,MCP 就像是 AI 世界中的 “通用插座”,為 AI 模型與外部數據源和工具之間搭建起溝通的橋梁,致力于標準化應用程序為 LLM 提供上下文的方式,讓不同的 AI 組件和系統能夠順暢連接、協同工作。這個就有點像 Jdbc 協議一樣,通過定義規范,讓中間件廠商進行實現。我們看幾張來自 SpringAI 官網的圖示:
在上圖中我們可以發現,我們開發的大模型應用程序可以通過 MCPClient 去訪問外部資源,并且是典型的 Client-Server 架構。實現了大模型工具實現和工具調用的分離,就像通過一個第三方管家,來統一管理函數調用,在我看來 MCP 就是比 FunctionCalling 的更高一級抽像,也是實現智能體 Agent 的基礎。
在上面的圖示中我們可以發現目前的 MCP 服務端存在兩種類型一種是標準 IO,一種是 SSE 模式,對于標準 IO 的服務端和大模型應用是一對一的對應關系,而通過 SSE 方式進行客戶端服務端通信的模型,服務端則可以與客戶端進行一對多通信也許在不久的未來,目前的服務端開發人員,不再需要和前端頁面對接只需要和大模型的 McpClient 對接了,即一切服務端程序都需要接入 MCP 協議以更好的服務大模型和智能體。
下面我們開發一個簡單 demo,去體驗一下 SpringAI 的 MCP 能力,我們計劃通過 MCPClient 讓大模型具備訪問數據庫和本地文件系統的能力,實際效果如下圖所示:
這個 demo 中我們先不開發 MCP 服務端應用程序,而是利用 modelcontextprotocol.io/introductio…[4] ?中提供的一些開源 MCP 服務端實現。?此外額外說明下版本問題,考慮到目前大部分人使用的都是 SpringAI 1.0.0-M5 版本,所以本節先使用 SpringAI 1.0.0-M5 版本做演示,在第四節將具體演示 SpringAI 1.0.0-M6 的功能,也可以給大家做一個對比,方便大家比較兩個大版本的 API 改動。?
npx 是 nodeJs 下的一個工具可以執行一些 Ts 或者 JS 腳本甚至應用程序,uvx 的功能則和他很類似是 python 環境下執行腳本的工具,因為我們使用的文件服務器 MCP 服務端是使用 Ts 代碼寫的,而訪問數據庫的 MCP 服務端程序是用 Python 寫的所以我們必須去安裝下這兩個命令以支持 MCPClient 調用,簡單來說,使用 TypeScript 編寫的 MCP server 可以通過 npx 命令來運行,使用 Python 編寫的 MCP server 可以通過 uvx 命令來運行。uvx 安裝命令如下:
# 確保你已經安裝了python
pip install uvx
npx 則是隨著 npm 一起安裝的,一般情況下只要安裝了 npm 則也就自動安裝了 npx 工具。
這一步就比較簡單,筆者是使用 DeepSeek 生成的前端頁面,但是需要注意的是 Prompt 需要反復打磨,Deepseek 具備識圖能力也可以直接發圖給 Deepseek,記住需要在提示詞里面告訴大模型,不要用 Vue 或者 React, 因為我們的功能非常簡單,所以不需要上這種重量級框架,此外可以告訴大模型需要使用 EventSource 來接收服務端的數據以實現打字機效果
@Configuration
public class McpConfig {
@Bean
public List functionCallbacks(List mcpSyncClients) {
List list = new ArrayList();
for (McpSyncClient mcpSyncClient : mcpSyncClients) {
list.addAll(mcpSyncClient.listTools(null)
.tools()
.stream()
.map(tool -> new McpFunctionCallback(mcpSyncClient, tool))
.toList());
}
return list;
}
@Bean(destroyMethod = "close")
public McpSyncClient mcpFileSysClient() {
// 把這里的路徑記得改為自己的真實路徑
var stdioParams = ServerParameters.builder("D:\software\nodeJs\npx.cmd")
.args("-y", "@modelcontextprotocol/server-filesystem", "D:\工作日志")
.build();
var mcpClient = McpClient.using(new StdioClientTransport(stdioParams))
.requestTimeout(Duration.ofSeconds(10)).sync();
var init = mcpClient.initialize();
System.out.println("mcpFileSysClient loading init=" + init);
return mcpClient;
}
@Bean(destroyMethod = "close")
public McpSyncClient mcpDbClient() {
// 把這里的路徑記得改為自己的真實路徑
var stdioParams = ServerParameters.builder("D:\Program Files\python3.12.3\Scripts\uvx.exe")
.args("mcp-server-sqlite", "--db-path", "D:\work-space-study\spring-ai-mcp-demo\mcp-client\src\main\resources\test.db")
.build();
var mcpClient = McpClient.using(new StdioClientTransport(stdioParams))
.requestTimeout(Duration.ofSeconds(10)).sync();
var init = mcpClient.initialize();
System.out.println("mcpDbClient loading init=" + init);
return mcpClient;
}
}
在上面的代碼中我們聲明了兩個 McpClient 使用 stdIo(標準 IO)的方式去鏈接 MCP 服務端,并通過 @Bean(destroyMethod = "close") 讓 Spring 容器關閉的時候一并釋放客戶端占用,最后我們把兩個服務端提供的 tools 全部轉為 List 集合中,這個集合會在后面定義 ChatClient 中使用到。
public FileSysController(ChatClient.Builder chatClientBuilder,
List functionCallbacks) {
this.chatClient = chatClientBuilder
// 注意這里
.defaultFunctions(functionCallbacks.toArray(new McpFunctionCallback[0]))
.defaultAdvisors(new MessageChatMemoryAdvisor(new InMemoryChatMemory()))
.defaultOptions(DashScopeChatOptions.builder().withTopP(0.7).build())
.build();
}
在這里我們創建 ChatClient 并利用 Spring 的依賴注入能力注入了在之前 McpConfig 類中聲明的 functionCallbacks,至此這個大模型 ChatClient 就具備了調用 MCP 服務端的能力。使用大模型生成的前端頁面輸入問題訪問下面的 Controller 接口即可進行測試。
@RequestMapping(value = "/generate", method = RequestMethod.GET)
public Flux generateStream(HttpServletResponse response, @RequestParam("id") String id, @RequestParam("prompt") String prompt) {
response.setCharacterEncoding("UTF-8");
var messageChatMemoryAdvisor = new MessageChatMemoryAdvisor(chatMemory, id, 10);
return this.chatClient.prompt(prompt)
.advisors(messageChatMemoryAdvisor).stream().chatResponse();
}
在前面的開發環節中,我們使用的是開源的 MCP 服務端,如何自己用 Java 語言開發一個 MCP 服務端程序呢?本節將給出一個案例介紹使用 SpringAI 開發 MCPserver 的主要步驟?注意到這里你需要將你的 SpringAI 升級到 1.0.0-M6 版本,低版本的無法很好的支持 MCP 服務端調用?
開發 MCPserver 需要引入 SpringAI 下面三種依賴中的一種,取決于你打算使用什么通信方式
org.springframework.ai
spring-ai-mcp-server-spring-boot-starter
org.springframework.ai
spring-ai-mcp-server-webmvc-spring-boot-starter
org.springframework.ai
spring-ai-mcp-server-webflux-spring-boot-starter
一個標準的 MCP 服務端程序需要包含三個主要信息分別為 Tools、Prompts、Resources資源(Resources):資源是 AI 可以讀取的數據,比如文件內容、數據庫查詢結果或 API 的響應。 例如,AI 可能通過資源獲取你的日歷事件列表。工具(Tools):工具是 AI 可以調用的函數,用于執行特定操作,比如添加新任務或發送郵件,使用工具時,通常需要用戶先批準,以確保安全。提示詞(Prompts):提示詞是服務器提供給 AI 的預寫消息或模板,幫助 AI 理解如何使用資源和工具,例如,服務器可能告訴 AI:“你可以添加任務,試試說‘添加任務:買牛奶’”,從而幫助用戶更輕松地完成任務。提示詞雖然直接提供給 AI,但實際上是通過 AI 間接幫助用戶,比如 AI 會根據提示詞告訴用戶如何操作。這三個要素在 SpringAI 中對應的 API 如下:
@Configuration
@EnableWebMvc
public class McpServerConfig implements WebMvcConfigurer {
@Bean
public ToolCallbackProvider bookServiceTools(BookService bookService) {
// …… 注冊工具
return MethodToolCallbackProvider.builder().toolObjects(bookService).build();
}
@Bean
public List resourceRegistrations() {
// …… 注冊資源
return List.of(resourceRegistration);
}
@Bean
public List promptRegistrations() {
// ……注冊Prompt
return List.of(promptRegistration);
}
}
上面的代碼中 BookService 可以是你自己寫的一個服務,比如可以執行查詢 DB 或者調用其他微服務等代碼實現。最后還需要再 application.yaml 中進行服務暴露
# Using spring-ai-mcp-server-webmvc-spring-boot-starter
spring:
ai:
mcp:
server:
name: webmvc-mcp-server
version: 1.0.0
type: SYNC
sse-message-endpoint: /mcp/messages
至此你的服務端基本就開發完畢了。
在 4.1 中我們開發了我們自己的 MCP-server 那么怎么去讓大模型去調用它呢?相比 SpringAI1.0.0-M5 版本的 McpClient 集成方式,在 SpringAI 1.0.0-M6 那就發生了翻天覆地的變化。總體來說配置變得更加簡單:首先我們需要在 application.yaml 中做下述配置:
server:
port: 9999
spring:
ai:
mcp:
client:
enabled: true
name: call-mcp-server
sse:
connections:
server1:
# 這里是你的Mcp服務端的暴露地址
url: http://127.0.0.1:8080
dashscope:
api-key: {通義千問API-Key}
接著你需要在 pom 文件中引入下面的依賴:
org.springframework.ai
spring-ai-mcp-client-spring-boot-starter
1.0.0-SNAPSHOT
最后我們就可以直接使用了,詳細可以看下下面的代碼
@RestController
@RequestMapping("/dashscope/chat-client")
public class ChatController {
private final ChatClient chatClient;
private final ChatMemory chatMemory = new InMemoryChatMemory();
// 直接使用自動裝配就會自動注入McpClient
public ChatController(ChatClient.Builder chatClientBuilder,
List mcpSyncClients,
ToolCallbackProvider tools) {
this.chatClient = chatClientBuilder
// 注意這里API發生了變化 不再使用defaultFunctions
.defaultTools(tools)
.defaultOptions(DashScopeChatOptions.builder().withTopP(0.7).build())
.build();
}
@RequestMapping(value = "/generate_stream", method = RequestMethod.GET)
public Flux generateStream(HttpServletResponse response, @RequestParam("id") String id, @RequestParam("prompt") String prompt) {
response.setCharacterEncoding("UTF-8");
var messageChatMemoryAdvisor = new MessageChatMemoryAdvisor(chatMemory, id, 10);
return this.chatClient.prompt(prompt)
.advisors(messageChatMemoryAdvisor)
.stream()
.chatResponse();
}
}
我們對比下之前在 SpringAI1.0.0-M5 的客戶端實現方式就可以發現,現在我們不需要自己去定義 McpClient,由于我們引入了 spring-ai-mcp-client-spring-boot-starter 就可以自動識別我們配置的 mcpClient 信息,進行自動裝配,此外我們也可以發現我們創建 ChatClient 使用的 API 也不再是 defaultFunctions 而是 defaultTools。那就有聰明的讀者要問了那這樣配置的話我的超時時間怎么配置呢?SpringAI 提供了 McpSyncClientCustomizer 接口這個接口里面提供了定制邏輯,可以實現該接口做一些 McpClient 定制工作。最后我們先啟動 Mcp-Server 然后啟動我們的 Mcp-client 應用程序,開始提問,實際效果如下:
在上圖中大模型通過了 McpClient 調用了我在 Mcp-Server 提供了 BookService,然后按照名字查詢到了對應的書籍,這些書籍信息是我在代碼中預制的信息,大模型輸出這個信息也很好的證明了確實調用到了我們的 Mcp 服務端接口。
本文介紹了下目前比較火熱的 MCP 協議設計內容以及關鍵思想,同時基于 SpringAI 的兩個版本分別給出了具體的實現方案,希望對各位讀者有所幫助,由于 SpringAI 發展很快,內容如有錯誤歡迎溝通。也歡迎大家在評論區交流,源碼已經放于 github.com/AHUCodingBe…[5] ?,運行之前請前往阿里云百煉平臺[6]申請自己的 API-Key,目前 SpringAI 最新 1.0.0-M6 版本的網上資源比較少,如果源碼對你有幫助,歡迎 Star。內容創作不易,轉載請務必注明出處,謝謝。點擊關注公眾號,“技術干貨” 及時達!
原文轉載自:https://mp.weixin.qq.com/s/IiZ5-VGXvQTpr2a7Ds0ciw