玩转 Spring AI 第一篇:一套 RAG + MCP + 实时搜索的工程是怎么拼起来的
💡Java 后端接大模型,这两年基本绕不开 Spring AI。但真到你打开一个完整工程的时候,会发现难点根本不在"怎么调一次 chat",而在另一堆问题上:向量库放在哪、嵌入模型用什么、实时搜索从哪来、业务方法怎么变成模型能调用的工具、流式输出怎么推给浏览器、这些东西又分别跑在哪个端口上。
笔者手上有一份比较完整的工程快照:两个 Maven 模块、一个免构建的前端、21 篇讲义,把 Spring AI 的 model / vector store / advisors / MCP client / MCP server 五条能力线全用上了。它不是玩具 Demo,因为它已经把"上下文从哪来"这件事拆成了三条独立链路,也因为它里面藏了不少真实工程才会有的取舍和瑕疵。
这一篇不写代码怎么写,先把整栋房子的骨架看清楚:模块怎么分、端口怎么连、版本锁在哪、一条请求从浏览器走到模型再走回来要经过哪几站。后面四篇再逐条链路拆。
1. 先把地图画出来:两个 Java 模块 + 四个外部进程
这个工程最容易被名字误导:模块叫mcp-client和mcp-server,看起来像"MCP 双进程"。其实mcp-client才是业务主应用,它同时是 ChatClient 的宿主、RAG 的检索方、联网搜索的调用方、SSE 推送方;而mcp-server只是一个"把业务方法暴露成工具"的独立进程。
浏览器(Vue2 + ElementUI + axios,无构建) │ EventSource │ /sse/connect?userId=xxx POST │ /chat/doChat | /rag/search | /internet/search ▼ ┌───────────────────────── mcp-client :9090 ─────────────────────────┐ │ Controller → Service → ChatClient │ │ │ │ │ │ │ │ │ ├─ ToolCallbackProvider(MCP 工具集合)│ │ │ │ ├─ MessageChatMemoryAdvisor(会话记忆)│ │ │ │ └─ stream() → Flux<String> │ │ │ ├─ RedisVectorStore ──→ redis-stack :9379 │ │ │ ├─ OkHttp ────────────→ searxng :6080 │ │ │ └─ SSEServer(静态 SseEmitter 池) │ │ └─ dotenv 读 .env → OPENAI_API_KEY / BASE_URL / MODEL │ └───────────────┬──────────────────────────────┬──────────────────────┘ SSE │ 本地 9060 SSE │ 远程 + stdio 子进程 ▼ ▼ mcp-server :9060 ──→ MySQL :5506 高德 MCP / server-filesystem(npx) (@Tool 方法注册)端口是这套拓扑里最容易记错的部分,一次性列全(全部实读自配置文件,非推测):
| 进程 | 监听端口 | 容器映射 | 出处 |
|---|---|---|---|
mcp-client(dev) | 9090 | — | mcp-client/src/main/resources/application-dev.yml:2 |
mcp-client(prod) | 9999 | — | application-prod.yml:2 |
mcp-server(dev) | 9060 | — | mcp-server/.../application-dev.yml:2 |
mcp-server(prod) | 9999 | — | application-prod.yml:2 |
| Redis Stack | 宿主机9379 | -p 9379:6379 | 讲义6-2-1:8-11、application.yml:36 |
| MySQL | 宿主机5506 | -p 5506:3306 | 讲义02:24、mcp-server/application.yml:9 |
| SearXNG | 宿主机6080 | -p 6080:8080 | 讲义7-2:12、application.yml:45 |
| 前端静态站 | 5501 | VSCode Live Server 之类 | application-dev.yml:4-5的website.domain |
注意 dev 和 prod 的mcp-client与mcp-server端口不同,这意味着生产环境里前端连的是 9090 还是 9999,取决于讲义第 10 章那台服务器上到底怎么起的——这一条笔者没有取到一手证据,后面凡是涉及"生产拓扑"的地方都会标注。
还有一个必须现在就发现的事实:mcp-client的CorsConfig依赖一个只有 dev profile 才提供的占位符。
// CorsConfig.java:17@Value("${website.domain}")privateStringdomain;而application-prod.yml里只有两行——server.port: 9999,没有website.domain。也就是说,直接把 profile 切到 prod 启动,占位符解析这一步就过不去(除非用-Dwebsite.domain=...之类外部注入,但仓库里没有这种配置)。这是典型的"教学工程按 dev 一路跑通,生产配置从没真正落地过"的痕迹。
2. 版本清单:这套工程到底锁了哪些依赖
接大模型的工程有个共性麻烦:Spring AI 的 starter 名字长、命名在 1.0 前后改过一轮,再叠加 MyBatis-Plus 与 Boot 3 的mybatis-spring兼容问题,光是把 pom 抄对就要折腾半小时。所以这份清单值得完整抄下来。
2.1 构建基座
| 项 | 值 | 出处 |
|---|---|---|
| Spring Boot 父 POM | 3.5.0(spring-boot-starter-parent,<relativePath/>) | 父pom.xml:23-28 |
| Java | 21(maven.compiler.source/target,三个 pom 各写一遍) | 父pom.xml:18-20、子模块 pom |
| 模块 | 仅mcp-client、mcp-server,父工程<packaging>pom</packaging> | 父pom.xml:8-11,10 |
| Spring AI BOM | spring-ai-bom1.0.0,两个子模块各自import | mcp-client/pom.xml、mcp-server/pom.xml的dependencyManagement |
| 依赖拉取提示 | 父 pom 注释里建议加阿里云镜像mirrorOf: central | 父pom.xml:30-38 |
这里要说清一件容易混的事:工程锁的 1.0.0 不是当前版本。笔者 2026-09-29 用 GitHub Releases API 核对了一遍:
| Spring AI tag | 发布时间(UTC) |
|---|---|
v1.0.9 | 2026-06-12 |
v1.1.8 | 2026-06-12 |
v2.0.0 | 2026-06-12 |
v2.0.1 | 2026-08-21 |
v2.1.0-M1 | 2026-09-25 |
也就是说1.0.x线的维护版本已经走到1.0.9,而官方主版本号已经进入2.0.x。三个不同线的版本在同一天(2026-06-12)出现,说明官方在并行维护多条版本线。
如果你要照这套工程练:锁 1.0.0 是最稳的复现路径,因为 starter 的 artifactId(spring-ai-starter-model-openai、spring-ai-starter-vector-store-redis这种model-/vector-store-前缀命名)就是 1.0 这一代定下来的;升到 2.x 之前,请先确认配置项前缀和 Advisor 类名在你选的版本里还叫这个。这个提醒不是空话——本工程里就有一个"类名/包名对不上"的实例:讲义9-1里 XML 的resultMap type写com.itzixi.entity.Product,而代码实际是com.itzixi.pojo.Product。
2.2 mcp-client 的依赖
spring-boot-starter-web spring-boot-starter-aop spring-boot-starter-test spring-ai-starter-model-openai spring-ai-advisors-vector-store spring-ai-starter-vector-store-redis spring-ai-starter-model-transformers spring-ai-starter-mcp-client spring-ai-mcp spring-ai-tika-document-reader lombok commons-lang3 3.17.0 groovy-json 4.0.27 hutool-all 5.8.30 dotenv-java 3.2.0 okhttp 4.12.0这张依赖表里藏着一个后面第二篇要重点讲的事实:spring-ai-tika-document-reader和spring-ai-advisors-vector-store都进了 pom,但代码里一个都没用上——文档读取走的是TextReader,检索问答走的是手写的{context}/{question}模板而不是QuestionAnswerAdvisor。依赖引了、能力没用,这是教程型工程很常见的"提前铺路",也是读代码时必须交叉验证的原因。
2.3 mcp-server 的依赖
| 依赖 | 版本 | 备注 |
|---|---|---|
spring-ai-starter-mcp-server-webflux | BOM 管 | 用 WebFlux 版而非 MVC 版,MCP 服务端是响应式 SSE |
com.mysql:mysql-connector-j | 8.0.33 | 显式锁版本 |
com.baomidou:mybatis-plus-boot-starter | 3.5.10.1 | 排除org.mybatis:mybatis-spring |
org.mybatis:mybatis-spring | 3.0.4 | 排除后又手工回补 |
spring-boot-starter-mail | BOM 管 | 邮件工具用 |
flexmark-all | 0.64.8 | Markdown → HTML |
MyBatis-Plus 这一对"排除 + 回补"的动作值得单独说一句。Boot 3.x 要求mybatis-spring3.x,而 MP 3.5.10.1 传递依赖的版本与 Boot 3.5 对不上时,标准处置就是把传递依赖排掉、再显式声明一个兼容版本——这不是课程花招,是 Boot 3 迁移期真实存在的依赖冲突修法。笔者在自己的项目里遇到过完全一样的形状,只是当时是靠报错反推出来的。
3. 一个 ChatClient 是怎么被组装出来的
这套工程真正"Spring AI 味"最浓的地方,是ChatServiceImpl的构造器。整个类 200 多行,但大模型能力全部来自这几行:
// ChatServiceImpl.java:57-65publicChatServiceImpl(ChatClient.BuilderchatClientBuilder,ToolCallbackProvidertools,ChatMemorychatMemory){this.chatClient=chatClientBuilder.defaultToolCallbacks(tools)// ① 全部 MCP 工具.defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())// ② 会话记忆// .defaultSystem(systemPrompt) // ③ 系统提示词(当前注释掉).build();}ChatClient.Builder由spring-ai-starter-model-openai自动配置注入,你不需要自己new。这一行构造里同时挂了三件事,正好对应 Spring AI 的三个抽象层:
| 挂上去的东西 | 来自哪里 | 作用 |
|---|---|---|
ToolCallbackProvider tools | spring-ai-starter-mcp-client按 yml 里的 sse/stdio 配置自动装配 | 让模型能调用外部工具(第四篇) |
MessageChatMemoryAdvisor | spring-ai-chat-client的 advisor 包 + 一个ChatMemoryBean | 把历史消息带进下一轮 prompt |
defaultSystem(...)(未启用) | 代码里的systemPrompt常量 | 人设/系统提示词 |
调用侧只有两种形态,贯穿全篇:
非流式:chatClient.prompt(prompt).call().content() 流式 :chatClient.prompt(prompt).stream().content() → Flux<String>这里有个读代码时必须发现的断层:ChatMemory是从构造参数直接注入的,但整个仓库里没有任何ChatMemory或ChatMemoryRepository的@Bean声明,也没有spring.ai.chat.memory.*配置。那它从哪来?只能是 Spring AI 1.0.0 的自动配置给了一个默认实现(默认是按会话窗口放在内存里的那类)。这一条笔者没能从工程里取到一手依据,只能作为"读代码时的合理推断"标注出来——而它的工程含义很实际:如果ChatMemory是进程内存实现,那么服务一旦重启、或者多实例部署,会话记忆就会断。这在第五篇谈部署时会再撞上一次。
Q1:既然有了 ChatClient 的 stream(),为什么还要自己写一套 SSE?
这是这套工程最值得琢磨的设计选择,也是第二、五篇的分水岭。
Flux<String>直接作为 MVC 返回值确实能流式吐给浏览器(工程里HelloController就是这么干的,返回Flux<ChatResponse>/Flux<String>)。但主业务链路没用这条捷径,原因看前端就明白:
Flux 直出 → 一条 HTTP 响应,只有当前这个请求能看到,且要浏览器自己解析流 SseEmitter 池 → 服务端握着 userId → SseEmitter 的映射,可以随时向任意用户推事件聊天场景需要的是"服务端知道该推给谁":一次POST /chat/doChat请求本身不应该承载回答,回答是从另一条已经建立的/sse/connect?userId=xxx长连接上推下来的。所以工程用SSEServer维护了一个静态连接池,doChat只负责触发与推送,返回值是void。
Flux直出与SseEmitter池子的区别:
| 维度 | Flux作为 MVC 返回值 | SSEServer+SseEmitter(本工程主链路) |
|---|---|---|
| 连接方向 | 客户端发起请求,响应边生成边收 | 浏览器先建立长连接,之后服务端主动推 |
| 触发与推送是否同一条连接 | 同一条 | 不同(POST 触发,GET 长连接推送) |
| 服务端能否群发 | 不能 | 能(sendMsgToAllUsers) |
| 是否需要业务关联键 | 不需要 | 需要userId+botMsgId |
| 典型用途 | 单请求单回答、SSE 演示页 | 多轮对话、多端同步、消息中心 |
代价在第五篇细讲:为了跨这个边界,代码用了Flux.toStream(),等于把响应式流又拽回了阻塞世界。
4. 三条能力链路,其实只有一段不一样
这是笔者读完整个工程后最想说清楚的结构性结论,也是这套工程值得抄的地方。
工程对外暴露三个聊天入口,前端用两个互斥勾选框路由:
只勾"知识库搜索" → POST /rag/search → 先查向量库,再问模型 只勾"联网搜索" → POST /internet/search → 先搜 SearXNG,再问模型 两个都不勾 → POST /chat/doChat → 直接问模型三条链路的后半段完全同构:
Flux<String>stringFlux=chatClient.prompt(prompt).stream().content();List<String>list=stringFlux.toStream().map(chatResponse->{Stringcontent=chatResponse.toString();SSEServer.sendMsg(userId,content,SSEMsgType.ADD);// 逐 token 推returncontent;}).collect(Collectors.toList());StringfullContent=list.stream().collect(Collectors.joining());SSEServer.sendMsg(userId,JSONUtil.toJsonStr(newChatResponseEntity(fullContent,botMsgId)),SSEMsgType.FINISH);// 收尾推完整 JSONdoChat、doChatRagSearch、doInternetSearch三个方法里这段代码是一字不差重复的。差别只有一件事:喂给模型的 prompt 里那段"上下文"从哪来。
| 链路 | 上下文来源 | 关键类/方法 | 上下文形状 |
|---|---|---|---|
| 普通对话 | 无 | ChatServiceImpl.doChat | 用户原话直接进chatClient.prompt(prompt) |
| RAG | Redis 向量库 | DocumentServiceImpl.doSearch→redisVectorStore.similaritySearch(question) | 【上下文】{拼接的 Document.getText}【问题】{question} |
| 实时搜索 | 自建 SearXNG | SesrXngServiceImpl.search→ OkHttp?q=&format=json | <context>\n[来源] url \n [摘要] content \n </context>重复多次 |
三条链路的提示词模板都写在 Java 常量里(文本块),而且 RAG 与联网两条都留了同一句兜底:“如果没有查到,请回复:不知道。”
// ChatServiceImpl.java:123-134(RAG 模板)privatestaticfinalStringragPROMPT=""" 基于上下文的知识库内容回答问题: 【上下文】 {context} 【问题】 {question} 【输出】 如果没有查到,请回复:不知道。 如果查到,请回复具体的内容。不相关的近似内容不必提到。 """;这就是简单理解:RAG、联网搜索、微调,在工程视角下干的其实是同一件事——往 prompt 里塞上下文,区别只在塞的内容从哪来、什么时候塞。三条链路把这个等式摆得比一般 Demo 明白得多:
向量库(离线知识) ┐ 搜索引擎(实时知识)├─→ 拼进 prompt ─→ ChatClient ─→ Flux ─→ SSE 推给浏览器 会话记忆(历史知识)┘顺带就能看出这套工程没有做的事:检索结果与搜索结果都没有和 MCP 工具混进同一次调用;spring-ai-advisors-vector-store引了却没用QuestionAnswerAdvisor;RAG 的 topK、相似度阈值一次都没出现在代码里——全走 Spring AI 库默认值:SearchRequest.DEFAULT_TOP_K = 4、相似度阈值默认0.0(即不过滤),出处是官方 Vector Stores 参考页,第二篇会具体讲这两个默认值在真实文档上意味着什么。这些都留给后面几篇。
5. 前后端契约:三个字段撑起整条流式链路
前端是 Vue 2 + ElementUI 2.15.13 + axios + marked,全部用<script>直引本地libs/,没有任何构建步骤。这个选择对读者很友好(改一行刷新即可),但也决定了所有地址是硬编码的:baseURL: "http://10.0.0.8:5501"之类写死在js/request.js:3-10,SSE 地址写死在pages/spring-ai.html:167。
整条对话链路的契约只有三个字段:
{"currentUserName":"<前端生成的 userId>","message":"<用户输入>","botMsgId":"<12 位随机串>"}currentUserName:页面created()时用Math.random().toString(36).substring(2, 12)生成,同时用作 SSE 连接的 userId(spring-ai.html:127-133)。这就是"服务端该推给谁"的答案。botMsgId:每次发送前由generateRandomId(12)生成。它是一次回答的流身份——服务端每推一个 token 都往chatList里botMsgId匹配的那条气泡追加,finish事件到达后把这条气泡内容整体替换成marked.parse(message)渲染好的 HTML,并把me.botMsgId = null。message:用户原话。
为什么非要有botMsgId?因为add事件推的是碎片,finish事件推的是全文,两者必须能被前端认作同一条消息:
add("你") add("好") add(",我是") …… → 逐字拼进气泡(打字机效果) finish({"message":"完整 Markdown 原文","botMsgId":"xxx"}) → 整条重渲染(表格/代码块/标题才正确)如果不做这一步"收尾整体重渲染",Markdown 的表格和代码块会在逐字追加的过程中长期处于语法不完整状态,渲染出来是乱的。这是流式聊天 UI 的一个通用套路,值得单独抄下来:增量负责"快",全量负责"对"。
后端事件名由SSEMsgType枚举固定:
| 枚举 | 事件名 | 语义(按源码注释) |
|---|---|---|
MESSAGE | message | 单发的普通消息 |
ADD | add | 消息追加,适用于流式 stream 推送 |
FINISH | finish | 消息完成 |
CUSTOM_EVENT | custom_event | 自定义 |
DONE | done | 自定义(注释标了 ChatGLM v4) |
顺带一处工程里的真实错配:spring-ai.html:262监听的是customEvent(驼峰),而后端事件名是custom_event(下划线)——这个监听永远不会触发。另外js/apis/admin/doctorApi.js:27-32里还留着getRecords → GET /ollama/getRecords,但后端没有对应 Controller,属于另一段没有落地(或被删掉)的能力。读教程工程时,这类"前端残留接口"往往比正文更能提示你课程原本想做哪一步。
6. 上手前必须知道的四个工程真相
这一节是笔者自己的部分,不是课程内容。把一套教程工程当学习材料没问题,当生产模板要非常小心,因为下面四条都会跟着你进生产。
6.1 压缩包里带着真实形态的凭据
工程快照里有两份.env(DeepSeek 与通义千问 DashScope 兼容模式各一份)、高德 MCP 连接 URL 上的key=、163 邮箱账号与授权码、Redisrequirepass、MySQL 的root口令,讲义里还直接写了服务器公网地址。
这些值笔者在本系列里一律写作sk-<REDACTED>/<REDACTED>,不复现原值。但有一件事必须说清:只要一份课件压缩包在网盘上流传过,里面所有凭据就应该视为已泄露。如果你的工程是这种形态,正确的动作顺序是:轮换密钥 → 把凭据从仓库里请出去 → 再谈功能。
工程里用的 dotenv 方案本身是对的,值得抄:
// Application.java:16-22Dotenvdotenv=Dotenv.configure().ignoreIfMissing().load();dotenv.entries().forEach(entry->System.setProperty(entry.getKey(),entry.getValue()));SpringApplication.run(Application.class,args);配合 yml 里全部写成占位符,密钥就只存在于.env:
spring:ai:openai:api-key:${OPENAI_API_KEY}base-url:${OPENAI_BASE_URL}# 例:https://api.deepseek.comchat:options:model:${OPENAI_MODEL}# 例:deepseek-chat / qwq-plus这套写法真正的价值在于"换模型只换 .env":同一份代码,把.env从 DeepSeek 那份换成通义那份,base-url与model就整体切换,业务代码零改动。这也是工程用spring-ai-starter-model-openai而不是各厂商 SDK 的原因——只要厂商提供 OpenAI 兼容端点,Spring AI 这套 ChatClient 就照用不误。
6.2 dev / prod 分离只做了一半
mcp-server的两个 profile 文件各只有一行端口。数据源、Redis、邮箱、MCP server 名称全写在主application.yml里,而那里的数据库地址是jdbc:mysql://127.0.0.1:5506/...、口令是明文。
按这份配置直接切 prod,应用会去连它自己那台机器的 5506。生产配置的真实形态在仓库里没有答案(可能存在第三份配置或-D参数,笔者无法核实)。这条要记住的判断是:profile 只有端口差异,等于没有做环境分离。
6.3 讲义与代码会互相打脸
同一个工程里,笔者至少核到这几处对不上(都属于"照笔记做会崩、照代码跑才通"):
| 主题 | 讲义 | 代码 | 采信 |
|---|---|---|---|
| Redis 端口 | 6-3写port: 8379 | application.yml与6-2-1都是9379 | 9379(8379 视为笔误) |
| MCP server 端口 | 8-8写server.port: 8090 | dev9060/ prod9999 | 代码 |
sse-endpoint | 8-8没提 | spring.ai.mcp.server.sse-endpoint: /sse | 代码 |
| client 的 MCP 连接 | 8-7只配了高德 | 多了server2 → http://localhost:9060+ stdio | 代码 |
| JDBC 端口 | 9-1写 3306 | 5506 | 代码 |
| 主键策略 | 9-1写id-type: assign_id | 该行值为空 | 无法核实空值行为 |
| resultMap 包名 | com.itzixi.entity.Product | com.itzixi.pojo.Product | 代码 |
| MySQL 版本 | 文件名叫Docker安装MySQL8.md | 正文与镜像是mysql:9.3.0 | 无法判定 |
| Redis 密码 | 6-2-1的docker run带requirepass | 6-2-2 docker-compose.yml没有REDIS_ARGS | 两份互斥 |
| 打包插件 | 10-9要求加<build>+finalName | 两个子模块 pom都没有<build>段 | 快照与讲义不一致 |
最后一行尤其要紧:按当前快照mvn package产出的 jar 名是带版本号的,而讲义的启动命令写的是nohup java -jar mcp-server.jar。照抄会直接找不到文件。
6.4 有些能力是"引而未用"或"注释停用"
Tika文档读取器、向量Advisor、defaultSystem(systemPrompt)、前端的图片理解按钮(已注释)、/ollama/getRecords接口——都属于"铺了路没通车"。
这不算坏事,甚至很真实(笔者自己的仓库里也有一堆注释掉的下一步)。但它决定了一件事:读教程工程必须以代码为准,讲义只是索引。后面四篇每篇都会带一段"讲义没写但必须补上"的坑,其中最有代表性的两个先剧透:SearXNG 的 JSON 输出默认是关闭的(第三篇),以及 MCP 工具的注册其实只需要启动类里一个 Bean(第四篇)。
Q2:那这套工程值不值得照着敲一遍?
笔者的判断是值得,但要带着这份"体检报告"去敲。
值得的部分很实在:它没有停留在"调一次 chat",而是把 RAG、联网搜索、工具调用、流式推送这四件真实产品一定会做的事都各自落了一条能跑的链路;配置项、类名、端口全部可核对;前端零构建,卡住了容易定位;AOP 埋了耗时日志(ServiceLogAspect:>3000mserror、>2000mswarn),这在等大模型响应时非常有用。
要提防的部分就是 §6 那四条。尤其是Flux.toStream()那个桥接——它把流式链路变成阻塞链路,连带POST /chat/doChat的返回语义、线程占用、以及"Nginx 60s 超时下的长连接表现"全部变复杂,而第五篇会说明为什么这是这套工程最需要在生产前重写的地方。
7. 最后总结
- 看结构:
mcp-client是业务主应用,mcp-server只是工具宿主;四个外部进程(Redis Stack、MySQL、SearXNG、stdio 子进程)各占一个端口,别被模块名误导。 - 看版本:Boot 3.5.0 + Java 21 + spring-ai-bom 1.0.0是这套工程的复现基线;官方当前已走到
1.0.9 / 1.1.8 / 2.0.1 / 2.1.0-M1,升级前先核对 starter 命名与配置前缀。 - 看核心:一个
ChatClient.Builder挂上工具 + 记忆 + (未启用的)系统提示词,全工程的大模型能力都从这一行出来。 - 看骨架:三条链路只有"上下文来源"不同,后半段
stream → SSE add → finish完全同构——想理解 Spring AI,先理解这个同构。 - 看契约:
userId决定推给谁,botMsgId决定拼到哪条气泡;增量求快、全量求对,这是流式聊天 UI 的通用解法。 - 对于后端 / 架构开发者,我个人更关注的是:这份快照暴露的不是框架问题,而是教程工程到生产工程之间那段没人补的距离——凭据管理、profile 分离、依赖冲突修法、阻塞式流桥接、反向代理对长连接的影响。
后面四篇按链路拆:第二篇 RAG 与向量库,第三篇自建 SearXNG 实时搜索,第四篇 MCP 工具,第五篇 SSE 流式与上云部署。
事实说明:本文所有版本号、端口、类名、配置项、代码片段与"讲义 vs 代码"差异,均来自 2026-09-29 对课程工程快照(
_springai_src/springai-mcp-rag-dev,含 23 个 Java 文件、9 份配置、21 篇讲义)的逐文件实读,引用位置记为文件:行号,底稿见同目录research-springai-course.md。以下三项未取到一手依据,正文只作提问或明确标注,不作断言:生产环境的真实拓扑与端口映射、ChatMemoryBean 的来源与默认实现语义、MyBatis-Plusid-type空值的绑定行为。第 4 节引用的SearchRequest默认值(topK = 4、相似度阈值0.0)取自 Spring AI 1.0 参考文档 Vector Stores 页(2026-09-29 核对)。Spring AI 与 MCP 的版本时间线取自 GitHub Releases API(2026-09-29 查询)。凡标"笔者判断/我个人"的段落为工程观点,非课程或官方表述。所有密钥、口令、邮箱、公网地址已脱敏。
参考资料 & 致谢
[1] Spring AI 官网 / Spring AI GitHub 仓库 / Releases API 原始数据
[2] Spring Boot 项目页
[3] Model Context Protocol 官网 / MCP 规范仓库 Releases
[4] MCP 官方 servers 集合 / awesome-mcp-servers
[5] Redis 官网(Redis Stack / Vector Sets)
[6] SearXNG 文档站 / SearXNG 上游searx/settings.yml
[7] MyBatis-Plus 官网
[8] MySQL 官网 / Nginx 官网 / Docker 官网
[9] OkHttp 文档 / Hutool 官网 / flexmark-java
[10] dotenv-java
[11] DeepSeek 开放平台 / 阿里云百炼(DashScope 兼容模式) / 高德开放平台
[12] Smithery MCP 目录 / MCP.so / 魔搭社区
[13] 课程随附工程压缩包与 21 篇讲义(本地课件,未公开链接)——本文所有文件:行号均指向该快照,凭据与公网地址已在引用时脱敏。