1 簡介
隨著AI的不斷發展,RAG(檢索增強生成)和function calling等技術的出現,使得大語言模型的對話生成能力得到了增強。然而,function calling的實現邏輯比較復雜,一個簡單的工具調用和實現方式需要針對不同的系統和大模型單獨編寫適配接口,十分復雜。
在此背景下,mcp應運而生,為當前業內AI高效可靠地調用外部工具實現了標準化。下面,我將帶大家一起認識下mcp的基本原理和實現方式。
2 執行流程
在我們開始今天的正題之前,需要先了解下通常用戶與大模型進行一次交互的執行流程:↓
當用戶問“北京今天天氣怎么樣”時,我們的程序會將用戶的問題、以及預先識別到的工具列表包裝成提示詞發送給大模型。熟悉function calling原理的小伙伴們都知道,這時候大模型會基于預訓練的function calling技術識別到想要調用的工具是什么,并將其結構化輸出。我們的程序識別后再去調用對應的工具,最后將得到的結果和之前的上下文再次發送給大模型,得到最終的結果返回給用戶。當然,如何讓大模型選擇要調用的工具不是本期的重點,這里不再贅述。
我們需要關注重點在于工具調用的這部分邏輯,在mcp沒有誕生之前是這樣子調用的:
每次新增一個系統,都需要開發者單獨做適配,即使tool的功能很簡單,也會有極大的重復開發量。 在mcp出現后,調用方式發生了變化:
系統與工具的調用方式實現了解耦,調用邏輯統一封裝到了mcp client和 mcp server之間,這一步的交互方式由官方提供了不同開發語言的sdk,不再需要我們開發者處理了。
3 mcp架構
3.1 mcp架構設計
接下來讓我們詳細看下mcp的架構設計,mcp實現采用了標準的C/S架構模式。
host:用于承載接受用戶請求,與大模型交互,調用工具的一段程序。廣義上我們可以將其看作是一個AI Agent。
client: 基于mcp規則實現的客戶端,負責與mcp服務端進行通信。
server: 基于mcp規則實現的服務端,實現了工具內部的邏輯操作,并將執行結果返回給mcp客戶端。
3.2 mcp基本功能
當下主流的與大模型交互的三要素無非是:工具、資源、提示詞,而mcp針對這三類均做了標準化處理。 以下是幾個重要的功能:
Resource:類似文件的數據,可以被客戶端讀取,如數據庫數據或文件內容。
Tools:可以被大模型調用的函數。
prompt:預先編寫的模板,幫助用戶完成特定任務。
sampling:允許server主動通過client調用大模型獲取數據進行采樣。
4 mcp通信原理
4.1 JSON-RPC
MCP采用JSON-RPC作為底層的通信協議。JSON-RPC是一種基于JSON的輕量級遠程調用協議,相較于HTTP來說它更加簡潔、高效、容易處理。
請求結構體
{jsonrpc:?"2.0",id: number | string,method: string,params?: object
}
響應結構體
{jsonrpc:?"2.0",id: number | string,result?: object,error?: {code: number,message: string,data?: unknown}
}
在發起通信的源碼中我們也可以看到確實使用到了json-rpc
?@Override
public?<T>?Mono<T>?sendRequest(String method, Object requestParams, TypeReference<T> typeRef)?{String requestId =?this.generateRequestId();return?Mono.<McpSchema.JSONRPCResponse>create(sink -> {this.pendingResponses.put(requestId, sink);// 構建json-rpc請求McpSchema.JSONRPCRequest jsonrpcRequest =?new?McpSchema.JSONRPCRequest(McpSchema.JSONRPC_VERSION, method,requestId, requestParams);// 發送請求this.transport.sendMessage(jsonrpcRequest).subscribe(v -> {}, error -> {this.pendingResponses.remove(requestId);sink.error(error);});}).timeout(this.requestTimeout).handle((jsonRpcResponse, sink) -> {// 省略異常處理});}
json-rpc與http的對比
屬性 | HTTP | JSON-RPC |
---|---|---|
本質 | 應用層協議(Web核心協議) | 輕量級RPC協議(基于JSON格式) |
數據格式 | 支持JSON/XML/二進制等多種格式 | 強制JSON格式,結構更簡潔 |
協議功能 | 包含緩存/認證/狀態碼等完整功能 | 僅定義RPC調用規范(無底層邏輯) |
通信模式 | 無狀態,支持GET/POST等多方法 | 無狀態,基于method字段調用 |
適用場景 | Web API、瀏覽器交互、復雜業務 | 微服務內部調用、物聯網等輕量場景 |
典型應用 | RESTful接口、網頁加載 | 服務間函數調用、嵌入式設備通信 |
4.2 通信方式
mcp基于以上通信協議,實現了以下通信方式:
STDIO
采用STDIO的方式,server端會在client端啟動時,作為client端的子進程一起啟動。這種方式適用于client和server在同一臺機器上通信的場景,通常用于工具調試。 它的實現原理是client和server兩個進程間通過stdin和stdout進行雙向通信。
優點:
無外部依賴
進程間通信極快
脫機可用
缺點
并發能力差,是同步阻塞模型
不支持多進程通信
SSE
全名是server send event,是一種基于服務端到客戶端的流式傳輸方式,同時客戶端向服務端通信采用http的方式進行傳輸。一般用于client在本地,server在遠程服務器的場景。
具體執行流程如下:
客戶端會向服務端的
/sse
端點發送http請求,服務端會返回sessionID等信息建立sse連接。初始化連接完成后,客戶端會向服務端請求
tools/list
接口獲取所有的tool列表,用于之后發送給大模型。在工具調用時,客戶端會將調用信息如
method
,args
通過post請求調用tools/call
接口發送給服務端處理,服務端通過sse連接通知客戶端結果。
從本質上看,sse是一種異步非阻塞的通信模型,極大的提高了agent的吞吐能力,但其服務器和客戶端需要做長連接容易連接中斷,會丟失上下文。而官方在今年又推出了一項通信方式的更新,使用streamable http
替代sse解決了以上的問題。
5 生命周期
以下是mcp的生命周期:
在mcp client和mcp server建立連接后,client會立即向server請求獲取可用的工具列表,這里也體現了mcp工具的動態可插拔性。 接下來我將用Spring AI帶大家一起了解下mcp client的調用流程。
我們需要引入Spring AI的maven依賴,以及對spring AI對Mcp的依賴。
5.1 環境搭建
我們需要在server端向外暴露一個工具。
? ??/** 構建根據城市獲取天氣的tool*?@param?city 城市名稱*?@return?天氣信息*/@Tool(name =?"getWeather", description =?"根據城市獲取天氣")public?String?getWeather(String city)?{return?new?String((city).getBytes(), StandardCharsets.UTF_8) +?" 天氣為晴天 25℃";}
SpringAi會將標有@Tool注解的方法自動注入到ToolCallbackProvider
中。 在client端,我們需要配置下mcp server的地址。
spring:ai:mcp:client:sse:connections:server1:?# sse服務端url:?http://127.0.0.1:8080
寫一個demo來模擬用戶詢問大模型的流程。
? ??@Beanpublic?CommandLineRunner?callToolByLLM(ChatClient.Builder chatClientBuilder,ToolCallbackProvider toolCallbackProvider,ConfigurableApplicationContext context)?{return?args -> {System.out.println("基于spring-ai,llm調用方法------");Gson gson =?new?Gson();// 模擬用戶輸入的信息,并把工具列表傳給LLMString userInput =?"獲取北京的天氣";System.out.println("用戶問: "?+ userInput);var?chatClient = chatClientBuilder.defaultUser("獲取北京的天氣").defaultTools(toolCallbackProvider).build();// 包裝請求LLMString content = chatClient.prompt(userInput).call().content();System.out.println("AI回答: "?+ gson.toJson(content));// 結束會話context.close();};}
5.2 建立連接獲取可用工具列表
當程序啟動后,spring會自動注入McpClient和ToolCallbackProvider,此時會向server端發送請求獲取所有可用的工具列表。
public?class?SyncMcpToolCallbackProvider?implements?ToolCallbackProvider?{@Overridepublic?ToolCallback[] getToolCallbacks() {var?toolCallbacks =?new?ArrayList<>();this.mcpClients.stream().forEach(mcpClient -> {// mcpClient.listTools()toolCallbacks.addAll(mcpClient.listTools().tools().stream().filter(tool -> toolFilter.test(mcpClient, tool)).map(tool ->?new?SyncMcpToolCallback(mcpClient, tool)).toList());});var?array = toolCallbacks.toArray(new?ToolCallback[0]);validateToolCallbacks(array);return?array;}
}
mcpClient會用json-rpc的格式調用tools/list
方法,獲取當前server下所有可用的工具列表。
public?Mono<McpSchema.ListToolsResult> listTools(String cursor) {return?this.withInitializationCheck("listing tools", initializedResult -> {if?(this.serverCapabilities.tools() ==?null) {return?Mono.error(new?McpError("Server does not provide tools capability"));}return?this.mcpSession.sendRequest(McpSchema.METHOD_TOOLS_LIST,?new?McpSchema.PaginatedRequest(cursor),LIST_TOOLS_RESULT_TYPE_REF);});
}
5.3 調用工具
當用戶詢問"北京今天天氣怎么樣"時,程序會將上述獲取到的所有工具和用戶的信息生成提示詞告訴大模型,大模型選擇一個合適的工具告訴程序去調用工具。
? ??public?ChatResponse?internalCall(Prompt prompt, ChatResponse previousChatResponse)?{// 構建提示詞、工具ChatCompletionRequest request = createRequest(prompt,?false);// 構建要調用的大模型信息ChatModelObservationContext observationContext = ChatModelObservationContext.builder().prompt(prompt).provider(OpenAiApiConstants.PROVIDER_NAME).requestOptions(prompt.getOptions()).build();ChatResponse response = ChatModelObservationDocumentation.CHAT_MODEL_OPERATION.observation(this.observationConvention, DEFAULT_OBSERVATION_CONVENTION, () -> observationContext,this.observationRegistry).observe(() -> {// post請求大模型ApiResponseEntity<ChatCompletion> completionEntity =?this.retryTemplate.execute(ctx ->?this.openAiApi.chatCompletionEntity(request, getAdditionalHttpHeaders(prompt)));// 解析結果省略步驟 ...return?chatResponse;});// 判斷是否是工具調用if?(toolExecutionEligibilityPredicate.isToolExecutionRequired(prompt.getOptions(), response)) {var?toolExecutionResult =?this.toolCallingManager.executeToolCalls(prompt, response);// 判斷是否返回結果if?(toolExecutionResult.returnDirect()) {// Return tool execution result directly to the client.return?ChatResponse.builder().from(response).generations(ToolExecutionResult.buildGenerations(toolExecutionResult)).build();}else?{// 帶著工具結果直接調用returnthis.internalCall(new?Prompt(toolExecutionResult.conversationHistory(), prompt.getOptions()),response);}}return?response;}
這里我們對大模型返回的結果進行抓包,可以看到大模型想要調用的方法信息
[{"assistantMessage": {"toolCalls": [{"id":?"call_b4a9cb0f04a3495d941b71","type":?"function","name":?"spring_ai_mcp_client_server1_getWeather","arguments":?"{\"city\": \"北京\"}"}],// 中間內容省略..."chatGenerationMetadata": {"metadata": {},"finishReason":?"TOOL_CALLS","contentFilters": []}}
]
mcpClient執行調用邏輯。
?public?Mono<McpSchema.CallToolResult> callTool(McpSchema.CallToolRequest callToolRequest) {return?this.withInitializationCheck("calling tools", initializedResult -> {if?(this.serverCapabilities.tools() ==?null) {return?Mono.error(new?McpError("Server does not provide tools capability"));}return?this.mcpSession.sendRequest(McpSchema.METHOD_TOOLS_CALL, callToolRequest, CALL_TOOL_RESULT_TYPE_REF);});}
執行完成后,程序會攜帶結果和上下文再次請求大模型獲取結果,直到大模型認為可以結束了,會將最終的結果返回給用戶。 此次請求的執行結果如下:
6 總結
本文介紹了mcp的基本底層原理,mcp作為AI大模型時代的標準化交互協議,具備顯著的優勢。對于開發者來說mcp的出現降低了功能集成的成本,有更大的發展前景。但mcp當下也有很多不可回避的缺點,比如頻繁與大模型交互,為了保證消息連貫上下文內容劇增,token消耗大,使用成本變高。另外在安全性方面不夠健全,對于提示詞注入等手段沒有成熟的解決方案。
盡管mcp當前不是那么的完美無缺,但他的出現給AI的發展提供了一種全新的交互模式和更多的可能。