尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
玩转 Spring AI 第五篇:SSE 流式推送与上云部署——教程工程和生产工程差的那几段
玩转 Spring AI 第五篇SSE 流式推送与上云部署——教程工程和生产工程差的那几段前四篇把 ChatClient、向量库、自建搜索、MCP 工具分别拆完了。这一篇收尾收在最容易 Demo 一切正常、上线当场翻车 的那一段流式输出怎么推到浏览器以及整套东西怎么扔上云服务器。笔者把这个工程的部署讲义和代码逐行对过之后最大的感受是教程把能跑的路径讲得很完整但流式链路和部署链路恰好是它两处讲得最薄的地方——一处是把响应式流硬拽回阻塞的实现选择另一处是 Nginx 只教了怎么编译安装、完全没讲反向代理对 SSE 意味着什么。这两处恰恰是你把它当生产模板时最先要重写的部分。这一篇就把它们讲透SseEmitter池子怎么工作、Flux.toStream()的代价在哪、连接治理缺了什么、以及一份部署前必须补的清单。1. 为什么没有直接把Flux返回给浏览器Spring AI 的流式调用拿到的是一个 Reactor 流FluxStringfluxchatClient.prompt(prompt).stream().content();如果只做一次请求换一次回答这行直接当 MVC 返回值就够了。工程里的HelloController就是这么干的——GET /hello/chat/stream/response返回FluxChatResponseGET /hello/chat/stream/str返回FluxString浏览器能看到字一段段出来。但主业务链路没用这条捷径。它的形状是这样的浏览器 GET /sse/connect?userIdu1 ──→ 长连接挂起等服务端推 浏览器 POST /chat/doChat {u1, msg, botMsgId} ──→ 触发模型立刻返回 void └──→ 回答从上面那条长连接里推回来请求与推送走的是两条连接。这就是SSEServer存在的全部理由服务端必须知道该推给谁。// SSEServer.java:23privatestaticfinalMapString,SseEmittersseClientsnewConcurrentHashMap();这个类不是 Spring Bean是纯静态工具类。它的核心方法就三个publicstaticSseEmitterconnect(StringuserId){SseEmittersseEmitternewSseEmitter(0L);// 0L 永不超时sseEmitter.onTimeout(timeoutCallback(userId));sseEmitter.onCompletion(completionCallback(userId));sseEmitter.onError(errorCallback(userId));sseClients.put(userId,sseEmitter);returnsseEmitter;}privatestaticvoidsendEmitterMessage(SseEmittersseEmitter,StringuserId,Stringmessage,SSEMsgTypemsgType){try{SseEmitter.SseEventBuildermsgEventSseEmitter.event().id(userId).data(message).name(msgType.type);// add / finish / message ...sseEmitter.send(msgEvent);}catch(IOExceptione){log.error(SSE异常...{},e.getMessage());remove(userId);// 发送失败即摘除}}new SseEmitter(0L)这个0L是有说法的源码注释也写了默认值是 30 秒超时未完成任务会抛异常。等一次大模型流式回答30 秒的默认超时显然太短所以直接设成永不超时。事件名由一个枚举固定下来这是整套流式协议的全部枚举事件名用途MESSAGEmessage单发的普通消息群发接口用的就是它ADDadd逐 token 追加流式主力FINISHfinish收尾携带完整答案的 JSONCUSTOM_EVENTcustom_event自定义DONEdone自定义源码注释标了某国产模型 v4前端配合的分工在上一篇讲过add负责打字机式追加finish负责用marked.parse()整体重渲染 Markdown。另外还有三个纯演示用的手工触发接口理解它们有助于看清协议GET /sse/sendMessage单条普通消息、GET /sse/sendMessageAdd循环 10 次、每次Thread.sleep(200)推add用来肉眼看打字机效果、GET /sse/sendMessageAll群发。Q1这套 SSE 方案和自己写 WebSocket 有什么区别维度SSE本工程WebSocket协议就是 HTTPContent-Type: text/event-stream独立协议需要 Upgrade 握手方向服务端 → 客户端单向双向客户端上行走普通POST所以才有两条连接的形状同一条连接浏览器原生 APIEventSource自带断线重连要自己实现重连与心跳鉴权只能挂在 URL / Cookie 上本工程挂在 URL 参数握手阶段可带任意头反向代理需要关缓冲、拉长读超时需要Upgrade头透传适合场景推送流式回答、消息通知协同编辑、实时对战、高频双向说白了大模型流式回答是天然的单向推送场景SSE 是更省事的选择代价是上行必须另开一条 POST于是就得维护userId → 连接的映射也才有了本节后面那堆连接治理问题。2.Flux.toStream()整个工程最需要重写的一行流式链路的骨架是这样的ChatServiceImpl.java:104-117三条链路一字不差重复FluxStringstringFluxchatClient.prompt(prompt).stream().content();ListStringliststringFlux.toStream().map(chatResponse-{// ← 这一行StringcontentchatResponse.toString();SSEServer.sendMsg(userId,content,SSEMsgType.ADD);log.info(content: {},content);returncontent;}).collect(Collectors.toList());StringfullContentlist.stream().collect(Collectors.joining());SSEServer.sendMsg(userId,JSONUtil.toJsonStr(newChatResponseEntity(fullContent,botMsgId)),SSEMsgType.FINISH);toStream()的作用是把 Reactor 流适配成 JavaStream代价是这条链路从此变成阻塞的。它会向源订阅请求无限额度并逐块拉取调用线程一路等到大模型把整段答案吐完才返回。这带来几个直接影响POST /chat/doChat ──→ 线程被占住等模型可能几十秒──→ 才返回 200 ↑ 期间模型每吐一个 token就往另一条连接的 SseEmitter 里 send问题具体表现缓解方向触发接口变成一个慢请求doChat名义上是发起对话实际耗时等于整段回答的生成时长前端 axios 的timeout: 60000就是为它留的触发动作应立刻返回任务 id生成与推送放到异步线程线程占用随并发线性增长每个进行中的对话吃掉一个 MVC 工作线程而SseEmitter长连接又各自占住容器异步上下文换mcp-server那种 WebFlux 栈或用有界线程池 队列拿不到取消语义用户关页面、SseEmitter被摘除后toStream()仍在往下拉sendMsg只是静默不发保留Flux在subscribe里推连接断开时dispose()背压被绕过map副作用 collect全量落地Flux的背压形同虚设边收边推、只在finish时聚合异常路径薄全仓库没有ControllerAdvice/ExceptionHandlerSesrXngServiceImpl里失败直接throw new RuntimeException统一异常出口 给前端一个明确的error事件更贴合这套语义的写法其实很短把阻塞桥接换成订阅即可这段是笔者给的通用做法不属于课程内容StringBuilderfullnewStringBuilder();chatClient.prompt(prompt).stream().content().subscribe(token-{full.append(token);SSEServer.sendMsg(userId,token,SSEMsgType.ADD);},err-SSEServer.sendMsg(userId,err.getMessage(),SSEMsgType.CUSTOM_EVENT),()-SSEServer.sendMsg(userId,JSONUtil.toJsonStr(newChatResponseEntity(full.toString(),botMsgId)),SSEMsgType.FINISH));returnResponseEntity.ok().build();// 立刻返回不占线程注意最后一行换成了finish里聚合发送全文——这本来就是前端已经支持的协议add求快、finish求对改造不需要动前端。一个简单理解Flux到SseEmitter之间隔了一次线程模型的翻译翻译方式选了最笨的那一种拉平成同步循环。功能上没错性能模型上错了。3. 连接治理四处在工程之外但生产里必须补SSEServer只有 120 行作为教学工具类是合格的把它原样放到公网下面四条都会变成事故。缺口代码事实生产后果userId无鉴权GET /sse/connect?userIdxxx直接取查询参数SSEController.java:35connect里没有任何校验任何人猜到一个 userId 就能注册同名连接接收别人的回答。而前端 userId 是Math.random().toString(36).substring(2, 12)——10 位随机串可猜测性不低重复连接覆盖旧连接但不关闭sseClients.put(userId, sseEmitter)SSEServer.java:41同一 userId 第二次连接后第一个SseEmitter成为孤儿既不推也不complete()只等容器超时回收。用户刷新页面几次就攒一堆悬挂连接无心跳、无空闲回收全类没有任何定时任务SseEmitter(0L)永不超时反向代理或中间网络设备可能先于应用断开静默连接而服务端还以为连接活着add事件之间模型长时间思考时更容易踩到见 §5单进程内存态无法水平扩展Map是静态字段多实例部署时推add的实例和持有连接的实例可能不是同一台回答直接丢。Redis Pub/Sub 或粘性会话是必选项第四条要和上一篇埋的那个问题连起来看ChatMemory在整个仓库里没有Bean声明也没有spring.ai.chat.memory.*配置只能推断为 Spring AI 1.0.0 自动配置的默认实现未取到一手依据正文不作断言。如果它确实是进程内存实现那么多实例部署下这轮请求打到 A 实例、上轮记忆存在 B 实例会同时导致会话记忆断裂和 SSE 推送丢失——这是同一个根因的两个症状进程内状态。LeeResult也值得一提这个统一返回体的类头注释还保留了历史命名和一张状态码语义表200 成功、500 错误、501 校验、502 token、555 异常但项目里实际只用到LeeResult.ok(...)两处错误分支从没走过它没有全局异常处理。所以前端response.status 200的成功判断只覆盖了成功路径。4. 上云链路讲义给了什么缺了什么第 10 章的部署路径很直白装 JDK 21 → 装 Docker → 用 Docker 起 MySQL / Redis Stack / SearXNG → 打两个 jar →nohup起来 → Nginx。步骤讲义命令要点出处JDK从jdk.java.net/archive/下载export JAVA_HOME/usr/java/jdk-2110-3:3-10Dockeryum-config-manager --add-repo download.docker.com/linux/centos/docker-ce.repoyum install docker-ce docker-ce-cli containerd.iosystemctl enable docker10-4:5-63MySQLdocker pull mysql:9.3.0-p 5506:3306-e MYSQL_ROOT_PASSWORDREDACTED字符集utf8mb410-5:4-31Redis Stackdocker pull redis/redis-stack:latest-p 9379:6379REDIS_ARGS--requirepass REDACTED10-6:3-16SearXNGdocker run -p 6080:8080 --restartalways -v /home/docker/SearXNG:/etc/searxng10-7:3-25打包pom 加buildfinalName${project.artifactId}/finalNamespring-boot-maven-plugin10-9:4-14启动nohup java -jar mcp-server.jar my-server.log 21 client 同10-9:16-23暴露某公网地址:8888/sse10-9:26Nginx源码编译nginx-1.16.1装 gcc/pcre/zlib/openssl →./configure一堆--*-temp-path→make→make install→./nginx10-10:3-75这份清单里有四处必须在照抄前先处理掉。其一JAVA_HOME那段 classpath 写的是 Windows 语法而且引用了 JDK 9 之后就不存在的 jar。exportCLASSPATH.:%JAVA_HOME%/lib/dt.jar:%JAVA_HOME%/lib/tools.jar# 讲义 10-3:8Linux shell 里%VAR%不会被展开而dt.jar/tools.jar从 JDK 9 起就没有了。这段照抄到 CentOS 上不会带来任何效果只是不报错而已。这属于典型的旧笔记复用提醒读者环境变量只需要JAVA_HOME与PATH。其二打包讲义与仓库快照不一致。第 10 章要求往 pom 里加build段和finalName但mcp-client/pom.xml128 行与mcp-server/pom.xml97 行都没有build。按当前快照直接mvn package产出的是带版本号的 jar而讲义启动命令写的是nohup java -jar mcp-server.jar——找不到文件。要么补build要么改启动命令。其三profile与website.domain。application-prod.yml只有server.port: 9999CorsConfig依赖的${website.domain}只在 dev 里给过。生产上要么补配置要么用-Dwebsite.domainhttps://你的域名注入顺带一提CORS 配置是allowCredentials(true) 单个 origin allowedMethods(*)这个组合方向是对的带凭证就不能用通配 origin只是生产域名那份缺失。其四两个镜像标签的语义在这一年内已经变了照抄会踩到一个纯粹是名字含义造成的坑。讲义写的2026-09-29 实际含义取自 Docker 官方镜像定义文件影响mysql:9.3.0该 9.x 线已被 MySQL 推到9.7.2且9现在带lts标签小版本固定写法本身没问题但9.x 是 Innovation 版、不能上生产这个流行说法已经不再成立—mysql:8→8.4.11LTS8.0标签已从官方镜像里移除网上大量老教程写mysql:8.0今天要么拉不到、要么拉到的是被重定向的旧内容—latest/innovation→26.7.0Innovation 线已经跳到 26.x 版本号和 8.x/9.x 的命名方式完全断开redis/redis-stack:latestredis-stack 仓库 README 已写明 “New users should use Redis instead of Redis Stack”这行还能跑但新项目直接换redis:8向量检索已并入主线细节见本系列第二篇一句话mysql:8不等于 MySQL 8.0mysql:9也不再是 Innovation 版。部署文档里靠标签名推断版本区间的做法在这两个镜像上都会失效——要固定就写全major.minor.patch。5. Nginx 与 SSE讲义最大的那块空白第 10 章把 Nginx 从源码编译讲得非常细十几个./configure参数逐条解释但没有任何一段nginx.conf没有server{}、没有location、没有proxy_pass全仓库搜proxy_pass零命中。也就是说浏览器 → Nginx → Spring Boot这一段完全没写而:8888到底怎么映射到后端9999也没有答案是 Nginxlisten 8888还是云厂商端口转发笔者无法核实。这块空白在生产上非常要命因为Nginx 默认会缓冲上游响应而 SSE 恰恰不能被缓冲。官方ngx_http_proxy_module文档写得很清楚proxy_buffering Default: on Enables or disables buffering of responses from the proxied server. When buffering is enabled, nginx receives a response from the proxied server as soon as possible, saving it into the buffers ...翻译成人话缓冲开着时Nginx 会先把上游输出收进缓冲区攒够了再发给客户端。对普通接口无所谓对 SSE 就是打字机效果直接失效——前端会看到憋很久然后一大段一起出来甚至超时。同一个文档里还有两个默认值需要一起注意proxy_read_timeout Default: 60s ← The timeout is set only between two successive read operations send_timeout Default: 60sproxy_read_timeout的语义是两次读操作之间的间隔上限不是整个响应时长。这对 SSE 很关键只要 token 之间的间隔不超 60 秒连接可以无限期活着但模型思考很久、或者中间某段网络静默超过 60 秒Nginx 就先断。而应用侧代码是SseEmitter(0L)永不超时——于是断开发生在应用之外的代理层日志里看不到应用抛错前端只会观察到 EventSource 的error。这类看起来是模型卡了的问题八成是这样来的。笔者给一份能用的 SSE 反向代理配置通用做法非课程内容proxy_pass端口请按你的实际后端改location /sse { proxy_pass http://127.0.0.1:9999; proxy_http_version 1.1; proxy_buffering off; # 关键不许缓冲流式响应 proxy_cache off; chunked_transfer_encoding on; proxy_read_timeout 3600s; # 远大于模型 token 间隔 proxy_send_timeout 3600s; add_header Cache-Control no-cache; # 由后端响应头关闭缓冲也可以X-Accel-Buffering: no }官方文档还提到响应头里的X-Accel-Buffering取yes/no可以逐响应地开启或关闭缓冲——这比在location上全局关更精准只想让 SSE 走无缓冲不想让静态资源也失去缓冲时让后端在text/event-stream响应上加这个头即可。对应到这套工程还有两个前端地址要改js/request.js的 axiosbaseURL与pages/spring-ai.html里new EventSource(http://...:9090/sse/connect)都是硬编码的局域网地址。上云不改这两处前端在公网机器上根本连不上——这也是免构建前端省事之后的必然代价。Q2那要不要干脆用 WebFlux 全链路响应式值得考虑而且工程里已经有半只脚MCP 服务端用的就是spring-ai-starter-mcp-server-webflux。方案好处代价保持 MVC 手工异步改subscribe改动最小Flux.toStream()换掉即可业务代码结构不变仍然要在应用层自己管好线程与连接生命周期客户端也换 WebFluxFlux直接一路透传到text/event-stream不需要SseEmitter池天然背压阻塞式 JDBC MyBatis-Plus 在响应式栈里是隐患本工程mcp-server恰好就是 WebFlux MySQL MP 的组合值得留意它是怎么处理的笔者倾向先做第一条把阻塞桥接改成订阅因为它同时解决线程占用与取消语义而不需要重写数据访问层。全链路响应式是另一篇的题。6. 部署前检查清单按这份清单走完再谈上线顺序是有依赖的#事项判断依据1轮换所有凭据两份.env的 API Key、高德 MCP 的key、邮箱与授权码、Redisrequirepass、MySQL 口令它们随压缩包公开流传过一律视为已泄露2把凭据从仓库请出去只留.env.example.gitignore确认忽略.env工程用 dotenv yml 占位符机制已就位3补齐 prod 配置数据源地址、Redis 地址、website.domain、CORS 生产 originprod profile 目前只有端口一行4补buildfinalName或改启动脚本里的 jar 名讲义与快照不一致5SseEmitter治理连接鉴权、同 userId 重复连接时complete()旧连接、空闲心跳、连接数上限§3 四条6去掉Flux.toStream()改成订阅式推送并立刻返回§27Nginxproxy_buffering off、拉长proxy_read_timeout、确认8888 → 后端端口的映射归属§5讲义完全缺失8前端两处硬编码地址改成可配置request.js:3-10、spring-ai.html:1679若要多实例会话记忆与 SSE 连接池外置RedisChatMemory来源未核实 静态Map10加全局异常出口给前端一个失败事件仓库内无ControllerAdvice7. 最后总结流式这件事框架只负责给你Flux推给谁、怎么推、断了怎么办全是应用自己的事。这套工程用静态MapString, SseEmitteruserId/botMsgId两个键解决了前两件第三件断开与回收没解决。Flux.toStream()是这套工程最该改的一行它把一次异步流式生成变成了一次同步长请求。改法不难subscribe 保留finish全量帧即可前端不用动。SSE 上生产绕不过反向代理而Nginx 默认proxy_buffering on、proxy_read_timeout 60s官方文档默认值这两个设置恰好都对 SSE 不友好。讲义只教了怎么编译 Nginx没教怎么配它——这段空白必须自己补。部署链路上有三处照抄就崩Windows 语法的CLASSPATH、缺失的build段导致 jar 名对不上、prod profile 只有端口。对于后端 / 架构开发者我个人更关注的是第 5、6 两节教程工程和生产工程的差距基本不在功能而在状态放在哪、连接归谁管、代理怎么配、凭据怎么活下来这四件事上。这四件事 Spring AI 帮不了你它只保证前面那半条路很好走。全系列五篇到此结束第一篇讲架构与版本第二篇讲 RAG 与向量库第三篇讲自建搜索第四篇讲 MCP 工具这一篇讲流式与部署。事实说明本文引用的类名、方法、字段、端口、配置项、代码片段与讲义命令均来自 2026-09-29 对课程工程快照_springai_src/springai-mcp-rag-dev的逐文件实读底稿见research-springai-course.md含文件:行号定位。proxy_buffering默认on、proxy_read_timeout与send_timeout默认60s、X-Accel-Buffering的语义取自 Nginx 官方ngx_http_proxy_module文档2026-09-29 核对原文。§4 镜像标签语义表mysql:8→8.4.11 LTS、mysql:9→9.7.2 LTS、latest/innovation→26.7.0、8.0标签已移除逐行取自 2026-09-29 的 Docker 官方镜像定义文件 中三条Tags:声明redis-stack的弃用指向取自其仓库 README 当日原文。以下各项未取到一手依据正文只提问或明确标注、不作断言生产端口8888与后端9999的映射归属、ChatMemoryBean 的来源与默认实现语义、该工程 WebFlux 版 MCP 服务端如何处理阻塞式 JDBC、以及实际部署后 SSE 在默认代理配置下的具体表现。§2 的subscribe写法与 §5 的 Nginx 配置块为笔者给出的通用做法不属于课程内容凡标笔者倾向/我个人的段落为工程观点。所有密钥、口令、邮箱、公网地址已脱敏为REDACTED。参考资料 致谢[1] Nginx 官方文档ngx_http_proxy_moduleproxy_buffering/proxy_read_timeout/X-Accel-Buffering[2] Nginx 官方文档ngx_http_core_modulesend_timeout[3] Spring AI 官网 Spring AI GitHub 仓库[4] Spring Framework 文档Spring MVC 的异步请求与SseEmitter[5] MDNServer-Sent Events /EventSource[6] Reactor 项目主页Flux与背压[7] MyBatis-Plus 官网 MySQL 官网[8] Redis 官网 redis/redis 主发行版已内置 Search and Query / 向量检索 redis-stack 仓库README 弃用指向 SearXNG 文档站[9] Docker 官方文档 docker-library/official-imageslibrary/mysql标签定义§4 表格来源 OpenJDK 历史版本归档[10] Hutool 官网JSONUtil用于finish帧序列化 dotenv-java[11] Model Context Protocol 官网第四篇的工具协议来源[12] 课程随附工程压缩包与第 10 章部署讲义本地课件未公开链接——本文所有文件:行号与10-x:行号均指向该快照凭据与公网地址已在引用时脱敏。
RELATED

相关推荐

Context-Mode 上下文模式管理:从设计到落地的工程实践

Context-Mode 上下文模式管理:从设计到落地的工程实践

1. 从“上下文模式”说起:一个被低估的工程概念第一次看到“context-mode”这个词,很多人会下意识觉得它是个抽象到没法落地的概念。我一开始也这么想。但真正在几个项目里被上下文切换的问题折磨过之后,才意识到这个词背后藏着一类非常具体的…

📅 2026/10/20 4:29:26
剑指Offer 21:双指针原地调整数组奇偶顺序实战

剑指Offer 21:双指针原地调整数组奇偶顺序实战

1. 从一道经典题看双指针的实战价值剑指 Offer 21 这道题,题目本身一句话就能说清楚:给一个整数数组,把所有的奇数挪到前面,偶数挪到后面,顺序不限。很多人第一次看到会觉得“这不就是排序的简化版吗”,但真…

📅 2026/10/20 4:29:26
Python深度学习入门实战:从环境配置到模型训练的完整路线与避坑指南

Python深度学习入门实战:从环境配置到模型训练的完整路线与避坑指南

写这篇东西之前,其实我犹豫了一阵。市面上讲Python深度学习的教程太多了,从西瓜书到各类视频课,从免费文档到几千块的训练营,信息已经严重过载。但有个现象很有意思:很多人学完理论觉得懂了,一打开编辑器还…

📅 2026/10/20 4:29:26
MORE NEWS

更多资讯

📰

基于YOLO的跌倒检测实战:从数据处理到模型部署全流程解析

1. 项目核心与需求拆解:跌倒检测到底难在哪里我第一次接触跌倒检测这个项目,是一个做社区养老服务的合作方找上门来。他们想在一批养老驿站的活动区域装摄像头,用视觉方案判断老人有没有摔倒,出现异常就自动告警。这个需求听起来很…

📰

PyTorch模型转TensorRT全链路实战:ONNX导出、引擎构建与精度调优

模型训练完之后,真正让人头疼的往往不是网络结构本身,而是怎么把它塞进推理引擎里跑出该有的速度。我最近刚把一个 PyTorch 模型从.pth一路折腾到 TensorRT 的.trt引擎,中间踩的坑比训练本身还多。这篇就把整条链路——PyTorch 导出 ONNX、ON…

📰

Bibo:基于systemd-nspawn的Linux按需沙箱方案

先把话说清楚:这篇东西讲的是“Bibo”,一个我自己折腾出来的按需沙箱方案。核心思路一句话就能概括——平时日常操作桌面、浏览器、办公软件这些活,都是在干净的主系统里干;但凡涉及敲命令行、装依赖、编译、跑脚本这种Linux执行层…

📰

LLM2Vec实战:把大语言模型改造成语义检索编码器

如果只是把一个大模型的所有权重塞给分类层,那它永远只是一个“看起来能用的编码器”。真正让大语言模型变成文本编码器的,是两件事:让因果注意力能回头读上下文,以及把表示空间里的语义坐标摆正。这个结论是我在一个文档检索项目…

📰

企业专属AI系统落地全攻略:从选型、算力到知识库实践

企业专属AI系统这件事,我最近一年反复被身边的朋友问到,关键词无非是:该自己搭一套,还是直接用大模型API?硬件投入要多少?模型怎么选?数据怎么喂进去才靠谱?这些问题的背后&#xff…

📰

从单模型到复合系统:AI应用开发的工程化转型

先说结论:CS329Z 这门某高校 AI 方向课程的第一讲,把 Compound AI Systems(复合 AI 系统)放到最前面,还真不是随便排的。我最近完整啃完这三篇阅读材料之后,明显感觉到整个 AI 应用开发的语境正在发生一次很…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬