Spring AI 实战:第九章、Spring AI MCP之万站直通

引言:AI模型们的"巴别塔困境"

在人工智能的江湖里,各大门派(模型)各怀绝技:GPT-4的"舌灿莲花"、Stable Diffusion的"妙笔生花"、Claude的"逻辑鬼才"、DeepSeek的"庖丁解牛"、通义千问的"八卦推演"。但要让这些"武林高手"同台竞技,就像让李白、达芬奇和爱因斯坦开圆桌会议,专业术语满天飞,协议标准各不同,最后可能演变成"鸡同鸭讲"的惨剧。

2024年11月,Anthropic正式发布模型上下文协议MCP(Model Context Protocol)试图尝试解决这种困境,虽然该协议没有成为行业规范,但在国内外已有较强的共识,各大厂商陆续都开始支持MCP。

一、认识MCP

1.1 基本概念

1.1.1 定义

MCP 是一种开放协议,旨在标准化 应用程序向大语言模型(LLM)提供上下文的交互方式。我们可以把 MCP 比作AI 应用的USB-C通用接口,正如USB-C为各类设备提供标准化连接方案,MCP为AI模型与不同数据源/工具建立了统一对接规范

1.1.2 作用

该协议助力开发者在 LLM 之上构建智能体和复杂工作流。由于 LLM 常需整合多方数据与工具,MCP 提供:

  • 即插即用集成库:持续扩展的预构建集成方案
  • 供应商灵活切换:支持不同 LLM 供应商的无缝迁移
  • 数据安全最佳实践:基于基础设施的数据防护机制

1.1.3 架构

采用经典的客户端-服务器架构,支持宿主应用连接多个服务节点:

  • MCP宿主:如Claude桌面版、IDE或AI 工具,通过MCP获取数据的应用程序
  • MCP客户端:协议客户端,维护与服务端1:1连接的通信管道
  • MCP 服务器:轻量化服务程序,通过标准化协议暴露特定能力
    • 本地数据源:计算机本地的文件/数据库/服务,MCP 服务器安全访问的内部资源
    • 远程服务:通过 API 连接的互联网外部系统(如云服务)

通过这种架构设计,MCP 既保证了本地数据的安全性 (敏感信息不出域),又实现了云端服务的扩展性,堪称 AI 时代的"数据外交官协议"。

1.2 MCP小试

说概念总是生涩难懂, 直接看示例

1.2.1 安装Cline

下载Visual Studio Code,https://code.visualstudio.com/后安装Cline,配置大模型的API-KEY,Cline作为集成MCP Client的宿主机,可以安装MCP Server

1.2.2 Mcp Server安装

选择右上角的图标,进入公开可访问的MCP Server MarketPlace,选择Github Starts可以看到按热门的MCP

选择File System,它是一个Node.js(本机需要提前完成Node.js的安装)实现的电脑文件操作服务,点Install进行安装,如果点文件名称可直接跳转到对应github地址

安装过程比较简单,按照提示操作点确定即可,最后配置可访问的文件目录/Users/celen/Desktop

然后输入桌面上有几个文件在桌面创建一个hello.txt的文件会自动执行文件的读或写

1.2.3 配置项

在Installed列出已安装的MCP Servers列表,其中Configure MCP Servers(文件名cline_mcp_settings.json)为对应配置文件

{

"mcpServers": {

复制代码
"github.com/modelcontextprotocol/servers/tree/main/src/filesystem": {

  "autoApprove": [],

  "disabled": false,

  "timeout": 60,

** "command": "npx",**

复制代码
  "args": [

    "-y",

    "@modelcontextprotocol/server-filesystem",

** "/Users/celen/Desktop"**

复制代码
  ],

  "transportType": "stdio"

}

}

}

mcpServers定义多个MCP Server的配置(包含运行的命令、参数等),该配置可以手动修改或者走可视化安装时自动写入

  • github.com/.../filesystem: 标识该 Server 的类型或来源
  • autoApprove:定义需要**自动批准**的权限或操作列表(例如文件读写、网络访问等),空数组 [] 表示不自动批准任何额外权限,需手动授权
  • disabled:是否禁用该 MCP Server 实例,false表示启用该服务
  • timeout:设置 Server 的超时时间(单位:秒)60 表示如果 Server 在 60 秒内无响应,则认为操作超时。
  • command:指定启动该 MCP Server 的命令行工具,npx 表示使用 Node.js 的包执行工具,python执行Python包,<font style="color:#000000;">java执行Java包
  • args:传递给 command 的参数列表,
    • "-y":可能表示自动确认(类似 --yes),避免交互式提示
    • "@modelcontextprotocol/server-filesystem":指定要运行的 npm 包名称
    • "/Users/celen/Desktop":文件系统 Server 的根目录路径(服务将监控或操作此目录)
  • transportType:定义客户端与 Server 的通信方式,"stdio" 表示通过标准输入输出(stdin/stdout)进行通信(常见于本地进程间通信)

二、自定义MCP Server

前面安装的MCP Server是由第三方开发完成,那通过Spring AI如何开发一个呢?在Spring AI中支持两类(三种)传输机制,标准输入/输出(STDIO)、服务端主动向客户端发送事件流SSE(Server-Sent Events),其中SSE可以拆分为基于Spring MVC框架实现的SSE和基于Spring WebFlux响应式编程模型实现的 SSE。

基于STDIO形式是将MCP Server当做一个本地的子进程,基于SSE可将MCP Server部署在远端,各有千秋

2.1 STDIO

2.1.1 应用开发

创建工程 ,选择依赖**Model Context Protocol Server**

开发一个天气服务,定义两个工具,具体实现直接mock

java 复制代码
@Service
public class WeatherService {

    @Tool(description = "获取指定经纬度地点的天气预报")
    public String getWeatherForecastByLocation(double latitude,   // Latitude coordinate
                                               double longitude   // Longitude coordinate
    ) {
        // Implementation
        return "天气一片晴朗V2 " + System.currentTimeMillis() + "," + latitude + "," + longitude;
    }

    @Tool(description = "获取指定地域的天气预警")
    public String getAlerts(String state  // Two-letter US state code (e.g., CA, NY)
    ) {
        // Implementation
        return "快跑,有毒V2," + System.currentTimeMillis() + "," + state;
    }
}

定义ToolCallbackProviderBean

java 复制代码
@Bean
public ToolCallbackProvider weatherTools(WeatherService weatherService) {
return MethodToolCallbackProvider.builder().toolObjects(weatherService).build();
}

application.properties中配置

java 复制代码
spring.application.name=mcp-stdio
spring.ai.mcp.server.name=mcp-stdio-weather
spring.ai.mcp.server.version=0.0.1
spring.ai.mcp.server.stdio=true
spring.main.banner-mode=off
logging.file.name=/Users/celen/Desktop/log/mcp-stdio.log

./mvnw clean install打包,在target目录下找到demo-mcp-server-0.0.1-SNAPSHOT.jar

2.1.2 配置运行

打开cline_mcp_settings.json文件添加如下配置,左侧就展示成功安装对应MCP Server

java 复制代码
   "mcp-stdio-weather":
    {
        "command": "java",
        "args":
        [
          
            "-Dspring.ai.mcp.server.stdio=true",
            "-Dspring.main.web-application-type=none",
            "-Dlogging.pattern.console=",
            "-jar",
                 "/Users/celen/Documents/code/spring-ai-action/mcp-stdio/target/mcp-stdio-0.0.1-SNAPSHOT.jar"
        ]
    }
  • command:"java",指定使用 Java 运行时来执行 JAR 文件。
  • args:列表中的参数
    • -Dspring.ai.mcp.server.stdio=true :启用 MCP 服务器的 STDIO(标准输入输出)传输模式。服务器将通过 stdin/stdout 与客户端通信(无需 HTTP 端口)。
    • -Dspring.main.web-application-type=none:强制禁用 Spring Boot 的 Web 容器(如 Tomcat、Netty),因为使用了 STDIO 模式,不需要启动 HTTP 服务,避免不必要的资源占用(如端口冲突)。
    • Dlogging.pattern.console= : 清空控制台日志的输出格式,默认情况下,Spring Boot 会输出带颜色和格式的日志,设为空字符串可减少日志干扰(适合作为子进程运行时)。
    • -jar + JAR 文件路径:指定要运行的 Spring Boot 打包的 JAR 文件,/Users/celen/.../mcp-stdio-0.0.1-SNAPSHOT.jar 是本地构建的 Spring Boot 可执行 JAR。

展开mcp-stdio-weather可看到该服务提供的工具清单

2.1.3 测试

  • 查看杭州天气

在配置项中设置autoApprove就可以自动执行工具调用,避免手动确认(也可以走可视化界面设置)

  • 杭州有天气预警吗系统模拟返回杭州当前有天气预警:有毒物质警告(建议迅速采取防护措施),经过加工后大模型还做了友善的提醒

2.2 SSE

2.2.1 基本概念

SSE(Server-Sent Events)是一种基于HTTP的服务器推送技术,允许服务端主动向客户端发送事件流(如实时数据更新)。

特点

  • 单向通信:服务端 → 客户端(客户端通过普通HTTP请求交互)。
  • 文本协议:基于纯文本,默认使用 text/event-stream 格式。
  • 自动重连:客户端内置断线重试机制。
  • 轻量级:相比 WebSocket,SSE更简单,适合单向数据推送场景(如股票行情、实时日志)。
Spring MVC实现的SSE

Spring MVC提供的SSE实现,基于Servlet异步处理

特点:

  • 阻塞 IO:底层依赖 Servlet 线程模型,每个 SseEmitter 占用一个线程
  • 同步编程模型:需手动管理线程(如 ExecutorService)
  • 兼容性:适用于传统 Spring MVC 应用

局限性:

  • 线程阻塞:大量并发连接时,线程池可能耗尽
  • 扩展性差:不适合高并发场景(如万级连接)
java 复制代码
@RestController
public class HelloController {

    @GetMapping("/sse-mvc")
    public SseEmitter handleSse() {
        SseEmitter emitter = new SseEmitter(30000L); // 超时时间 30 秒
        // 模拟推送事件
        new Thread(() -> {
            try {
                for (int i = 0; i < 100; i++) {
                    emitter.send("Event " + i);
                    Thread.sleep(100);
                }
                emitter.complete();
            } catch (Exception e) {
                emitter.completeWithError(e);
            }
        }).start();
        return emitter;
    }
}
Spring WebFlux实现的SSE

Spring WebFlux实现的SSE,基于Reactive Streams(响应式流),使用Reactor的Flux推送事件

特点:

  • 非阻塞 IO:基于 Netty 或 Reactor-Netty,支持高并发(如 10K+ 连接)
  • 函数式编程:通过 Flux/Mono 声明式组合事件流
  • 资源高效:占用少量线程(EventLoop 线程池)

优势:

  • 背压支持:客户端可控制数据流速
  • 集成 Reactive 生态:无缝对接 R2DBC、WebClient 等响应式组件
java 复制代码
@RestController
public class HelloController {

    @GetMapping("/sse-flux")
    public Flux<ServerSentEvent<String>> handleSseFlux() {
        return Flux.interval(Duration.ofMillis(100))
                .map(sequence -> ServerSentEvent.<String>builder()
                        .id(String.valueOf(sequence))
                        .event("事件")
                        .data("SSE in WebFlux - " + sequence)
                        .build());
    }
}
差异对比
特性 Spring MVC (SseEmitter) Spring WebFlux (Flux)
底层技术 Servlet 异步(阻塞 IO) Reactor-Netty(非阻塞 IO)
编程模型 同步(需手动管理线程) 响应式(声明式流处理)
并发能力 低(受限于线程池大小) 高(基于 EventLoop,支持百万级连接)
资源消耗 高(每个连接占用一个线程) 低(少量线程处理所有连接)
协议支持 仅 SSE SSE + WebSocket + 其他响应式协议
适用场景 传统 MVC 应用,低并发需求 高并发、实时性要求高的场景(如 IoT、聊天)

2.2.2 基于SpringMVC的MCP Server

添加依赖

java 复制代码
<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.ai</groupId>
			<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
		</dependency>

代码开发

WeatherService该服务类和前面保持一致,额外增加McpConfig

  • 通过WebMvcSseServerTransportProvider构建信息传输Provider,在Endpoint为sse时处理get请求,mcp处理post请求
  • 访问 http://localhost:8080/sse 得到如下信息标识服务启动成功

id:107455ca-e6c4-4802-be43-7162ef884c7f

event:endpoint

data:/mcp?sessionId=107455ca-e6c4-4802-be43-7162ef884c7f

java 复制代码
@Configuration
public class McpConfig implements WebMvcConfigurer {
    @Bean
    public WebMvcSseServerTransportProvider transportProvider(ObjectMapper mapper) {
        return new WebMvcSseServerTransportProvider(mapper, "/mcp"); // 基础路径设为/mcp
    }

    @Bean
    public RouterFunction<ServerResponse> mcpRouterFunction(
            WebMvcSseServerTransportProvider transportProvider) {
        return transportProvider.getRouterFunction();
    }

    @Bean
    public ToolCallbackProvider weatherTools(WeatherService weatherService) {
        return MethodToolCallbackProvider.builder().toolObjects(weatherService).build();
    }

}

测试

在Cline中添加Remote Servers,Server URL=http://localhost:8080/sse,点击添加后可在已安装的Server中看到对应信息(注意提前把基于stdio的配置给删除,避免冲突)

测试可发现会正常运行

2.2.3 基于WebFlux的MCP Server

基于webFlux的流程基本和webMVC雷同,但注意不需要 添加spring-boot-starter-web,不用创建McpConfig(自动配置已完成),启用NettyWebServer

添加依赖

java 复制代码
	<dependency>
			<groupId>org.springframework.ai</groupId>
			<artifactId>spring-ai-starter-mcp-server-webflux</artifactId>
		</dependency>

三、自定义MCP Client

自定义MCP Server的测试上面都是基于Cline完成,它作为MCP中的宿主机,内部已集成MCP Client能力,抛开Cline,可以自定义MCP Client(生产环节可在集成大模型应用中添加MCP Client,一个MCP HOST就构建成功)。

3.1 STDIO

在Spring AI工程中依赖spring-ai-starter-mcp-client来集成客户端能力

添加依赖:

xml 复制代码
<dependency>
  <groupId>org.springframework.ai</groupId>
  <artifactId>spring-ai-starter-mcp-client</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.ai</groupId>
  <artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>

application.properties:

  • spring.ai.mcp.client.stdio.servers-configuration : 配置MCP Server相关服务信息
  • spring.ai.mcp.client.toolcallback.enabled:启用工具回调功能
xml 复制代码
spring.ai.openai.api-key=sk-***

spring.ai.openai.base-url=https://api.deepseek.com

spring.ai.openai.chat.options.model=deepseek-chat


spring.ai.mcp.client.stdio.servers-configuration=classpath:/mcp-servers-config.json
spring.ai.mcp.client.toolcallback.enabled=true

mcp-servers-config.json:

  • 类似在Cline中的配置
json 复制代码
{
  "mcpServers": {
    "mcp-stdio-weather":
    {
      "command": "java",
      "args":
      [

        "-Dspring.ai.mcp.server.stdio=true",
        "-Dspring.main.web-application-type=none",
        "-Dlogging.pattern.console=",
        "-jar",
        "/Users/celen/Documents/code/spring-ai-action/mcp-stdio/target/mcp-stdio-0.0.1-SNAPSHOT.jar"
      ]
    }

  }
}

调用Server代码:

java 复制代码
@Bean
public CommandLineRunner predefinedQuestions(ChatClient.Builder chatClientBuilder, ToolCallbackProvider tools, ConfigurableApplicationContext context) {

    String userInput = "查询杭州天气预警";

    return args -> {

        var chatClient = chatClientBuilder.defaultTools(tools).build();

        System.out.println("\n>>> QUESTION: " + userInput);
        System.out.println("\n>>> ASSISTANT: " + chatClient.prompt(userInput).call().content());

        context.close();
    };
}

输出:

QUESTION: 查询杭州天气预警
ASSISTANT: 杭州的天气预警信息如下:

  • 预警内容: 快跑,有毒

请注意安全,并关注相关部门的最新通知!

3.2 SSE

以webflux的形式来演示SSE,流程和STDIO类似

引入依赖:

xml 复制代码
<dependency>
  <groupId>org.springframework.ai</groupId>
  <artifactId>spring-ai-starter-mcp-client-webflux</artifactId>
</dependency>

application.properties:

  • spring.ai.mcp.client.sse.connections.自定义的服务名.url : 配置MCP Server地址
java 复制代码
spring.ai.mcp.client.sse.connections.weather.url=http://localhost:8080

其他逻辑保持一致可成功调用Server。

3.3 集成高德地图

目前国内阿里云百炼 https://bailian.console.aliyun.com/?tab=mcp#/mcp-market、 支付宝开放平台 https://opendocs.alipay.com/open/0go80l 等公司都开始支持MCP,通过代码Client的集成高德地图服务 https://bailian.console.aliyun.com/?tab=mcp#/mcp-market/detail/amap-maps ,请参考文档完成key的注册

官方示例为:要求在sse后面追加key

java 复制代码
{
  "mcpServers": {
    "amap-amap-sse": {
      "url": "https://mcp.amap.com/sse?key=您在高德官网上申请的key"
    }
  }
}

配置问题:

通过上文知道在Spring AI中添加SSE Server的URL格式,不需要添加sse路径,

java 复制代码
spring.ai.mcp.client.sse.connections.服务名称.url=http://localhost:8080(访问URL)

如果按照高德的示例直接配置spring.ai.mcp.client.sse.connections.map.url=https://mcp.amap.com/sse?key=您在高德官网上申请的key,启动应用直接抛错;因为代码会自动在URL后追加sse路径,理论应配置为spring.ai.mcp.client.sse.connections.map.url=https://mcp.amap.com,但这样就丢弃了key,高德服务端会直接拦截请求,陷入了一个尴尬的局面

McpSseClientProperties中配置参数为Map集合对应SseParameters,该实体只有一个属性url,无法配置更多参数;

java 复制代码
	public record SseParameters(String url) {
	}

	/**
	 * Map of named SSE connection configurations.
	 * <p>
	 * The key represents the connection name, and the value contains the SSE parameters
	 * for that connection.
	 */
	private final Map<String, SseParameters> connections = new HashMap<>();

SseWebFluxTransportAutoConfiguration中代码是直接使用url,也没有考虑用户配置路径中存在url情况(或者可配置额外可附加的参数),这块从能力上还是有所欠缺,希望后续会升级优化下

解决问题:

如果能自定义List<NamedClientMcpTransport>会更优雅,但该Bean会自动执行,目前采用一种比较粗暴的做法,拦截url的请求,在包含sse的url后追加参数key。

java 复制代码
@Configuration
public class WebClientConfig {
    @Bean
    public WebClient.Builder webClientBuilder() {
        return WebClient.builder().filter((request, next) -> {
            // 拦截 SSE 请求
            if (request.url().toString().contains("/sse")) {
                // 在原始 URL 后追加 key 参数
                URI newUri = UriComponentsBuilder.fromUri(request.url())
                        .queryParam("key", "您在高德官网上申请的key") // 自动处理编码
                        .build()
                        .toUri();

                // 保留原始请求头(关键!)
                ClientRequest mutatedRequest = ClientRequest.from(request)
                        .url(newUri)
                        .build();
                return next.exchange(mutatedRequest);
            }
            return next.exchange(request);
        });
    }
}

测试结果: 提问"明天要去杭州西湖出差,有什么推荐的性价比较高的酒店吗?"

java 复制代码
 @Bean
    public CommandLineRunner predefinedQuestions(ChatClient.Builder chatClientBuilder, ToolCallbackProvider tools, ConfigurableApplicationContext context) {

        String userInput = "明天要去杭州西湖出差,有什么推荐的性价比较高的酒店吗?";


        return args -> {

            var chatClient = chatClientBuilder.defaultTools(tools).build();

            System.out.println("\n>>> QUESTION: " + userInput);
            System.out.println("\n>>> ASSISTANT: " + chatClient.prompt(userInput).call().content());

            context.close();
        };
    }

QUESTION: 明天要去杭州西湖出差,有什么推荐的性价比较高的酒店吗?
ASSISTANT: 以下是一些杭州西湖附近性价比较高的酒店推荐:

  1. 杭州白金汉爵大酒店

    地址:珊瑚沙东路9号

  2. 维也纳国际酒店(杭州西溪灵隐店)

    地址:合贸路33号1号楼(古墩路地铁站C口步行400米)

  3. 锦辰酒店

    地址:文二路268号(文二路学院路交叉口)

  4. 汉庭酒店(杭州西湖保俶路店)

    地址:宝石二路2号

  5. 桔子酒店(杭州西湖区宋城店)

    地址:转塘街道之江长九中心1号楼

  6. 杭州文华景澜大酒店

    地址:文二路38号

  7. 如家商旅酒店(杭州西湖湖滨断桥店)

    地址:保俶路27号

  8. 全季酒店(杭州文二西路西溪湿地店)

    地址:文二西路710号

  9. 格雷斯精选酒店(杭州西溪店)

    地址:留下街道荆山岭路2号汇峰国际B座

  10. 湖滨四季酒店(杭州保俶路下宁桥地铁站店)

    地址:文二路125号4号楼(下宁桥地铁站B口步行210米)

如果需要更详细的信息(如价格、评分等),可以告诉我,我可以进一步查询!

四、资源暴露

MCP除了支持Server暴露工具能力(Tools)外,还支持Resources、Prompts、Sampling、Roots,目前就Tools被广泛使用,本小结只演示Resources的能力

功能模块 技术定位 交互方向 关键特性
Tools 服务端暴露的原子化API能力 LLM ⇄ 外部系统 • 安全沙箱执行 • 自动OpenAPI描述生成 • 多步骤事务支持
Resources 结构化数据供给层 客户端/LLM ← 服务端 • 动态权限控制 • 版本化数据快照 • 多模态内容支持(文本/图像/音频)
Prompts 预置提示工程模板 服务端 → LLM • 参数化模板引擎 • A/B测试版本控制 • 跨模型兼容性适配
Sampling 服务端驱动的智能请求编排 服务端 → 客户端 → LLM • 动态参数调控(temperature/top_p) • 响应过滤 • 计费计量
Roots 分布式资源寻址系统 客户端 → 服务端 • 智能缓存路由 • 故障转移配置 • 混合云资源定位

4.1 定义Resource

静态资源

java 复制代码
   // 静态资源:产品手册
    @Bean
    public List<McpServerFeatures.SyncResourceSpecification> staticResources() {
        // 1. 创建资源定义(符合最新API)
        McpSchema.Resource productManual = new McpSchema.Resource("/resources/product-manual",  // URI
                "产品功能约束",         // 名称
                "详细描述天气查询工具的限制", // 描述
                "text/html",                  // MIME类型
                new McpSchema.Annotations(List.of(McpSchema.Role.USER, McpSchema.Role.ASSISTANT), // 允许访问的角色
                        0.8                            // 优先级(0.0-1.0)
                ));

        // 2. 定义资源内容处理器
        McpServerFeatures.SyncResourceSpecification spec = new McpServerFeatures.SyncResourceSpecification(productManual, (exchange, request) -> {
            try {
                String htmlContent = """
                        <html>
                            <body>
                                <h1>产品功能约束</h1>
                               * 每个用户每天最多调用10次,超过10次则会收费
                               * 目前所有的数据都是mock的,可靠度为0
                            </body>
                        </html>
                        """;

                return new McpSchema.ReadResourceResult(List.of(new McpSchema.TextResourceContents(request.uri(), "text/html", htmlContent)));
            } catch (Exception e) {
                throw new RuntimeException("Failed to load resource", e);
            }
        });

        return List.of(spec);
    }

动态资源

java 复制代码
    // 动态资源:实时系统指标
    @Bean
    public List<McpServerFeatures.SyncResourceSpecification> dynamicResources(ObjectMapper objectMapper) {

        McpSchema.Resource systemMetrics = new McpSchema.Resource("/monitoring/system-metrics", "System Metrics", "Real-time CPU/Memory/Disk metrics", "application/json", new McpSchema.Annotations(List.of(McpSchema.Role.USER), 0.9                  // 高优先级
        ));

        McpServerFeatures.SyncResourceSpecification spec = new McpServerFeatures.SyncResourceSpecification(systemMetrics, (exchange, request) -> {
            try {
                Map<String, Object> metrics = Map.of("cpu", Map.of("usage", Math.random() * 100, "cores", Runtime.getRuntime().availableProcessors()), "memory", Map.of("free", Runtime.getRuntime().freeMemory(), "max", Runtime.getRuntime().maxMemory()), "timestamp", System.currentTimeMillis());

                return new McpSchema.ReadResourceResult(List.of(new McpSchema.TextResourceContents(request.uri(), "application/json", objectMapper.writeValueAsString(metrics))));
            } catch (Exception e) {
                throw new RuntimeException("Metrics collection failed", e);
            }
        });

        return List.of(spec);
    }

4.2 访问Resource

基于Cline测试

查看到增加两个Resources

查询静态资源信息:

查询动态资源:

五、通信浅析

前面的部分已完成Server与Client的自定义开发,接下来一起看下Server与Client的通信,以及如何实现类似Cline的宿主机。

5.1 Charles抓包

Charles配置

通过Charles进行本地网络抓包, 下载安装完成后进行证书的安装、端口设置、启动代理

Host修改

以Cline+Webmvc的组合做示例,配置的url为http://localhost:8080/sse ,要抓包localhost需要修改host内容

java 复制代码
vim /etc/hosts
localhost http://localhost.charlesproxy.com/

cline_mcp_settings.json中修改url

java 复制代码
   "test-weather2": {
      "url": "http://localhost.charlesproxy.com:8080/sse",
      "disabled": false,
      "autoApprove": []
    }

抓包

点击server右侧的小圈圈(Restart Server)重新发起请求,Charles中已成功抓到请求

http://localhost.charlesproxy.com:8080/sse** **

  • 获取mcp请求的sessionId信息,Mime_Type=text/event-stream

http://localhost.charlesproxy.com:8080/mcp?sessionId=xxx

  • 包含mcp的请求一共五个,分别是initializenotifications/initializedtools/listresources/listresources/templates/list , 其中initialize会发送客户端相关信息,生产环境下可基于此做权限、流量等管控
json 复制代码
{
  "method": "initialize",
  "params": {
    "protocolVersion": "2024-11-05",
    "capabilities": {},
    "clientInfo": {
      "name": "Cline",
      "version": "3.13.2"
    }
  },
  "jsonrpc": "2.0",
  "id": 0
}
  • tools/list的请求为例,发现请求是遵从JSON-RPC 2.0 协议,但细心的同学发现请求完成后没有直观的看到工具列表信息,是因为整个通信是基于SSE的,要回到sse那个请求下才看到服务端推送的数据

至此,可以理解为基于sse端口获取Seesion信息并建立了服务端到客户端单向实时通信能力,在mcp端口遵守JSON-RPC 2.0协议,基于不同的method触发服务端向客户端信息的推送

5.2 inspector工具

除了Charles抓包以外,还有另外一种方式就是利用官方inspector工具。

浏览器请求

打开命令行终端执行安装与启动 <font style="color:#000000;">npx @modelcontextprotocol/inspector,打开http://127.0.0.1:6274访问,并利用浏览器的检查工具看网络请求

查看EventStream也可以看到类似信息

json 复制代码
{
    "jsonrpc": "2.0",
    "id": 1,
    "result":
    {
        "tools":
        [
            {
                "name": "getAlerts",
                "description": "获取指定地域的天气预警",
                "inputSchema":
                {
                    "type": "object",
                    "properties":
                    {
                        "state":
                        {
                            "type": "string"
                        }
                    },
                    "required":
                    [
                        "state"
                    ],
                    "additionalProperties": false
                }
            },
            {
                "name": "getWeatherForecastByLocation",
                "description": "获取指定经纬度地点的天气预报",
                "inputSchema":
                {
                    "type": "object",
                    "properties":
                    {
                        "latitude":
                        {
                            "type": "number",
                            "format": "double"
                        },
                        "longitude":
                        {
                            "type": "number",
                            "format": "double"
                        }
                    },
                    "required":
                    [
                        "latitude",
                        "longitude"
                    ],
                    "additionalProperties": false
                }
            }
        ]
    }
}

工具测试

<font style="color:#000000;">inspector提供可视化的操作后天,平时在自定义MCP时可快速发起测试验证

5.3 协议实现

5.3.1 协议内容

https://github.com/modelcontextprotocol/modelcontextprotocol 可以看到通信协议的schema,看到两个2024-11-052025-03-26 版本,协议

https://modelcontextprotocol.io/specification/2025-03-26/basic/transports 这介绍了新版本的特性

重点关注新版本中对服务端与客户端的通信协议做了升级,使用更灵活的Streamable HTTP传输协议替代原有的HTTP+SSE传输方案,差异对比如下(新版本优势明显):

对比维度 旧版 HTTP+SSE (2024-11-05) 新版 Streamable HTTP (2025-03-26) 新版优势
协议名称 HTTP+SSE Streamable HTTP 统一命名,明确功能扩展
通信模式 单向(服务器 ->客户端推送) 双向(客户端<->服务器全交互) 支持复杂交互场景
端点设计 独立 SSE 端点 单一 /mcp 端点(兼容 POST/GET) 简化部署与维护
客户端请求方式 仅 GET 请求启动 SSE 流 新增 POST 请求:携带 JSON-RPC 请求体 支持主动发起带参请求
服务器响应形式 仅能推送通知 可返回: • application/json (单次) • text/event-stream (持续流) 灵活适配不同场景
消息批处理 不支持 支持 JSON-RPC 批量消息(数组格式) 提升传输效率
多路复用 单一连接 多流并行:客户端可维护多个独立 SSE 流 避免消息阻塞
断线恢复 无规范 强制 Last-Event-ID + 全局唯一事件 ID 保障消息可靠性
会话管理 无状态 Session ID 机制: • 初始化分配 • 显式终止(DELETE) 支持有状态交互
安全增强 无强制要求 必须 : • OAuth 2.0/JWT • 验证 Origin 头 • 本地绑定 localhost 防御劫持与越权
兼容性策略 自动降级: • 新版客户端可回退旧版协议 平滑迁移过渡

由于本篇文章的示例都是基于2024-11-05版本协议实现的SDK,后续的解读也是基于此版本的(方法掌握才是根本)

ServerResult约束服务端的返回数据类型,以ListToolsResult(查询工具列表)为例子

json 复制代码
     "ServerResult": {
            "anyOf": [
                {
                    "$ref": "#/definitions/Result"
                },
                {
                    "$ref": "#/definitions/InitializeResult"
                },
                {
                    "$ref": "#/definitions/ListResourcesResult"
                },
                {
                    "$ref": "#/definitions/ListResourceTemplatesResult"
                },
                {
                    "$ref": "#/definitions/ReadResourceResult"
                },
                {
                    "$ref": "#/definitions/ListPromptsResult"
                },
                {
                    "$ref": "#/definitions/GetPromptResult"
                },
                {
                    "$ref": "#/definitions/ListToolsResult"
                },
                {
                    "$ref": "#/definitions/CallToolResult"
                },
                {
                    "$ref": "#/definitions/CompleteResult"
                }
            ]
        },

ListToolsResult要求返回tools数组,类型为Tool

json 复制代码
"ListToolsResult": {
            "description": "The server's response to a tools/list request from the client.",
            "properties": {
                "_meta": {
                    "additionalProperties": {},
                    "description": "This result property is reserved by the protocol to allow clients and servers to attach additional metadata to their responses.",
                    "type": "object"
                },
                "nextCursor": {
                    "description": "An opaque token representing the pagination position after the last returned result.\nIf present, there may be more results available.",
                    "type": "string"
                },
                "tools": {
                    "items": {
                        "$ref": "#/definitions/Tool"
                    },
                    "type": "array"
                }
            },
            "required": [
                "tools"
            ],
            "type": "object"
        }

Tool 需要具备 namedescriptioninputSchema 三个属性, 其中inputSchema是一个调用工具参数的JSON字符串

json 复制代码
    "Tool": {
            "description": "Definition for a tool the client can call.",
            "properties": {
                "description": {
                    "description": "A human-readable description of the tool.",
                    "type": "string"
                },
                "inputSchema": {
                    "description": "A JSON Schema object defining the expected parameters for the tool.",
                    "properties": {
                        "properties": {
                            "additionalProperties": {
                                "additionalProperties": true,
                                "properties": {},
                                "type": "object"
                            },
                            "type": "object"
                        },
                        "required": {
                            "items": {
                                "type": "string"
                            },
                            "type": "array"
                        },
                        "type": {
                            "const": "object",
                            "type": "string"
                        }
                    },
                    "required": [
                        "type"
                    ],
                    "type": "object"
                },
                "name": {
                    "description": "The name of the tool.",
                    "type": "string"
                }
            },
            "required": [
                "inputSchema",
                "name"
            ],
            "type": "object"
        },

5.3.2 Java实现

在Java SDKMcpSchema类基于schema完成协议的实现

WebMvcSseServerTransportProvider

WebMvcSseServerTransportProvider的构造方法中实例化<font style="color:rgba(0, 0, 0, 0.9);">org.springframework.web.servlet.function.RouterFunction示例(this.routerFunction),通过<font style="color:rgba(0, 0, 0, 0.9);">handleSseConnection处理GET请求,<font style="color:rgba(0, 0, 0, 0.9);">handleMessage处理POST请求

java 复制代码
public WebMvcSseServerTransportProvider(ObjectMapper objectMapper, String baseUrl, String messageEndpoint,
                                        String sseEndpoint) {
    Assert.notNull(objectMapper, "ObjectMapper must not be null");
    Assert.notNull(baseUrl, "Message base URL must not be null");
    Assert.notNull(messageEndpoint, "Message endpoint must not be null");
    Assert.notNull(sseEndpoint, "SSE endpoint must not be null");

    this.objectMapper = objectMapper;
    this.baseUrl = baseUrl;
    this.messageEndpoint = messageEndpoint;
    this.sseEndpoint = sseEndpoint;
    this.routerFunction = RouterFunctions.route()
    .GET(this.sseEndpoint, this::handleSseConnection)
    .POST(this.messageEndpoint, this::handleMessage)
    .build();
}

handleSseConnection

handleSseConnection通过函数式ServerResponse.sse构建响应,生成session并管理生命周期,在全局的map中存储McpServerSession示例

java 复制代码
private ServerResponse handleSseConnection(ServerRequest request) {
		if (this.isClosing) {
			return ServerResponse.status(HttpStatus.SERVICE_UNAVAILABLE).body("Server is shutting down");
		}

		String sessionId = UUID.randomUUID().toString();
		logger.debug("Creating new SSE connection for session: {}", sessionId);

		// Send initial endpoint event
		try {
			return ServerResponse.sse(sseBuilder -> {
				sseBuilder.onComplete(() -> {
					logger.debug("SSE connection completed for session: {}", sessionId);
					sessions.remove(sessionId);
				});
				sseBuilder.onTimeout(() -> {
					logger.debug("SSE connection timed out for session: {}", sessionId);
					sessions.remove(sessionId);
				});

				WebMvcMcpSessionTransport sessionTransport = new WebMvcMcpSessionTransport(sessionId, sseBuilder);
				McpServerSession session = sessionFactory.create(sessionTransport);
				this.sessions.put(sessionId, session);

				try {
					sseBuilder.id(sessionId)
						.event(ENDPOINT_EVENT_TYPE)
						.data(this.baseUrl + this.messageEndpoint + "?sessionId=" + sessionId);
				}
				catch (Exception e) {
					logger.error("Failed to send initial endpoint event: {}", e.getMessage());
					sseBuilder.error(e);
				}
			}, Duration.ZERO);
		}
		catch (Exception e) {
			logger.error("Failed to send initial endpoint event to session {}: {}", sessionId, e.getMessage());
			sessions.remove(sessionId);
			return ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
		}
	}

handleMessage

handleMessage处理客户端post请求,先解析出sessionId,基于sessionId获取已缓存的McpServerSession对象,由于请求是遵循JSON-RPC规范的,可以得到McpSchema.JSONRPCMessage 后调用handle触发服务端向客户端推送消息

java 复制代码
private ServerResponse handleMessage(ServerRequest request) {
		if (this.isClosing) {
			return ServerResponse.status(HttpStatus.SERVICE_UNAVAILABLE).body("Server is shutting down");
		}

		if (!request.param("sessionId").isPresent()) {
			return ServerResponse.badRequest().body(new McpError("Session ID missing in message endpoint"));
		}

		String sessionId = request.param("sessionId").get();
		McpServerSession session = sessions.get(sessionId);

		if (session == null) {
			return ServerResponse.status(HttpStatus.NOT_FOUND).body(new McpError("Session not found: " + sessionId));
		}

		try {
			String body = request.body(String.class);
			McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(objectMapper, body);

			// Process the message through the session's handle method
			session.handle(message).block(); // Block for WebMVC compatibility

			return ServerResponse.ok().build();
		}
		catch (IllegalArgumentException | IOException e) {
			logger.error("Failed to deserialize message: {}", e.getMessage());
			return ServerResponse.badRequest().body(new McpError("Invalid message format"));
		}
		catch (Exception e) {
			logger.error("Error handling message: {}", e.getMessage());
			return ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new McpError(e.getMessage()));
		}
	}

McpServerSession#handle

message instanceof McpSchema.JSONRPCRequest request消息类型是JSONRPCRequest时执行handleIncomingRequest

java 复制代码
public Mono<Void> handle(McpSchema.JSONRPCMessage message) {
		return Mono.defer(() -> {
			// TODO handle errors for communication to without initialization happening
			// first
			if (message instanceof McpSchema.JSONRPCResponse response) {
				logger.debug("Received Response: {}", response);
				var sink = pendingResponses.remove(response.id());
				if (sink == null) {
					logger.warn("Unexpected response for unknown id {}", response.id());
				}
				else {
					sink.success(response);
				}
				return Mono.empty();
			}
			else if (message instanceof McpSchema.JSONRPCRequest request) {
				logger.debug("Received request: {}", request);
				return handleIncomingRequest(request).onErrorResume(error -> {
					var errorResponse = new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), null,
							new McpSchema.JSONRPCResponse.JSONRPCError(McpSchema.ErrorCodes.INTERNAL_ERROR,
									error.getMessage(), null));
					// TODO: Should the error go to SSE or back as POST return?
					return this.transport.sendMessage(errorResponse).then(Mono.empty());
				}).flatMap(this.transport::sendMessage);
			}
			else if (message instanceof McpSchema.JSONRPCNotification notification) {
				// TODO handle errors for communication to without initialization
				// happening first
				logger.debug("Received notification: {}", notification);
				// TODO: in case of error, should the POST request be signalled?
				return handleIncomingNotification(notification)
					.doOnError(error -> logger.error("Error handling notification: {}", error.getMessage()));
			}
			else {
				logger.warn("Received unknown message type: {}", message);
				return Mono.empty();
			}
		});
	}

handleIncomingRequest

handleIncomingRequest通过匹配不同请求method找对应处理逻辑,在创建McpServerSession就初始化好requestHandlers

java 复制代码
private Mono<McpSchema.JSONRPCResponse> handleIncomingRequest(McpSchema.JSONRPCRequest request) {
		return Mono.defer(() -> {
			Mono<?> resultMono;
			if (McpSchema.METHOD_INITIALIZE.equals(request.method())) {
				// TODO handle situation where already initialized!
				McpSchema.InitializeRequest initializeRequest = transport.unmarshalFrom(request.params(),
						new TypeReference<McpSchema.InitializeRequest>() {
						});

				this.state.lazySet(STATE_INITIALIZING);
				this.init(initializeRequest.capabilities(), initializeRequest.clientInfo());
				resultMono = this.initRequestHandler.handle(initializeRequest);
			}
			else {
				// TODO handle errors for communication to this session without
				// initialization happening first
				var handler = this.requestHandlers.get(request.method());
				if (handler == null) {
					MethodNotFoundError error = getMethodNotFoundError(request.method());
					return Mono.just(new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), null,
							new McpSchema.JSONRPCResponse.JSONRPCError(McpSchema.ErrorCodes.METHOD_NOT_FOUND,
									error.message(), error.data())));
				}

				resultMono = this.exchangeSink.asMono().flatMap(exchange -> handler.handle(exchange, request.params()));
			}
			return resultMono
				.map(result -> new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), result, null))
				.onErrorResume(error -> Mono.just(new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(),
						null, new McpSchema.JSONRPCResponse.JSONRPCError(McpSchema.ErrorCodes.INTERNAL_ERROR,
								error.getMessage(), null)))); // TODO: add error message
																// through the data field
		});
	}

以查询工具列表为例会执行McpAsyncServer.AsyncServerImpl#toolsListRequestHandler,获取到tools后推送给客户端

java 复制代码
	private McpServerSession.RequestHandler<McpSchema.ListToolsResult> toolsListRequestHandler() {
			return (exchange, params) -> {
				List<Tool> tools = this.tools.stream().map(McpServerFeatures.AsyncToolSpecification::tool).toList();

				return Mono.just(new McpSchema.ListToolsResult(tools, null));
			};
		}

至此,一次交互过程完成。

5.4 Cline与大模型交互

发起请求: 杭州天气有预警吗?

在Charles抓到**api.deepseek.com**请求(是因为Cline配置的大模型为DeepSeek)

在请求中(temperature=0),messages存在三条消息,

  • role=system:设置系统角色,提示词内容很长,直接以文件保存在cline_prompt.txt中
  • role=user[0] : 用户输入的任务信息
  • role=user[1]:以格式约定的调用方的环境以及时间信息

cline_prompt.txt

提示词解读:

    1. 角色定义
    • 名称:Cline
    • 身份:高级软件工程师
    • 能力:精通多种编程语言、框架、设计模式和最佳实践
    1. 工具系统
    • 工具调用格式:XML风格标签(value)
    • 核心工具:
      • 文件操作:read_file、write_to_file、replace_in_file
      • 代码分析:search_files、list_code_definition_names
      • 命令执行:execute_command(需用户批准危险操作)
      • MCP集成:use_mcp_tool、access_mcp_resource(连接外部服务)
      • 交互工具:ask_followup_question(仅必要时使用)
      • 任务管理:new_task(保存上下文)、attempt_completion(提交最终结果)
    1. 工作模式
    • ACT模式(执行模式):
      • 使用工具完成任务
      • 禁止使用plan_mode_respond
    • PLAN模式(规划模式):
      • 仅能使用plan_mode_respond进行讨论
      • 用于需求分析、方案设计,用户批准后切换回ACT模式执行
    1. 文件编辑策略
    • write_to_file:适用于创建新文件或完全重写
    • replace_in_file:适用于局部修改(精确匹配,支持多段替换)
    • 注意事项:
      • 修改后需考虑IDE自动格式化影响
      • 替换内容必须完整,不能截断
    1. 安全与规范
    • 工作目录限制:固定在/Users/celen/Desktop,不能cd切换
    • 命令执行安全:
      • 危险操作(如删除、安装)需用户明确批准(requires_approval=true)
      • 命令需适配用户系统(macOS/zsh)
    • 沟通风格:
      • 禁止无意义开场白(如"Great"、"Certainly")
      • 必须直接、技术性表达
      • 禁止开放式提问(除非必要)
    1. 任务执行流程
    • 分析任务:结合environment_details(自动提供的文件结构)
    • 选择工具:在中评估最优工具 逐步执行:一次仅用一个工具,等待用户确认后再继续 提交结果:用attempt_completion交付最终成果(不能含未完成任务)
    1. MCP(Model Context Protocol)扩展 支持连接外部服务(如Git、GitHub、天气API等) 每个MCP服务提供特定工具(如git_commit、get_weather_forecast)
    1. 关键原则
    • 一次一步:工具必须按顺序执行,等待用户反馈
    • 最小交互:尽量用工具获取信息,而非提问
    • 结果导向:任务完成后必须用attempt_completion明确结束
    • 禁止:
      • 假设工具执行成功(必须等用户确认)
      • 修改文件时截断或不完整替换
      • 无意义的客套话

借鉴经验:

上述提示词的解读来自大模型的理解,仅供参考,但特别值得借鉴的地方在于它建立了一个高度结构化、安全可控的AI操作体系,同时保持了足够的灵活性来处理各种软件开发任务。它通过明确的规范、严谨的工作流程和丰富的工具集,使AI能够在软件工程领域进行专业、可靠的操作

  • 明确的角色定义:清晰界定了AI的专业领域和能力范围(软件工程相关),设定了专业身份(高级软件工程师)
  • 工具化操作体系:采用XML格式的工具调用规范,结构清晰,每个工具都有详细的参数说明和使用示例,工具分类明确(文件操作、命令执行、代码分析等)
  • 严谨的工作流程:强调迭代式工作方法,一次只使用一个工具
  • 双模式设计:ACT模式(执行模式)用于实际操作,PLAN模式(规划模式)用于方案设计,两种模式有明确的切换规则和使用限制
  • 文件编辑规范:区分write_to_file和replace_in_file的使用场景,提供详细的文件修改指南和最佳实践
  • 安全控制机制:危险操作需要用户明确批准(requires_approval参数),限制工作目录,防止越权操作,命令执行前需考虑系统环境
  • 沟通规范:禁止无意义的寒暄用语,要求直接、技术性的表达方式,限制不必要的问题询问
  • 结果交付规范:使用attempt_completion工具明确标记任务完成,禁止开放式结尾,要求提供确定性结果

不足之处是每次发起请求都会将所有的工具scheme传输给大模型,导致提示词内容过长

代码执行:

按照Cline类似的模式,用代码构造请求

java 复制代码
  @GetMapping(value = "/cline", produces = "text/html;charset=UTF-8")
    public Flux<String> cline(@RequestParam(value = "input", defaultValue = "讲一个笑话") String input) {


        SystemMessage systemMessage = new SystemMessage("""
            You are Cline, a highly skilled software engineer
             ... 省略
            
            """);
        UserMessage userMessage = new UserMessage("""
                <task>\\n杭州天气有预警吗?\\n</task>
                """);

        UserMessage env =new  UserMessage("""
                <environment_details>\\n# VSCode Visible Files\\n../Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json\\n\\n# VSCode Open Tabs\\n../Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json\\n\\n# Current Time\\n2025/4/28 下午10:23:28 (Asia/Shanghai, UTC+8:00)\\n\\n# Current Working Directory (/Users/celen/Desktop) Files\\n(Desktop files not shown automatically. Use list_files to explore if needed.)\\n\\n# Context Window Usage\\n0 / 64K tokens used (0%)\\n\\n# Current Mode\\nACT MODE\\n</environment_details>
                """);

        Prompt p = new Prompt(List.of(systemMessage,userMessage,env));

        return chatClient.prompt(p).stream().content();
    }

得到结果:

复制代码
1. The user is asking about weather alerts in Hangzhou, China.

2. Looking at the connected MCP servers, there is a 'test-weather2' server that provides weather-related tools.

3. The server has a 'getAlerts' tool that can get weather alerts for a specified region.

4. The tool requires a 'state' parameter - for Hangzhou, we should use 'Zhejiang' as the province/state.

5. All required parameters are available or can be reasonably inferred.

<use_mcp_tool>

复制代码
<server_name>test-weather2</server_name>

<tool_name>getAlerts</tool_name>

<arguments>

    {

    "state": "Zhejiang"

    }

</arguments>

</use_mcp_tool>

Response file saved.

得到的内容和Cline返回几乎一致(可以对比下图); 返回的结果中提示需要使用mcp工具;如果再开发一个界面来授权调用工具,把工具执行结果再发给大模型,就完成类似效果

点击确认使用工具后,会触发localhost工具的调用,然后把工具执行结果返回给大模型

结语:本来没有路,走的人多了就有了

MCP或许还不是行业标准,但已展现出巨大价值:

  • 拆巴别塔:用统一协议终结AI服务的"方言割据"
  • 修高速路:让数据、工具、提示词在标准化通道上飞驰
  • 建服务区:开发者再不用自己"铺路搭桥",专注业务创新

正如USB-C统一了充电接口,MCP正在成为AI服务的"万能插头"。很快,或许只需一句:"嘿~MCP,帮我调天气AI写首诗,再用Stable Diffusion配个图!"

相关推荐
benpaodeDD25 分钟前
双列集合——map集合和三种遍历方式
java
CHNMSCS1 小时前
PyTorch_张量基本运算
人工智能·pytorch·python
时而支楞时而摆烂的小刘1 小时前
CUDA、pytorch、配置环境教程合集
人工智能·pytorch·python
试着1 小时前
【AI面试准备】元宇宙测试:AI+低代码构建虚拟场景压力测试
人工智能·低代码·面试
Q_Boom1 小时前
前端跨域问题怎么在后端解决
java·前端·后端·spring
Frankabcdefgh1 小时前
颠覆者DeepSeek:从技术解析到实战指南——开源大模型如何重塑AI生态
人工智能·科技·深度学习·自然语言处理·职场和发展
搬砖工程师Cola1 小时前
<Revit二次开发> 通过一组模型线构成墙面,并生成墙。Create(Document, IList.Curve., Boolean)
java·前端·javascript
等什么君!1 小时前
学习spring boot-拦截器Interceptor,过滤器Filter
java·spring boot·学习
caihuayuan42 小时前
Linux环境部署iview-admin项目
java·大数据·sql·spring·课程设计
浪前2 小时前
【项目篇之统一内存操作】仿照RabbitMQ模拟实现消息队列
java·分布式·rabbitmq·ruby