SpringAI1.0下的MCP的异步请求和同步请求的区别
代码简单解释
private final ChatClient chatClient;public OllamaMCPClien(ChatClient.Builder chatClientBuilder, List<McpSyncClient> mcpSyncClients) {// 使用 chatClientBuilder 构建 ChatClient 实例this.chatClient = chatClientBuilder// 设置默认的系统消息,指导 AI 的行为。// 这里明确告诉AI它是一个必须使用工具来回答问题的助手。// 强调了当用户提问时,应优先寻找并使用可用工具(如天气查询或回显),而不是直接回答。.defaultSystem("你就是一个执行器,你可以调取远程的MCP的工具来进行执行,必须要执行一个工具")// 注册 MCP 工具回调,它会发现并集成所有可用的MCP工具.defaultToolCallbacks(new SyncMcpToolCallbackProvider(mcpSyncClients))// 添加消息历史记忆功能,以支持多轮对话.defaultAdvisors(MessageChatMemoryAdvisor.builder(MessageWindowChatMemory.builder().build()).build()).build();
}
这里的代码演示的就是一段同步请求MCPServer的请求,当用户想要调取一个被MCPClient管理的Server的管理的列表的话,那么种类进行同步的请求的话会直接发送请求的阻塞,只有当用户请求的MCPServer的请求被响应之后才会继续进行服务的响应和处理,如果LLM没有找到服务进行处理的话,会直接被LLM处理结果然后直接返回给用户结果。
详细对比
我用一个比喻来解释:
- 同步 (Synchronous):就像是打电话。你拨通电话问对方一个问题,你必须在线上一直等着,直到对方想好答案告诉你,然后你才能挂电话去做别的事情。在等待期间,你什么也做不了。
- 异步 (Asynchronous):就像是发短信/邮件。你把问题发给对方,然后就可以马上去做自己的事情了。对方收到后,有空了会处理,处理完再把答案发给你。你随时可以查看有没有新消息,而不需要一直干等着。
现在我们把这个概念应用到你的 MCP
客户端和服务端上:
同步 (Synchronous) - McpSyncClient
在你的代码里,你正在使用 McpSyncClient
,这是一个同步客户端。
-
工作方式:
- 当
OllamaMCPClien
(AI) 决定调用一个远程工具时,它会通过McpSyncClient
向MCP-SERVER
发起一个网络请求(比如 HTTP 请求)。 - 发起请求的那个线程会被阻塞 (block),进入等待状态。
- 这个线程会一直等待,直到
MCP-SERVER
上的工具执行完毕,并将结果通过网络返回。 - 收到返回结果后,该线程才会被唤醒,继续执行后面的代码。
- 当
-
优点:
- 简单直观:代码的执行流程是线性的,“调用->等待->返回”,非常容易理解和调试。
- 实现简单:不需要处理复杂的回调函数、
Future
或响应式编程模型。
-
缺点:
- 性能和资源瓶颈:这是最主要的区别。如果
MCP-SERVER
上的工具执行时间很长(例如,需要几秒钟甚至更久),那么MCP-CLINE
这边的调用线程就会被长时间占用。在高并发场景下,如果大量请求都在等待慢速工具的返回,服务器的线程资源会很快被耗尽,导致系统吞吐量下降,无法响应新的请求。
- 性能和资源瓶颈:这是最主要的区别。如果
异步 (Asynchronous) - (如果存在 McpAsyncClient
)
如果有一个异步的实现(我们称之为 McpAsyncClient
),它的工作方式会完全不同。
-
工作方式:
- 当 AI 决定调用一个工具时,
McpAsyncClient
会向MCP-SERVER
发起请求。 - 但是,它不会等待
MCP-SERVER
的响应。调用会立即返回,通常返回一个“凭证”,比如CompletableFuture<T>
或Mono<T>
(在使用 Spring WebFlux 的情况下)。 - 发起调用的线程不会被阻塞,它可以立即去处理其他任务。
- 当
MCP-SERVER
处理完请求并返回结果时,会通过回调机制或者完成CompletableFuture
来通知客户端。一个独立的线程(或线程池中的线程)会接着处理这个返回的结果。
- 当 AI 决定调用一个工具时,
-
优点:
- 高吞吐量和高伸缩性:线程不会因为等待 I/O (网络、磁盘) 操作而被阻塞。少量的线程就可以处理大量的并发请求,系统资源利用率极高。
- 更好的用户体验:对于需要调用多个工具或长耗时工具的场景,系统不会被单个请求卡死,整体响应性更好。
-
缺点:
- 编程模型更复杂:你需要处理回调、
Futures
或响应式流 (Reactive Streams)。代码不再是简单的线性执行,调试和排查问题也相对困难一些(比如所谓的 “Callback Hell”,尽管现代Java已经大大改善了这一点)。
- 编程模型更复杂:你需要处理回调、
总结
特性 | 同步 (Synchronous) | 异步 (Asynchronous) |
---|---|---|
通信模型 | 阻塞式,客户端必须等待服务端响应 | 非阻塞式,客户端发送请求后无需等待 |
资源占用 | 每一个请求在等待时都会占用一个线程 | 少量线程可处理大量并发请求,资源利用率高 |
性能 | 性能较低,容易因慢速任务而产生瓶颈 | 性能高,适合I/O密集型和高并发场景 |
编程复杂度 | 简单,代码逻辑直观 | 复杂,需要处理回调或响应式编程 |
适用场景 | 逻辑简单、快速返回的调用、低并发场景 | I/O密集型、高并发、长耗时任务的调用 |
对于你的 MCP
架构来说,因为涉及到微服务之间的网络通信,这本身就是一种 I/O 操作。如果你的工具都是毫秒级就能快速返回的,那么使用同步模型是完全可以接受的,因为它更简单。但如果你的工具可能涉及到复杂计算、访问数据库、调用第三方API等耗时操作,那么异步模型会是更健壮、性能更好的选择。