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