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