今天学习下Spring AI 2.0.0:
第一个Spring AI Hello World
结构化输出
Tool Calling
RAG
Advisor
环境要求
JDK : temurin-lts-jdk (25.0.3-9.0.LTS)
Spring Ai : 2.0.0
SpringBoot : 4.1.0
Gradle : 9.5.1
创建项目 创建正常的Spring项目即可,添加的依赖有:
spring-boot-starter-web
spring-boot-starter-webflux
spring-ai-starter-model-openai
lombok
spring-boot-devtools
junit
spring-ai-bom
我选择的是Gradle,下面就贴一下build.gradle,创建好应该是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 plugins { id 'java' id 'org.springframework.boot' version '4.1.0' id 'io.spring.dependency-management' version '1.1.7' } group = 'cn.net.dev' version = '0.0.1-SNAPSHOT' description = 'springai' java { toolchain { languageVersion = JavaLanguageVersion.of(25 ) } } repositories { mavenCentral() } ext { set ('springAiVersion' , "2.0.0" ) } dependencies { implementation 'org.springframework.boot:spring-boot-starter-webmvc' implementation 'org.springframework.boot:spring-boot-starter-webflux' implementation 'org.springframework.ai:spring-ai-starter-model-openai' compileOnly 'org.projectlombok:lombok' developmentOnly 'org.springframework.boot:spring-boot-devtools' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test' testCompileOnly 'org.projectlombok:lombok' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' testAnnotationProcessor 'org.projectlombok:lombok' } dependencyManagement { imports { mavenBom "org.springframework.ai:spring-ai-bom:${springAiVersion} " } } tasks.named('test' ) { useJUnitPlatform() }
Hello Spring Ai 先来第一个SpringAI程序
application.yml 1 2 3 4 5 6 7 8 9 spring: application: name: springai ai: openai: api-key: ${DEEPSEEK_API_KEY} base-url: https://api.deepseek.com/v1 chat: model: deepseek-v4-flash
即使添加了OpenAI的模块,但其实只要是兼容OpenAI协议的都可以配置上去。
ChatController 创建controller包,在里面新建一个ChatController
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 package cn.net.dev.springai.controller; import org.springframework.ai.chat.client.ChatClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import reactor.core.publisher.Flux; @RestController public class ChatController { private final ChatClient chatClient; public ChatController (ChatClient.Builder builder) { this .chatClient = builder .defaultSystem("你是一个个专业的Java技术顾问,回答问题简洁准确" ) .build(); } @RequestMapping("/chat") public String chat (@RequestParam String message) { return chatClient .prompt() .user(message) .call() .content(); } @GetMapping(value = "/stream", produces = "text/event-stream;charset=UTF-8") public Flux<String> streamChat (@RequestParam String message) { return chatClient.prompt().user(message).stream().content(); } }
对于IDEA,打开运行/调试配置,点击修改选项(alt+M),点击环境变量(alt+E),在添加的环境变量表单里,输入如下内容:
1 DEEPSEEK_API_KEY=你的DeepSeek API KEY
或者创建.env文件,也可以直接设置系统级环境变量。总之,能读取出来即可。
启动项目后,在浏览器输入http://127.0.0.1:8080/chat?message=JVM的内存结构是怎样的?,此时浏览器就会返回如下数据:
1 JVM 内存主要分为以下几个区域: - **程序计数器**:线程私有,记录当前线程执行的字节码行号。 - **虚拟机栈**:线程私有,存储局部变量表、操作数栈、动态链接、方法出口等。每个方法调用对应一个栈帧。 - **本地方法栈**:线程私有,为 Native 方法服务。 - **堆**:线程共享,存放对象实例和数组。是垃圾回收的主要区域,可细分为新生代(Eden、Survivor)和老年代。 - **方法区(元空间)**:线程共享,存储已被加载的类信息、常量、静态变量等。JDK 8 后元空间取代永久代,使用本地内存。 此外,还有**运行时常量池**(属于方法区的一部分)和**直接内存**(NIO 使用的堆外内存)。
同样的方式,请求http://127.0.0.1:8080/stream?message=JVM的内存结构是怎样的?。这样,一个Spring AI的Hello World就完成了。
结构化输出 Spring AI 2.0支持把模型输出映射到Java对象,地产自动处理JSON解析和类型转换。
单对象输出 在ChatController中,继续加入如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 record Product ( String name, List<String> prop, List<String> cons, int score, String recommendation ) { } @GetMapping("/analyze") public Product analyzeProduct (@RequestParam String productName) { return chatClient.prompt() .user("分析产品:" + productName + ",给出优缺点和推荐指数" ) .call() .entity(Product.class); }
然后打开浏览器,输入http://127.0.0.1:8080/analyze?productName=美的热水壶,此时就会输出如下内容跟:
1 { "name" : "美的热水壶" , "prop" : [ "加热速度快" , "自动断电防干烧" , "双层防烫设计" , "大容量适合家庭使用" ] , "cons" : [ "部分型号壶盖密封圈易老化" , "烧水噪音相对较大" ] , "score" : 9 , "recommendation" : "性价比高,适合注重安全和耐用性的家庭用户" }
列表输出 在ChatController中,继续加入如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 record Menu ( String name, String ingredient, String calories ) {} @RequestMapping("/menu") public List<Menu> recommendMenu (@RequestParam String scene) { return chatClient.prompt() .user("为以下人群推荐5种菜谱:" + scene) .call() .entity(new ParameterizedTypeReference <List<Menu>>() { }); }
启动项目后,在浏览器输入:http://127.0.0.1:8080/menu?scene=减脂,此时就会输出如下:
1 [ { "name" : "蒜蓉西兰花炒鸡胸" , "ingredient" : "鸡胸肉、西兰花、蒜、橄榄油" , "calories" : "150 kcal" } , { "name" : "柠檬烤三文鱼配芦笋" , "ingredient" : "三文鱼、芦笋、柠檬、黑胡椒" , "calories" : "200 kcal" } , { "name" : "番茄菠菜豆腐汤" , "ingredient" : "豆腐、菠菜、番茄、洋葱" , "calories" : "180 kcal" } , { "name" : "凉拌黄瓜鸡蛋丝" , "ingredient" : "黄瓜、鸡蛋、醋、盐" , "calories" : "120 kcal" } , { "name" : "双椒炒海鲜" , "ingredient" : "虾仁、鱿鱼、青椒、红椒" , "calories" : "160 kcal" } ]
Tool Calling是让大模型具备执行力的关键。模型判断什么情况下会调用工具,Spring AI负责执行并把结果返回给模型. 下面模拟两个场景,定义两个工具的实际功能: service/WeatherService.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 package cn.net.dev.springai.service; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @Slf4j @Service public class WeatherService { public String getWeather (String cityName) { log.info("大模型调用本工具........" ); return """ 🌍 城市: %s (%s) ☁️ 天气数据: %s """ .formatted("北京" , "中国" , "晴" ); } } service/EmailService.java package cn.net.dev.springai.service; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @Slf4j @Service public class EmailService { public String send (String destEmail, String subject, String body) { log.info("大模型调用发送邮件工具..." ); log.info("Subject " + subject + " body " + body); return "邮件已发送给" + destEmail; } }
定义工具 tools/MyTools.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 package cn.net.dev.springai.tools; import cn.net.dev.springai.service.EmailService; import cn.net.dev.springai.service.WeatherService; import org.springframework.ai.tool.annotation.Tool; import org.springframework.ai.tool.annotation.ToolParam; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class MyTools { @Autowired private WeatherService weatherService; @Autowired private EmailService emailService; @Tool(description = "查询指定城市的实时天气信息") public String getWeather (@ToolParam(description = "城市名称,例如:北京") String cityName) { System.out.println(cityName); ; return weatherService.getWeather(cityName); } @Tool(description = "发送邮件") public String sendMail (@ToolParam(description = "收件人邮箱") String destEmail, @ToolParam(description = "邮件标题") String subject, @ToolParam(description = "邮件内容") String body) { return emailService.send(destEmail, subject, body); } }
注册工具到ChatClient controller/ChatController
1 2 3 4 5 6 7 8 9 10 11 private final ChatClient chatClient; private MyTools tools; public ChatController (ChatClient.Builder builder,MyTools tools) { this .chatClient = builder .defaultSystem("你是一个个专业的Java技术顾问,回答问题简洁准确" ) .build(); this .tools = tools; }
然后新增一个接口
1 2 3 4 5 6 7 8 @RequestMapping("/testTools") public String testTools (@RequestParam String question) { return chatClient .prompt() .user(question) .tools(tools) .call().content(); }
尝试发送:
127.0.0.1:8080/testTools?question=今天北京天气怎么样?,此时,会返回硬编码的天气:
1 2 3 4 北京今天天气晴朗 ☀️,适合外出活动。 大模型调用本工具........
127.0.0.1:8080/testTools?question=给contact@dev.net.cn发送邮件,说明天早晨9点要开产品调研会?,此时返回:
1 2 3 4 5 6 7 8 邮件已成功发送至 **contact@dev.net.cn**。 **邮件内容摘要:** - **主题:** 产品调研会通知 - **正文:** 提醒明天早晨9点准时参加产品调研会 如有需要调整会议时间或补充其他信息,请随时告知。 2026-07-19T14:57:47.028+08:00 INFO 18316 --- [springai] [nio-8080-exec-5] c.net.dev.springai.service.EmailService : 大模型调用发送邮件工具... 2026-07-19T14:57:47.029+08:00 INFO 18316 --- [springai] [nio-8080-exec-5] c.net.dev.springai.service.EmailService : Subject 产品调研会通知 body 您好, 通知您明天(即次日)早晨9点将召开产品调研会,请准时参加。 谢谢!
RAG RAG(检索增强生成),解决大模型因训练数据时效问题,导致出现幻觉,胡乱回答,同时也是解决内部资料能够被大模型读取的场景。
安装向量数据库 就是加了个vector类型的PostgreSQL 17
1 2 3 4 5 6 7 docker run -d \ --name pgvector-db \ -e POSTGRES_USER=postgres \ -e POSTGRES_PASSWORD=postgres \ -e POSTGRES_DB=testdb \ -p 5432:5432 \ pgvector/pgvector:pg17
添加依赖 打开build.gradle,新增依赖:
1 2 3 4 implementation 'org.springframework.boot:spring-boot-starter-jdbc' implementation 'org.springframework.ai:spring-ai-vector-store-advisor' implementation 'org.springframework.ai:spring-ai-starter-vector-store-pgvector' implementation 'org.postgresql:postgresql:42.7.11'
修改application.yml 修改支持embedding 的模型,目前国内支持embedding的有:qwen3.7-text-embedding、BGE-M3、m3e-base。目前我使用的是qwen。
然后加入datasource、vectorstore的配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 spring: application: name: springai datasource: url: jdbc:postgresql://127.0.0.1:5432/testdb username: postgres password: postgres ai: openai: api-key: ${QWEN_API_KEY} chat: model: glm-5.2 base-url: https://ws-xxxxx.cn-beijing.maas.aliyuncs.com/compatible-mode/v1 embedding: model: qwen3.7-text-embedding base-url: https://ws-xxxxxxxx.cn-beijing.maas.aliyuncs.com/compatible-mode/v1 vectorstore: pgvector: initialize-schema: true index-type: HNSW distance-type: cosine_distance dimensions: 1024 schema-name: public table-name: vector_store
创建表(实际上配置中配置了自动创建的)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 CREATE EXTENSION IF NOT EXISTS vector;CREATE TABLE IF NOT EXISTS public.vector_store ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), content TEXT NOT NULL , metadata JSONB, embedding VECTOR(1024 ) ); CREATE INDEX IF NOT EXISTS vector_store_embedding_idx ON public.vector_store USING HNSW (embedding vector_cosine_ops);
RAG入库 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 controller/DocumentIngestionController package cn.net.dev.springai.controller; import org.springframework.ai.document.Document; import org.springframework.ai.transformer.splitter.TokenTextSplitter; import org.springframework.ai.vectorstore.VectorStore; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; @RestController public class DocumentIngestionController { @Autowired private VectorStore vectorStore; @Autowired private TokenTextSplitter tokenTextSplitter; @PostMapping("/load") public String loadDocument () { var doc1 = new Document ("Spring AI 2.0 与 2026年7月31日正式发布,支持Java 25 和虚拟线程" ); var doc2 = new Document ("詹姆斯高斯林最喜欢的语言是Go语言" ); List<Document> splitDocs = tokenTextSplitter.apply(List.of(doc1, doc2)); vectorStore.add(splitDocs); return "%d 个文档已经存入向量数据库pgvector" .formatted(splitDocs.size()); } }
启动项目后,调用该接口,尝试将模拟的文档写入到向量数据库。
1 2 3 POST http://localhost:8080/load
查看SQL中是否存在
1 2 3 4 5 6 7 SELECT id, LEFT (content, 50 ) as content_preview, metadata, embedding FROM vector_store LIMIT 3 ;
RAG问答 接下来就试试,利用RAG知识库去回答问题 controller/RagController
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 package cn.net.dev.springai.controller; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.advisor.vectorstore.QuestionAnswerAdvisor; import org.springframework.ai.vectorstore.SearchRequest; import org.springframework.ai.vectorstore.VectorStore; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class RagController { private final ChatClient chatClient; @Autowired private VectorStore vectorStore; public RagController (final ChatClient.Builder builder) { this .chatClient = builder.build(); } @GetMapping("/ask") public String askRagQuestion (String question) { var qaAdvisor = QuestionAnswerAdvisor.builder(vectorStore) .searchRequest(SearchRequest.builder().topK(3 ).similarityThreshold(0.7 ).build()) .build(); return chatClient.prompt().user(question) .advisors(qaAdvisor) .call() .content(); } }
调用接口,问问题:
1 http://127.0.0.1:8080/ask?question=詹姆斯高斯林最喜欢的语言是什么
按照之前入库的信息,接口会返回:
1 根据提供的信息,詹姆斯高斯林最喜欢的语言是Go语言。
Advisor链 Advisors API 提供了一种灵活且强大的方式,拦截、修改和增强Spring应用中的AI驱动交互。调用带有用户文本的AI模型时,可以添加或补充上下文数据,就像拦截器一样。
创建一个自己的Advisor 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 component/AuditAdvisor.java package cn.net.dev.springai.compoent; import cn.net.dev.springai.repository.AuditLogRepository; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.chat.client.ChatClientRequest; import org.springframework.ai.chat.client.ChatClientResponse; import org.springframework.ai.chat.client.advisor.api.CallAdvisor; import org.springframework.ai.chat.client.advisor.api.CallAdvisorChain; import org.springframework.core.Ordered; import org.springframework.stereotype.Component; import java.util.Objects; import java.util.UUID; @Slf4j @Component public class AuditAdvisor implements CallAdvisor { private final AuditLogRepository auditLog; public AuditAdvisor (AuditLogRepository auditLog) { this .auditLog = auditLog; } @Override public ChatClientResponse adviseCall (ChatClientRequest chatClientRequest, CallAdvisorChain callAdvisorChain) { log.info("====== AuditAdvisor ======" ); String requestId = UUID.randomUUID().toString(); auditLog.logRequest(requestId, chatClientRequest.prompt().toString()); ChatClientResponse chatClientResponse = callAdvisorChain.nextCall(chatClientRequest); if (chatClientResponse.chatResponse() != null ) { auditLog.logResponse(requestId, Objects.requireNonNull(chatClientResponse.chatResponse().getResult()).getOutput().getText()); } return chatClientResponse; } @Override public String getName () { return "AuditAdvisor" ; } @Override public int getOrder () { return Ordered.HIGHEST_PRECEDENCE; } }
注册AuditAdvisor 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 config/ChatClientConfig.java package cn.net.dev.springai.config; import cn.net.dev.springai.compoent.AuditAdvisor; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; import org.springframework.ai.chat.client.advisor.vectorstore.QuestionAnswerAdvisor; import org.springframework.ai.chat.memory.ChatMemory; import org.springframework.ai.chat.memory.InMemoryChatMemoryRepository; import org.springframework.ai.chat.memory.MessageWindowChatMemory; import org.springframework.ai.vectorstore.SearchRequest; import org.springframework.ai.vectorstore.VectorStore; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class ChatClientConfig { @Bean public ChatClient chatClientWithMemory ( ChatClient.Builder builder, VectorStore vectorStore, // 注入自己定义的Advisor AuditAdvisor auditAdvisor ) { ChatMemory chatMemory = MessageWindowChatMemory.builder().maxMessages(20 ).build(); return builder.defaultAdvisors( auditAdvisor, MessageChatMemoryAdvisor.builder(chatMemory).build(), QuestionAnswerAdvisor.builder(vectorStore) .searchRequest(SearchRequest.builder().topK(5 ).build() ).build() ).defaultSystem("我是李大爷,一名专业摸鱼工程师" ).build(); } }
创建一个repositoryrepository/AuditLogRepository.java
1 2 3 4 5 6 package cn.net.dev.springai.repository; public interface AuditLogRepository { void logRequest (String requestId, String content) ; void logResponse (String requestId, String content) ; }
实现类 repository/InMemoryAuditLogRepository.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package cn.net.dev.springai.repository; import org.springframework.stereotype.Component; @Component public class InMemoryAuditLogRepository implements AuditLogRepository { @Override public void logRequest (String requestId, String content) { System.out.println("[REQUEST][" + requestId + "] " + content); } @Override public void logResponse (String requestId, String content) { System.out.println("[RESPONSE][" + requestId + "] " + content); } }
接入Controller 改一下之前的ChatController就行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 package cn.net.dev.springai.controller; import cn.net.dev.springai.tools.MyTools; import jakarta.servlet.ServletRequest; import jakarta.servlet.http.HttpServletRequest; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.memory.ChatMemory; import org.springframework.core.ParameterizedTypeReference; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import reactor.core.publisher.Flux; import java.util.List; @RestController public class ChatController { private final ChatClient chatClient; private final ChatClient memoryChatClient; private MyTools tools; public ChatController (ChatClient.Builder builder, MyTools tools, ChatClient memoryChatClient) { this .chatClient = builder .defaultSystem("你是一个个专业的Java技术顾问,回答问题简洁准确" ) .build(); this .tools = tools; this .memoryChatClient = memoryChatClient; } @GetMapping("/chat") public String chat (@RequestParam String message, HttpServletRequest request) { String sessionId = request.getSession().getId(); return memoryChatClient.prompt().user(message) .advisors(a -> a.param( ChatMemory.CONVERSATION_ID, sessionId )) .call().content(); } @GetMapping(value = "/stream", produces = "text/event-stream;charset=UTF-8") public Flux<String> streamChat (@RequestParam String message) { return chatClient.prompt().user(message).stream().content(); } record Product ( String name, List<String> prop, List<String> cons, int score, String recommendation ) { } @GetMapping("/analyze") public Product analyzeProduct (@RequestParam String productName) { return chatClient.prompt() .user("分析产品:" + productName + ",给出优缺点和推荐指数" ) .call() .entity(Product.class); } record Menu ( String name, String ingredient, String calories ) { } @RequestMapping("/menu") public List<Menu> recommendMenu (@RequestParam String scene) { return chatClient.prompt() .user("为以下人群推荐5种菜谱:" + scene) .call() .entity(new ParameterizedTypeReference <List<Menu>>() { }); } @RequestMapping("/testTools") public String testTools (@RequestParam String question) { return chatClient .prompt() .user(question) .tools(tools) .call().content(); } }
调配用接口:http://127.0.0.1:8080/chat?message=你是谁?,接口返回:
1 我是李大爷,一名专业摸鱼工程师。不过,根据提供的上下文信息,我无法回答“你是谁”这个问题,因为上下文中并未包含关于我身份的相关内容,仅提供了关于詹姆斯·高斯林和Spring AI 2.0的信息。
此时,控制台会打印:
1 2 3 4 2026-07-19T19:03:38.908+08:00 INFO 21532 --- [springai] [nio-8080-exec-1] c.n.dev.springai.compoent.AuditAdvisor : ====== AuditAdvisor ====== [REQUEST][6689000b-85f0-41ab-a34f-9b82cdef2681] Prompt{messages=[SystemMessage{textContent='我是李大爷,一名专业摸鱼工程师' , messageType=SYSTEM, metadata={messageType=SYSTEM}}, UserMessage{content='你是谁?' , metadata={messageType=USER}, messageType=USER}], modelOptions=org.springframework.ai.openai.OpenAiChatOptions@3571c333} [RESPONSE][6689000b-85f0-41ab-a34f-9b82cdef2681] 我是李大爷,一名专业摸鱼工程师。不过,根据提供的上下文信息,我无法回答“你是谁”这个问题,因为上下文中并未包含关于我身份的相关内容,仅提供了关于詹姆斯·高斯林和Spring AI 2.0的信息。 已与地址为 '' 127.0.0.1:62287',传输: ' 套接字'' 的目标虚拟机断开连接