Spring AI 1.1.0 周学习回忆录:从 ChatClient 到手拼 RAG 的全部代码与坑

发布时间:2026/10/29 2:18:50

Spring AI 1.1.0 周学习回忆录:从 ChatClient 到手拼 RAG 的全部代码与坑 Spring AI1.1.0 实战周记从 ChatClient 到手写 RAG全套可运行代码 踩坑速查AI 转型系列・第 1 周技术复盘Java 后端转型 AI 应用开发第一周总结。10 个实验接口、22 个踩坑记录、通过javap校验 7 个 Jar 包 API 签名。本文不讲故事全部为干货可直接复制运行核心代码、Spring AI1.1.0 真实 API 签名全部反编译本地 Jar 验证不是复制网上过期文档、真实实测数据。如果你正在跟进 Spring AI 1.1.0本篇可以当做随身速查手册。技术栈与 Maven 依赖直接复制可用环境Spring Boot 3.5.3 Spring AI 1.1.0 JDK 21 本地 Ollama生成模型qwen3.8:27b‑mlx嵌入模型bge‑m3全部本地运行零大模型 API 调用费用propertiesjava.version21/java.versionspring-ai.version1.1.0/spring-ai.version/properties!-- BOM统一管理版本子依赖无需指定version --dependencygroupIdorg.springframework.ai/groupIdartifactIdspring-ai-bom/artifactIdversion${spring-ai.version}/versiontypepom/typescopeimport/scope/dependencydependencies!-- Ollama 模型核心starter对话嵌入 --dependencygroupIdorg.springframework.ai/groupIdartifactIdspring-ai-starter-model-ollama/artifactId/dependency!-- JDBC持久化会话记忆H2数据库存储对话历史 --dependencygroupIdorg.springframework.ai/groupIdartifactIdspring-ai-starter-model-chat-memory-repository-jdbc/artifactId/dependency!-- ⚠️重点向量存储是独立模块不会被starter自动传递引入必须手动添加 --dependencygroupIdorg.springframework.ai/groupIdartifactIdspring-ai-vector-store/artifactId/dependency!-- RAG问答Advisor组件QuestionAnswerAdvisor --dependencygroupIdorg.springframework.ai/groupIdartifactIdspring-ai-advisors-vector-store/artifactId/dependency!-- 额外需要spring‑web、spring‑jdbc、h2数据库自行引入 --/dependencies第一个高频坑Spring AI1.0 版本之后 Ollama 的 starter 已经改名为spring‑ai‑starter‑model‑ollama向量库、RAG 问答 Advisor 被拆分为独立 Artifact。不要完全照搬网络旧教程拿不准就去 Maven Central 核对组件是否存在。application.yml 配置spring:ai:ollama:chat:options:model:qwen3.8:27b-mlx# 模型名称必须和 ollama list 输出完全一致embedding:options:model:bge-m3# 对话模型、嵌入模型是两套独立配置使用不同模型1. ChatClient同步调用 SSE 流式输出// 同步调用一次性获取完整回答 String resp chatClient.prompt().user(q).call().content();SSE 流式接口实现打字机逐字输出效果importreactor.core.publisher.Flux;importorg.springframework.http.ResponseEntity;importorg.springframework.web.bind.annotation.GetMapping;importorg.springframework.web.bind.annotation.RequestParam;importorg.springframework.web.bind.annotation.RestController;RestControllerpublicclassChatController{privatefinalChatClientchatClient;publicChatController(ChatClientchatClient){this.chatClientchatClient;}GetMapping(value/chat/stream,producestext/event-stream;charsetUTF-8)publicResponseEntityFluxStringchatStream(RequestParamStringq){returnResponseEntity.ok(chatClient.prompt().user(q).stream().content());}}import jakarta.servlet.*; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponseWrapper; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; Component // ⚠️千万不能漏不加该注解Filter不会注册静默失效无任何报错 public class SseCharsetFilter extends OncePerRequestFilter { private static String fixContentType(String value) { if (value ! null value.startsWith(text/event-stream) !value.toLowerCase().contains(charset)) { return value ;charsetUTF-8; } return value; } Override protected void doFilterInternal(HttpServletRequest req, HttpServletResponse resp, FilterChain chain) throws ServletException, IOException { chain.doFilter(req, new HttpServletResponseWrapper(resp) { Override public void setContentType(String type) { super.setContentType(fixContentType(type)); } Override public void setHeader(String name, String value) { if (Content-Type.equalsIgnoreCase(name)) { value fixContentType(value); } super.setHeader(name, value); } Override public void addHeader(String name, String value) { if (Content-Type.equalsIgnoreCase(name)) { value fixContentType(value); } super.addHeader(name, value); } }); } }2. 会话记忆Spring AI1.1.0 真实 API排坑⛔网上大量旧教程、AI 生成代码会出现chatMemoryRetrieveSize、ChatMemory.RETRIEVE_SIZE常量1.1.0 版本已经完全删除API 属于编造直接编译报错。正确配置importorg.springframework.ai.chat.client.ChatClient;importorg.springframework.ai.chat.memory.ChatMemory;importorg.springframework.ai.chat.memory.MessageWindowChatMemory;importorg.springframework.ai.chat.memory.MessageChatMemoryAdvisor;importorg.springframework.ai.chat.memory.repository.ChatMemoryRepository;importorg.springframework.ai.model.chat.ChatModel;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;ConfigurationpublicclassChatClientConfig{/** * 会话记忆Bean唯一可以设置消息窗口大小的位置 * 默认最大保存20条消息SystemMessage人设消息永远不会被窗口截断清理 */BeanpublicChatMemorychatMemory(ChatMemoryRepositoryrepository){returnMessageWindowChatMemory.builder().chatMemoryRepository(repository).maxMessages(30).build();}BeanpublicChatClientchatClient(ChatModelchatModel,ChatMemorychatMemory){returnChatClient.builder(chatModel).defaultSystem(你是一位严谨的技术助手回答保持简洁。).defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build()).build();}}运行时指定会话 ID实现多会话隔离CONVERSATION_ID属于字符串契约拼写错误不会抛异常只会记忆静默失效不要在代码手动ChatClient.create(chatModel)裸创建对象会绕过全部 Advisor 记忆能力。// cid会话唯一id不同用户/对话使用不同cid实现会话隔离StringrespchatClient.prompt().user(q).advisors(a-a.param(ChatMemory.CONVERSATION_ID,cid)).call().content();记忆四层链路ChatModel(无状态大模型) ← ChatClient ← MessageChatMemoryAdvisor(拦截存取历史) ← MessageWindowChatMemory(消息窗口截断) ← ChatMemoryRepository(持久化存储)消息截断发生在消息存入存储的瞬间系统人设 SystemMessage 不会被清理。3. 结构化输出 entity () 三种写法// 使用Record做数据契约无样板代码publicrecordProductInfo(Stringname,Stringcategory,ListStringsellingPoints){}// ① 返回单个对象.call()之后调用老Milestone版本写在prompt链中间GA正式版API位置变更ProductInfoinfochatClient.prompt().user(u-u.text(抽取商品信息{desc}).param(desc,q)).call().entity(ProductInfo.class);// ② 返回顶层集合ListT对抗Java泛型擦除{}匿名子类不能省略ListProductInfolistchatClient.prompt().user(u-u.text(批量抽取商品信息{desc}).param(desc,q)).call().entity(newParameterizedTypeReferenceListProductInfo(){});底层原理BeanOutputConverter自动生成 JSON Schema 约束追加到 Prompt拿到返回做清洗 Jackson 反序列化。⚠️重要认知只能提升输出合规概率不是 100% 保证。生产环境必须做字段判空、解析异常捕获降级、高敏感业务增加重试。调试打印框架自动生成的格式指令BeanOutputConverterProductInfoconverternewBeanOutputConverter(ProductInfo.class);System.out.println(converter.getFormat());4. Prompt 模板花括号安全律RAG 高危坑chatClient.prompt()// system提示词没有内置param参数能力只能字符串替换.system(你是{domain}领域的专家.replace({domain},domain)).user(u-u.text(分析这段商品描述\n{desc}).param(desc,q))// ✅ 用户输入、检索文档永远使用param传入.call().content();三种运行时行为重点 RAG 开发表格场景现象模板占位符数量多于 param 传入参数直接抛出运行时异常param 传入参数模板没有对应占位符静默忽略不会报错隐蔽大坑直接把用户 / 检索文档字符串拼接进 text ()文本内部{xxx}会被识别为占位符直接报错铁律用户输入、RAG 检索出来的文档内容一律使用.param()传参禁止直接字符串拼接进模板。类比数据库相当于 SQL 的预编译#{}防止 Prompt 注入。5. Embedding 嵌入 手写余弦相似度// 文本向量化float[]vectorembeddingModel.embed(你好);// 自检配置声明维度 和 返回向量长度必须相等intdeclareDimembeddingModel.dimensions();// 批量向量化返回 Listfloat[]下标和输入文本一一对应Listfloat[]batchVectorsembeddingModel.embed(List.of(文本A,文本B));手写余弦相似度和 Spring AI 内置EmbeddingMath.cosineSimilarity逻辑完全一致反编译实锤异常逻辑、计算公式全部相同publicclassSimilarityUtil{/** * 余弦相似度 [-1 ~ 1]越接近1语义越相似 */publicstaticdoublecosine(float[]a,float[]b){if(a.length!b.length){thrownewIllegalArgumentException(向量维度不一致RAG铁律入库、检索必须使用同一个Embedding模型);}doubledot0,normA0,normB0;for(inti0;ia.length;i){dota[i]*b[i];normAa[i]*a[i];normBb[i]*b[i];}returndot/(Math.sqrt(normA)*Math.sqrt(normB));}}实测踩坑数据nomic‑embed‑text 中文短板同义句0.705无关中文句子高达 0.996短词退款 vs 垫款相似度 1.0向量几乎完全一致。不同语义文本全部挤在 0.99 窄区间排序直接失效。✅嵌入模型选型铁律嵌入模型语言必须匹配业务语料不要迷信排行榜分数拿自己业务文档实测 topK 检索命中率更换嵌入模型整个向量库必须全部重新向量化维度就是模型的户口不可混用。6. VectorStore 向量存储 手拼最小 RAG项目启动自动加载知识库CommandLineRunnerimportorg.springframework.ai.document.Document;importorg.springframework.ai.reader.TextReader;importorg.springframework.ai.splitter.TokenTextSplitter;importorg.springframework.ai.vectorstore.VectorStore;importorg.springframework.core.io.ClassPathResource;importorg.springframework.boot.CommandLineRunner;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importjava.util.List;ConfigurationpublicclassRagLoadConfig{BeanpublicCommandLineRunnerloadKnowledge(VectorStorevectorStore){returnargs-{// 1.读取文档ListDocumentdocsnewTextReader(newClassPathResource(knowledge/notes.txt)).get();// 2.文本切块切块大小直接影响检索质量ListDocumentchunksnewTokenTextSplitter().apply(docs);// 3.存入向量库内部自动调用EmbeddingModel完成向量化vectorStore.add(chunks);};}}裸检索接口RAG 排错第一现场⚠️排查 RAG 故障优先调用检索接口不要直接看问答输出老版本 APIgetContent()已经删除使用getText()获取文档内容// 只做向量检索不调用大模型用来排查检索是否命中正确片段ListDocumenthitsvectorStore.similaritySearch(SearchRequest.builder().query(q).topK(3).build());ListStringtextListhits.stream().map(Document::getText).toList();完全手写 RAG 链路不使用官方 AdvisorListDocumenthitsvectorStore.similaritySearch(SearchRequest.builder().query(q).topK(3).build());// 使用分割符拼接检索片段Stringcontexthits.stream().map(Document::getText).collect(Collectors.joining(\n---\n));// 组装prompt交给大模型StringanswerchatClient.prompt().system(仅根据参考材料回答问题参考材料没有相关信息就直接说不知道禁止编造。).user(u-u.text({question}\n\n参考材料\n{context}).param(question,q).param(context,context)).call().content();官方封装 QuestionAnswerAdvisor 对照组写法searchRequest 不要设置 query组件会自动拿用户输入的消息当做检索 queryStringanswerchatClient.prompt().advisors(QuestionAnswerAdvisor.builder(vectorStore).searchRequest(SearchRequest.builder().topK(3).build()).build()).user(q).call().content();SimpleVectorStore 内存向量库特性重启服务数据全部丢失项目启动通过 CommandLineRunner 重新灌入切换 Milvus/Qdrant 等专业向量数据库只是把「内存逐条余弦计算」替换为 HNSW 索引加速上层业务逻辑不变。RAG 最阴险坑点检索完全脱靶但程序不会抛出任何异常HTTP 返回 200只是返回错误片段。排查口诀/ask结果不对先打/search看原始检索结果7. 本周 API 速查表全部 javap 反编译验证避坑对照表表格功能Spring AI1.1.0 真实写法 ✅过期 / AI 编造旧写法 ❌结构化 entitycall().entity(Class)/entity(PTR)entity 写在 prompt 链中间Milestone 旧版设置记忆窗口大小MessageWindowChatMemory.builder().maxMessages(n)chatMemoryRetrieveSize()AI 编造不存在构建 SimpleVectorStoreSimpleVectorStore.builder(embeddingModel).build()new SimpleVectorStore(em)构造器 protected不能直接 new构建检索请求SearchRequest.builder().query(q).topK(3).build()new SearchRequest(q).withTopK()获取文档正文document.getText()document.getContent()方法已删除RAG 问答 AdvisorQuestionAnswerAdvisor.builder(vs).searchRequest(...).build()new QuestionAnswerAdvisor(vectorStore)记忆 Advisor 包路径org.springframework.ai.chat.client.advisororg.springframework.ai.chat.memory1.0 已经迁移8. 高频踩坑速查表10 个最高频问题表格现象排查 解决方案AI 生成的代码编译报错API 找不到使用javap/ 解压 jarunzip‑l反编译本地 Jar 核对真实签名不要迷信 AI 输出Ollama 报 404 模型找不到ollama list核对本地模型名称yml 必须和输出完全一致Spring 服务端口变成 4397环境变量 SERVER_PORT 优先级高于 yml 配置launchctl unsetenv SERVER_PORT清除环境变量curl 传中文 / 带{}参数报 400使用curl‑G --data‑urlencode做 URL 编码RFC3986 不允许裸传特殊字符SSE 流式中文乱码先用xxd确认原始字节乱码样式改变 后端字节没问题浏览器解码问题Filter / CommandLineRunner 完全不执行检查是否加Component/Bean没有注册到 Spring 容器等于不存在会话记忆完全不生效必须注入容器管理的 ChatClient Bean禁止代码内手动ChatClient.create()裸创建RAG 问答结果不对但没有报错调用裸检索接口/search验证优先排查嵌入模型是否适配业务语言param 传了参数模板不生效占位符名称与 param key 必须完全一致字符串契约不会编译报错合并代码引入莫名其妙 bug坚持每日 git commit留存变更避免残留半截旧代码本文是「Java 后端 AI 转型实录」系列周总结第 1 篇。项目后续开源在 GitHub链接见主页。下周预告Tool Calling 工具调用。
延伸阅读

更多相关文章

2026/10/29 2:18:50

Linux 服务器磁盘占满完整排查流程(宝塔环境)

# Linux服务器磁盘占满完整排查流程(宝塔环境)在用宝塔管理云服务器的时候,经常遇到磁盘使用率100%的告警,网站直接打不开。很多时候明明手动删了文件,空间还是不释放,这里整理一套完整排查步骤&#xff0c…

2026/10/29 2:18:50

Flink实时同步Oracle到StarRocks的几个踩坑经验分享

最近在搞数据实时同步的活,把Oracle的业务数据实时同步到StarRocks给分析用。用的阿里云Flink最新版本 vvr-11.7-jdk11-flink-1.20。选型没什么好说的,Flink CDC 读 Oracle,写 StarRocks,方案很成熟。但阿里云原生不带 Oracle CDC…

2026/10/29 3:28:55

AI Agent核心架构与工程实践全解析

1. AI Agent核心架构全景解析在AI技术爆发的2023年,AI Agent已成为最具潜力的技术方向之一。不同于传统单任务AI模型,一个完整的AI Agent需要具备记忆存储、工具调用和任务规划三大核心能力。这就像组建一支特种部队——记忆系统是士兵的经验库&#xff…

2026/10/29 3:28:55

hyperframes:用对象化封装告别tf2坐标变换样板代码

如果你写过一段时间ROS,对下面这段代码应该不陌生:先创建tf2_ros.Buffer,再挂一个TransformListener,然后每次做坐标变换之前,要先wait_for_transform,再lookup_transform,最后还要用tf2_geomet…

2026/10/29 3:28:55

营销技能地图:从硬技能、软技能到商业感知的完整修炼路径

每次有人问我“做营销到底要学哪些技能”,我都觉得这是个比“怎么追到喜欢的人”更难回答的问题。圈外人以为营销就是写文案、拍视频、搞活动,圈内人清楚,营销是一个由策略、数据、内容、渠道、沟通组成的复杂系统,它既要求你懂人…

2026/10/29 3:28:55

MySQL数据导出实战:工具选型与性能优化指南

1. MySQL数据导出的核心场景与工具选型作为关系型数据库的标杆产品,MySQL的数据导出功能是每个开发者必须掌握的生存技能。根据我十年DBA经验,数据导出需求主要出现在以下典型场景:生产环境数据迁移至测试环境(需排除敏感字段&…

2026/10/29 3:28:55

impeccable:基于npx的零配置前端E2E工作流

1. 项目概述:一个被误读的“完美”工具名,实则指向一套现代前端开发工作流最近在多个技术社区和 CLI 工具讨论区里,“impeccable”这个词频繁出现——但它根本不是某个知名开源项目的官方名称,而是一个被开发者自发用作代号、昵称…

2026/10/29 3:23:55

英伟达GPU架构演进:从SIMT到Transformer Engine的五层实战逻辑

1. 这不是芯片参数表,而是一份GPU架构演进的实战观察笔记“从初代架构到大模型时代,英伟达GPU底层架构演进与核心逻辑深度解析”——这个标题乍看像一篇学术综述,但如果你真把它当论文去读,十有八九会卡在G80和Fermi的寄存器分配差…

2026/10/28 14:42:12

全国计算机二级C语言真题PDF高效刷题指南

简介:这份PDF是全国计算机二级C语言考试的试题与答案解析,专为备考二级C语言的考生准备。内容涵盖C语言特点与设计理念、程序基本组成单位、main函数执行流程、函数嵌套调用规则、标识符命名规范、整型实型字符型数据类型、八进制十六进制数合法形式、运…

2026/10/28 15:28:56

requests+BeautifulSoup:爬虫抓取h1标题的入门实战指南

简介:一份以实战为导向的Python爬虫入门资源,面向具备基本编程基础、希望快速掌握网络数据抓取的初学者,可解决从零编写简单爬虫的常见困惑。实例以requests库发送HTTP请求、BeautifulSoup解析HTML文档,完整演示从网页中提取 标题…

2026/10/28 3:32:29

递推最小二乘参数辨识MATLAB编程:二阶系统与渐消记忆实现

简介:这是一篇面向控制系统、信号处理与机器学习等领域学习者的最小二乘参数辨识程序说明文档。资源共包含一个docx格式文档,压缩包大小七十八KB,内容围绕递推最小二乘算法和渐消记忆最小二乘算法展开:先构造一个极点位于单位圆内…

2026/10/29 0:03:42

学生选课系统DFD图绘制指南:边界划分与分层校验实战

简介:文档以顶层与第一层DFD图为核心,完整刻画学生选课系统的逻辑模型,面向软件工程课程设计、系统分析学习及毕设前期建模人群。顶层图界定系统边界,外部实体包括管理员、教师和学生三类角色;第一层图进一步细化用户登…

2026/10/29 0:03:42

JIRA敏捷落地:Scrum角色权限、Sprint与燃尽图避坑指南

简介:围绕“基于JIRA的敏捷开发项目管理”这一主题,面向产品负责人、Scrum Master及研发测试成员,梳理Scrum角色分工、迭代流程与JIRA工具配置的对应关系,适合初、中级团队导入敏捷管理时参考。压缩包共1个docx文件,大…

2026/10/29 0:03:42

SEMI S8人机工程验收实战:从条款拆解到现场实测的完整指南

简介:面向半导体设备设计、制造与运维工程师及安全合规人员,这份资料聚焦SEMI标准体系下的设备安全规范,重点收录SEMI S8-0308《半导体制造设备人体工程学工程安全指南》。内容围绕设备设计原则、错误与事故预防、人体工程学适配三大主线展开…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑