接到这个需求的时候,我心里其实挺犯嘀咕的。一套运行多年的Java业务系统,想在两周内把大模型能力接进去,还要把公司内部的文档做成知识库问答,业务同事上来就能直接提问。当时的方案会上,有人说自己封装HTTP调用,有人建议用Python单独起一套服务,我最后拍板用Spring AI 2.0,Java侧几乎零胶水代码就把大模型和RAG知识库拉通了。今天这篇就是把这次落地的完整思路、具体代码和踩过的坑全部抖出来,给正准备做同样事情的朋友一个可以直接照着动手的参照。
1. 为什么把Spring AI 2.0放进技术选型
1.1 自己封装HTTP调用,到底输在哪里
我见过太多Java团队接入大模型的第一反应,就是写一个HttpClient工具类,调一下模型接口,解析JSON返回。这个方案在Demo阶段跑得很欢,但一旦进入企业级场景,问题会一个接一个冒出来。
首先是协议适配问题。不同服务商的接口风格各不相同,有的兼容一套通用协议,有的有独特的流式返回格式,有的鉴权方式都不一样。今天接A家,明天业务方说要换B家,你的工具类就得跟着大改。其次是重试、超时、限流、Token统计这些“脏活”,全部要自己写。你可能还要维护一个线程池,处理流式SSE解析,处理网络抖动,最后你会发现,真正跟业务有关的核心代码没写几行,全在跟模型网关做基础设施搏斗。
更深层的问题是跟Spring生态脱节。项目里普遍依赖Spring Boot的配置中心、Actuator指标、RetryTemplate,自己封装的那套HTTP调用就像一座孤岛,没法自动参与配置管理,没法统一监控,没法优雅地注入到业务Bean里。团队里任何一个人接手,都要花时间重新读懂这套私有封装。
所以我的建议很直接:能用社区成熟的统一抽象,就不要自己维护一套大模型客户端。Spring AI 2.0恰好就是干这个事的。
1.2 Spring AI 2.0 解决的六个问题
在Spring生态里,Spring AI做的事情可以理解成“大模型版的Spring JDBC Template”。它不绑定某一家具体供应商,而是把常见的大模型能力抽象成一套统一API。
我实际用下来,它主要帮我们解决了六类问题。第一,统一对话客户端。不管你底层接的是哪家模型网关,业务代码里拿到的都是同一个ChatClient,换供应商基本不动代码。第二,自动配置。引入对应的Starter之后,连一个@Bean都不用写,配置项填好就能注入客户端,这就是标题里说的“零代码接入”的基础。第三,Prompt模板管理。把系统提示词、用户问题、上下文参数化,避免在业务代码里拼接字符串。第四,结构化输出。模型返回的JSON可以映射到Java对象,直接用强类型的方式处理结果。第五,RAG全套组件。文档解析器、文本切分器、向量模型客户端、向量存储抽象、检索增强流程都有现成的接口,串起来就行。第六,可观测性。依托Spring Boot的Actuator,调用耗时、Token消耗等指标能接进监控体系。
2.0版本在我看,最明显的变化是ChatClient的API更顺滑了,链式调用非常贴合Java开发者的习惯。同时Advisor机制给横向扩展留了很好的口子,比如后面我可以把安全审核、敏感词过滤、知识库引用追溯都做成一个个Advisor,挂到调用链上,而不是散落在业务代码里。
1.3 “零代码接入”不是玄学
必须要说清楚,标题里讲的“零代码”,不是指一行代码都不写。Java项目接大模型,无论如何都要写调用代码。这里的“零代码”指的是面向基础设施的样板代码几乎为零。
你自己封装HTTP调用的时候,至少要写:网络客户端、请求构建、响应解析、错误处理、重试逻辑、Token统计、异步线程池。用Spring AI 2.0,这些全部被Starter的自动配置覆盖了。你只需要干两件事:在配置文件里填上模型网关的地址和密钥,然后在业务代码里注入ChatClient,调用一次call或者stream。
真正的业务代码反而更聚焦:把用户问题传进来,带着业务上下文调模型,把结果返回出去。这个体验,对从零开始接大模型的Java团队来说,提升是非常明显的。
2. 核心细节解析与实操要点
2.1 一个ChatClient Bean是怎么自动装配的
很多人看到ChatClient可以直接注入,会好奇这到底是怎么发生的。其实逻辑不复杂:Spring AI的Starter通过AutoConfiguration,在应用启动时读取Spring AI前缀的配置项,然后创建对应的模型客户端Bean,再包装成ChatClient。
所以你在pom.xml里引入Starter,然后在application.yml里配上模型供应商地址、密钥、模型名,系统启动后ChatClient就已经在容器里了。整个过程你看不到任何工厂类,这就是Spring Boot自动配置的威力。
但这里有个容易忽略的点:如果你的项目里有多个模型网关,或者需要不同的模型参数组合,只靠自动装配一个Bean是不够的。这时候可以用ChatClient.Builder在代码里创建多个命名用的客户端实例。比如一个低成本模型做摘要,一个高智商模型做复杂推理,底层都用同一个ChatClient.Builder来创建,配置互相隔离。
核心的Bean依赖关系是这样的:模型供应商的RestClient负责HTTP通信,RetryTemplate负责重试,ModelOptions承载温度、最大Token数等参数,最后统一交给ChatClient对外暴露。这套链路全部由Spring容器管理,想替换任何一环,都只需要替换对应Bean。
2.2 调用大模型的四个关键配置
实战里最常用的配置就四个,把它们调明白了,大部分问题也就解决了。
第一个是模型网关地址与密钥。生产环境千万不能把密钥直接写在application.yml里提交到代码仓库,而是用环境变量注入,比如${LLM_API_KEY}。第二个是模型名。不同网关的模型命名规则不统一,要在供应商文档里确认准确的模型标识ID,填错会直接报模型不存在。第三个是温度参数。它控制生成结果的随机性。知识库问答这类对准确性要求高的场景,温度建议调到0.1到0.3;闲聊、创意生成可以放到0.7以上。第四个是超时时间。大模型接口的响应速度波动很大,默认超时往往不够,建议显式配置连接超时和读取超时。
这里还有一个非常容易被坑的点:Token长度限制。模型有最大上下文长度,你塞进去的知识库片段、系统提示词、多轮对话历史都会占上下文空间。生产上要做长度预算,别让用户的输入把上下文撑爆。
2.3 结构化输出与业务对象转换
如果只是让模型生成一段文本,接入工作其实很简单。但企业级场景里,模型往往是业务流程的一环,需要返回能够直接入库或者继续处理的结构化数据。比如一个工单分类需求,我要的是“类别、紧急程度、处理建议”这几个字段,而不是一段散文。
Spring AI 2.0在结构化输出上做得相当顺手。你可以定义一个Java记录类,把期望的字段列出来,调用时传入这个类型,框架会指示模型按JSON Schema返回,并自动完成反序列化。
public record TicketAnalysis(String category, String priority, String suggestion) { }ChatClient chatClient = ...; TicketAnalysis result = chatClient.prompt() .user("请分析这条工单内容: " + ticketText) .entity(TicketAnalysis.class);这段代码看起来很简洁,但背后有几个细节。第一,模型返回的JSON必须严格匹配字段名,字段命名建议统一用英文,避免中文命名的兼容性问题。第二,entity方法会隐式地在提示词中加入输出格式约束,如果模型服务的接口不支持JSON Schema,框架会退化成文字约束,效果会差一些。第三,如果模型经常返回格式错误的JSON,前端要配合容错逻辑,解析失败时给用户一个友好的重试提示,不能直接抛异常。
3. RAG知识库接入实战
3.1 从原始文档到最终回答的完整链路
RAG全称叫检索增强生成,核心思路很简单:模型不知道你公司内部的业务资料,那就先把这些资料切碎、向量化、存起来,等用户问问题的时候,先从资料库里检索出最相关的片段,再把片段塞进提示词,让模型基于这些片段回答。
这个链路拆开就是五个环节:文档解析、文本切分、文本向量化、向量存储与检索、提示词增强与生成。
文档解析解决的是“怎么把PDF、Word、Markdown里的内容读出来”。文本切分解决的是“怎么把长文档切成适合检索和喂给模型的片段”。文本向量化是把片段变成一组浮点数,用数学上的相似度来表达语义相似性。向量存储与检索负责在海量向量中快速找出最接近用户问题的候选片段。最后的提示词增强则把这些片段和原始问题组装成一个完整Prompt,交给模型生成答案。
我曾经看过不少团队在这条链路上偷工减料,直接把整篇文档塞进提示词,结果要么超出上下文长度,要么回答时被大段无关信息干扰。RAG的关键不是“有没有做”,而是每一步的参数和策略有没有认真调。
3.2 文档切分策略:最容易被忽视的胜负手
我始终认为,RAG效果不好,80%的问题出在文档切分上。很多团队上来就用固定字符长度切,比如每500个字符一段,结果一个完整的知识点被拦腰截断,模型拿到的是残缺信息。
比较稳妥的做法是分两层切分。第一层按文档结构切,优先识别标题、段落、列表等结构块,保证一个语义单元尽量完整。第二层对仍然过长的块做长度控制,同时设置重叠区域。这个重叠区域很关键,它能避免检索时因为切分边界把关键上下文丢掉。
chunkSize: 800 overlap: 150我见过一个真实场景,某产品的技术问答里有一句话跨了两个切分片,前半句在上一片结尾,后半句在下一片开头。用户问到这个点的时候,模型只看到后半句,理解完全跑偏。加了150字符的重叠之后,问题立刻消失。
在切分时还要考虑文档的格式特征。如果是PPT,按页切;如果是PDF,按标题和段落切;如果是代码仓库,按文件切。没有放之四海而皆准的切分方式,必须结合文档类型做迭代。
3.3 向量存储选型与参数细节
向量化之后,所有切分片段都变成了一组高维浮点数。给这些浮点数建索引、做相似度检索,就是向量存储的工作。
选型上,我建议分阶段考虑。第一,快速验证阶段,直接用内存版向量存储,数据量小、部署简单、零运维成本。第二,数据量上来之后,迁移到关系型数据库自带的向量扩展,好处是复用数据库的运维体系,权限、备份、监控都一起解决。第三,如果数据量真的到了千万级,再考虑专业向量数据库。
我用下来最常用的检索参数是topK和相似度阈值。topK表示返回最相似的前K个片段,一般取3到5个就够。相似度阈值用来过滤那些“看着相关、实际无关”的片段,宁可不注入,也别误导模型。
这里还有一个用户体验技巧:检索结果要带上文档来源和页码。这样用户看到答案,可以点开原文核对,信任感会强很多。
3.4 用Prompt模板把检索结果喂给模型
检索出来的片段不能直接丢给模型,那样模型会分不清哪些是参考知识、哪些是用户问题,甚至可能被不相关内容带偏。所以一般要设计一套固定的提示词模板,明确告诉模型:以下是参考资料,请基于资料回答;如果资料里没有答案,请直接说不知道,不要编造。
Spring AI的PromptTemplate很适合干这件事。模板里留出两个变量:一个是上下文片段列表,一个是用户问题。
String template = """ 你是一个企业内部知识库助手。 请严格基于以下资料回答用户问题。 如果资料中没有答案,请回答“资料库中未找到相关信息”。 参考资料: {context} 用户问题: {question} """;这样写的好处是,提示词的结构完全跟业务代码分离。业务方想调整语气、想增加输出格式要求,直接改模板文件就行,不需要发版。
在生产环境,我还会加上“引用来源”要求,让模型在回答末尾列出参考了哪些文档片段。这一步不仅能追溯,还能压制模型胡编乱造,因为它知道每句话都能被溯源。
4. 从Demo到企业级上线的实操记录
4.1 工程骨架速览
我建议直接基于Spring Boot 3.x起工程,Java 17以上。因为Spring AI自动配置依赖Spring Boot的机制,用太老的Boot版本会遇到很多兼容性问题。
依赖方面,引入对应模型网关的Spring AI Starter,再加上向量存储相关的扩展包。这里的Starter命名要以你实际使用的版本和适配器为准,不同服务商的包名不同,但使用方式大同小异。如果用的是本地模型方案,不需要额外引入网络配置。
工程目录我习惯这样拆:
src/main/java/com/example/ragdemo/ controller/ ChatController.java RagController.java service/ RagService.java DocumentIngestService.java vectorstore/ VectorStoreConfig.java advisor/ SecurityAdvisor.java src/main/resources/ prompts/ rag-prompt.st application.yml一眼扫过去就知道职责边界。Controller只负责HTTP入参出参,Service负责业务编排,VectorStoreConfig负责初始化向量存储,Advisor放横切逻辑。
4.2 最小可运行的问答接口
第一步,先把最基础的大模型问答跑通。配置好之后,写一个Controller,注入ChatClient,一个接口就出来了。
@RestController public class ChatController { private final ChatClient chatClient; public ChatController(ChatClient.Builder builder) { this.chatClient = builder.build(); } @PostMapping("/chat") public String chat(@RequestBody String message) { return chatClient.prompt() .user(message) .call() .content(); } }这个接口看上去很朴素,但它是后面所有复杂功能的基础。跑通之后,你再慢慢加参数、加系统提示词、加流式响应。建议先用Postman调通,确认配置没问题,再开始写RAG。
一个很容易踩的坑是:ChatClient.Builder在应用里可能已经被自动配置覆盖了,如果你在别的地方又创建了Builder,要注意不要覆盖默认配置。我通常会保留一个build()出来的ChatClient作为主客户端,其他客户端都从这个Builder二次创建,命名隔离。
4.3 RAG问答接口完整实现
接下来是重头戏,把RAG链路串起来。我分两部分来实现:一部分是知识入库,一部分是检索问答。
知识入库的核心流程是:读文档、切分、向量化、存向量库。
public void ingest(String path) { DocumentReader reader = new ReadableDocumentReader(path); List<Document> documents = reader.get(); TextSplitter splitter = new TokenTextSplitter(800, 150); List<Document> chunks = splitter.apply(documents); vectorStore.add(chunks); }这里有个生产细节:入库是个相对重的操作,建议做成异步任务,或者通过消息队列触发。同时要做好幂等,同一份文档重复导入时,要么覆盖旧向量,要么生成新的版本号,避免向量库中积累一堆历史脏数据。
检索问答的接口是这样:
@PostMapping("/rag/query") public String ragQuery(@RequestBody String question) { return chatClient.prompt() .advisors(a -> a.param("question", question)) .user("请回答这个问题: " + question) .call() .content(); }advisors在这里干了一件很重要的事:它会自动触发检索流程,把向量库里最相关的片段取出来,填入RAG提示词模板,再交给模型。你不必手动把检索结果拼到用户消息里,这个动作被框架封装了。
当然,如果你对检索过程有很强的定制需求,也可以手动调vectorStore.similaritySearch,拿到结果后自己组装Prompt。两种方式我都试过,能用advisors的默认能力就不折腾,省下来的时间可以去做更有价值的事情。
4.4 生产配置与性能调优
从Demo到上线,中间差着一大截生产配置。我列几个重要的。
第一,线程池隔离。大模型调用是IO密集型,不能占用业务线程池。建议单独配置一个线程池给AI调用链路使用,核心线程数可以结合QPS估算,队列策略要谨慎,避免大模型响应慢导致线程堆积。
第二,超时和重试。大模型网关不是每次都能在几百毫秒内返回,网络抖动、服务过载都会导致慢响应。连接超时可以设置短一点,读取超时适当放宽。重试次数我一般控制在2次以内,且只在返回明确的网络错误时才重试。模型本身的欠费、鉴权失败这类错误,重试多少次都没用。
第三,熔断与降级。当模型网关持续异常时,不能无限等下去,要在一段时间内快速失败,并降级返回一些预设文案。Spring的CircuitBreaker可以接进来,配合@Recover定义降级逻辑。
第四,缓存。相似度很高的重复问题,可以直接缓存答案,省Token又能提升响应速度。但要注意,知识库更新后缓存要能失效,否则用户会一直拿到旧答案。
5. 常见问题排查与上线避坑
5.1 高频问题速查表
实战中我积累了一个问题排查清单,直接照着排查基本都能解决。
| 现象 | 可能原因 | 排查思路 | 解决办法 |
|---|---|---|---|
| 启动报模型不存在 | 模型名配置错误 | 核对供应商模型ID | 在管理后台确认可用模型ID |
| 调用超时 | 读取超时太短 | 查看日志中实际响应时间 | 放宽读取超时,开启流式 |
| Token额度很快用完 | 上下文塞入了大量无关信息 | 检查RAG片段长度 | 调小切分长度,降低topK |
| 回答完全没用知识库 | 提示词模板缺少RAG指令 | 检查advisors链路 | 显式注入检索结果和模板 |
| 检索结果不相关 | 切分粒度过粗或过细 | 抽样打印切分片段 | 调整chunkSize和overlap |
| 中文答案乱码或吞字 | 流式响应解析异常 | 检查SSE连接与编码 | 确保响应ContentType正确 |
| 密钥明文泄露 | 配置写进仓库 | 扫描历史提交记录 | 立即吊销,改用环境变量 |
5.2 几个必须提前做好的安全与合规动作
企业级上线,光有功能是不够的。大模型接口天然存在输入输出安全风险,我习惯在接入层加一层审核。
第一,输入侧要做敏感信息检测。用户提问时可能把身份证号、内部合同编号这类敏感信息发给模型,要在请求入口拦截,或者脱敏后再传给模型。第二,输出侧要做内容合规过滤。模型生成的内容需要经过审核,符合要求再返回给用户。第三,日志里不能记录完整的Prompt和模型输出原文,更不能用明文存储密钥和用户问题。这些一旦进入日志平台,就成为潜在的数据泄露点。
我通常把安全的动作做成Advisor或者过滤器,挂在ChatClient调用链上,这样不污染业务代码,还能保证所有入口的调用都经过同一套安全策略。
5.3 上线前检查清单
最后分享一份我每次上线前都会过一遍的清单,按顺序打钩。
- 密钥全部通过环境变量或配置中心注入,仓库中无明文密钥。
- 配置了连接超时、读取超时,重试次数不超过2次。
- 大模型调用所在线程池与业务线程池隔离。
- RAG知识库入库任务具备幂等性和失败重试。
- 检索结果带来源引用,回答支持溯源。
- 输入输出均有安全过滤,日志脱敏。
- Token消耗和调用量已经接入监控指标。
- 设计好降级文案,模型不可用时用户体验不被中断。
我个人在实际操作中最大的体会是,这个项目真正的难点从来不在调通接口,而在把文档喂得恰好、把成本控得住、把安全和可观测性补齐。Spring AI 2.0把基础设施那部分做得很扎实,让我可以把精力放在业务和知识处理上,这大概就是它最有价值的地方。如果后面有机会,我还会把多轮对话的上下文压缩和Agent工具调用再深挖一轮,到时候继续来分享。