简述MCP的原理-AI时代的USB接口
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的发展提供了一种全新的交互模式和更多的可能。