☰
Spring AI 1.0实战:Ollama接入、RAG与函数调用解析
2026/10/22 9:04:45 网站建设 项目流程

把Spring AI从头到尾撸一遍之后,最大的感触是:Java服务端接大模型这件事,终于不再靠手工拼JSON了。做服务端开发的朋友应该都有印象,前两年想在项目里加一个对话能力,最朴素的方案就是拿HTTP客户端去调模型接口,自己封装请求参数、解析返回结果,遇到流式输出还得自己处理分段。Spring AI正是冲着这个痛点来的,它把模型接入、提示词管理、结构化输出、向量检索这些高频操作抽象成了Spring风格的能力。

这篇记录是我在个人学习Spring AI过程中的实战整理,适合两类人看:一类是从Spring Boot出发、想快速跑通AI能力的后端工程师,另一类是已经接触过Python侧LLM应用、想了解Java生态里对应方案的人。文章不会只贴官方Demo,重点放在从1.0版本改名、本地模型接入、RAG和函数调用这几个实际会遇到的节点上,每个节点都会说清楚为什么这样做。文章里出现的代码都基于Spring AI 1.0这个稳定里程碑,如果你抄到的博客还是0.8.x的旧写法,正好按这篇的思路做一次升级。

1. Spring AI到底在解决什么问题:从手工调API到标准接入层

1.1 没有Spring AI之前,Java项目里的AI接入通常长什么样

要理解Spring AI的价值,先看没有它的时候Java工程里会写成什么样。最常见的做法是用RestTemplate或WebClient封装一个HTTP调用,业务代码里手写模型供应商要求的JSON结构,像model、messages、temperature、max_tokens这类字段得一个一个对上。流式输出更麻烦,需要自己解析SSE格式,按data:前缀切分返回内容,再拼成一段完整文本。

这样的代码在单个模型、单个接口时还能接受,一旦项目里有多个Agent角色,或者需要把历史消息带进上下文,代码就开始失控了。更难受的是模型切换:今天用云端接口,明天想换成本地部署模型,两边的请求格式、认证方式、超时处理全不一样,等于把业务层逻辑再重写一遍。我在早期一个内部工具项目里就吃过这个亏,最初只做了一版接口封装,后来要支持另一个模型供应商,光改底层调用和参数映射就花了大半天,而业务层没有任何变化也照样要跟着回归测试。

1.2 Spring AI核心抽象:Chat、Embedding、VectorStore三条线

Spring AI的基本思路,是把常见的模型能力抽成几个稳定的接口,再针对不同供应商提供对应的实现。这和JDBC的思路非常像:接口统一,厂商各有各的驱动,业务代码只依赖抽象即可。三条最核心的线分别是:ChatModel负责聊天和文本生成,EmbeddingModel负责文本向量化,VectorStore负责向量数据的存储与相似度检索。上层再叠加ChatClient、Advisor、Memory、StructuredOutput这些面向具体场景的API。

用一个不太好听但很准确的类比:Spring AI是模型世界的JDBC,VectorStore就是数据库连接,EmbeddingModel就是驱动里的类型映射器。ChatModel接口背后有OpenAI实现、Ollama实现、Azure实现等,你拿到的是同一个ChatClient,却可以用它对接完全不同的模型。第一次体会到这点的时候,我意识到自己做的手工封装就是在重复造轮子,而且造得还没官方框架圆。

1.3 边界认识:它不是训练框架,也不是提示词万能药

学习Spring AI时要先弄清楚它的边界。它做的是推理集成层,不负责模型训练,微调和预训练仍然主要在Python生态完成,这是两个领域的话题。Spring AI也不替你写提示词,更不保证提示词效果——它只是把提示词组装、参数传递、结果解析这些机械化动作收敛到框架里,让你能把精力放在业务逻辑上。

如果只盯着“AI生成”这个结果,很容易误以为框架能提升模型回答质量。实际上模型回答质量取决于模型本身、提示词设计、上下文质量和检索策略,框架解决的是“怎么更方便地接入和使用”的问题。理解了这条边界,你再去看Spring AI的模块划分,会清晰很多:它承认自己只是连接层,所以把连接和扩展都做到极致。

2. 版本选型与架构调整:1.0改名背后改了哪些东西

2.1 从AiClient到ChatClient/ChatModel

Spring AI早年的版本里,核心入口叫AiClient,我当时照着0.8.x教程写了一个Demo,调用方式大概是aiClient.call(new Prompt(...))。到了1.0阶段,这个类型被重新设计,拆分成了更面向业务的高层APIChatClient和更底层的模型抽象ChatModel。之所以这么做,是因为旧设计把“对话”和“模型通信细节”耦合在一起,调用链越长越难扩展。

现在你在官方文档里看到的主流写法,是基于ChatClient的流式API:先prompt(),再.user(),最后.call()或.stream()。这种写法最大的好处是把“提示词的组装”变成了一条声明式链路,你可以随时在中间插入advisor、记忆、函数注册等行为,而不必改动核心业务代码。底层换了模型,上层调用基本不用变。

我建了个简单的对照表,方便从老版本迁移时快速理解:

能力点0.8.x时代1.0时代
核心客户端类型AiClientChatClient / ChatModel
提问方式aiClient.call(new Prompt(...))chatClient.prompt().user(...).call()
流式处理返回类型不直观stream()返回Flux<String>
结果提取getResult().getOutput().content()直接拿字符串
模型能力组织集中在AiClient里ChatModel、EmbeddingModel、ImageModel等分线管理

2.2 Spring Boot版本与仓库配置

Spring AI 1.0稳定版发布以后,对Spring Boot版本有明确要求,整体围绕Spring Framework 6.x和Spring Boot 3.x体系来做。我建议直接使用Spring Boot 3.4或更高版本,JDK至少17,个人学习用21更省心。工程的依赖管理与普通Spring Boot不太一样,需要先引入spring-ai-bom的BOM,再按模型供应商引入对应的starter。

在pom.xml里的关键配置大概是这个样子:

<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>1.0.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-ollama-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>

注意,老教程里经常出现的spring-ai-openai-spring-boot-starter本身没错,但如果你用的是里程碑版本或快照版本,Maven Central里可能没有对应坐标,必须额外配置Spring的里程碑仓库。这也是很多人一上来依赖就拉不下来的原因。

2.3 依赖不一致的坑:自动配置失效与NoSuchMethodError

版本选择这件事,我觉得最值得记下来的坑就是“BOM版本和Boot版本不匹配导致的自动配置失效”。明明starter加进去了,启动时ChatClient.Builder却注入不进来,或者干脆报NoSuchMethodError。原因通常是:某个传递依赖被Boot的依赖管理覆盖成了旧版本,Spring AI的自动配置类在运行期找不到对应方法。

我当时的排查方法比较笨但有效:先把spring-ai-bom的版本和Spring Boot的版本写成一致管理,然后查看启动日志里的自动配置报告,搜索spring.ai相关条件的matched和did not match。如果发现条件判断失败,优先检查是哪个配置类的@ConditionalOnClass没满足。绝大多数情况下,把Spring Boot小版本往上升一级或者降一级选个官方配套版本,问题就消失了。版本这件事在AI框架里比普通业务库更敏感,因为模型供应商SDK更新快,框架需要跟着适配。

3. 第一个可运行的Demo:本地Ollama接入步骤与踩坑

3.1 为什么我用Ollama而不是直接调云端接口

个人学习阶段,我强烈建议先用Ollama跑本地模型,而不是一上来就申请云端API。理由很简单:本地启动没有API Key门槛,不需要考虑额度消耗,网络环境也干净,把精力集中在框架使用上。我现在回想,如果当初直接对接云端接口,大概率会把时间花在认证配置和费用问题上,而不是理解ChatClient的根本用法。

Ollama不仅免费,还提供了一个和主流协议兼容的接口,甚至可以给同一个模型起多个别名来测试不同参数。我用得比较多的是qwen2.5:7b这种7B级别模型,显存8GB左右就能跑;如果你机器配置一般,可以先用llama3.2:3b这类小模型验证链路,响应快很多。本地模型回答质量肯定不如云端顶尖模型,但用来学架构、跑流程已经完全够用。

3.2 从零搭一个chat工程

整个接入过程比我想象中短。第一步在本地安装并启动Ollama,执行ollama pull qwen2.5:7b把模型拉到本地,确认ollama list能看到模型后再继续。第二步创建Spring Boot工程,JDK选17以上,Web依赖必须加,因为我们要暴露HTTP接口。第三步按上文pom片段引入spring-ai-ollama-spring-boot-starter。第四步配置application.yml:

spring: application: name: spring-ai-demo ai: ollama: base-url: http://localhost:11434 chat: model: qwen2.5:7b options: temperature: 0.7

第五步写一个最简Controller,用ChatClient.Builder构建客户端,然后调用:

@RestController public class ChatController { private final ChatClient chatClient; public ChatController(ChatClient.Builder builder) { this.chatClient = builder.build(); } @GetMapping("/chat") public String chat(@RequestParam(defaultValue = "介绍一下你自己") String message) { return chatClient.prompt() .user(message) .call() .content(); } }

启动后访问/chat?message=你好,第一次会稍微慢一点,因为Ollama需要把模型加载到内存里。看到返回内容的那一刻,整个链路就算通了。

3.3 流式输出:让对话接口有打字机效果

浏览器里的对话应用基本都是逐字返回的,Spring AI的stream()方法天然返回响应式流,配合Spring WebFlux可以达到类似打字机效果。代码上只需要把接口改成返回Flux<String>:

@GetMapping(value = "/chat/stream", produces = "text/event-stream;charset=UTF-8") public Flux<String> chatStream(@RequestParam String message) { return chatClient.prompt() .user(message) .stream() .content(); }

这里注意produces一定要带charset=UTF-8,否则中文可能在客户端显示成乱码。前端如果用EventSource对象来接收,几乎不用做额外解析工作,SSE本身就是文本流。我用这个接口做了一个非常简陋的网页聊天框,体验已经完全接近真实应用。

3.4 联调时踩到的几个实际坑

本地联调最容易遇到的是连接问题。第一个坑是Ollama没启动,Spring AI启动不会报错,但请求时会出现连接拒绝,解决方式是检查http://localhost:11434能否访问,确认ollama serve或桌面端进程在运行。第二个坑是模型名写错,请求会返回“model not found”这类提示,执行ollama list核对一下名称就行。

第三个坑是响应慢得离谱。笔记本CPU推理7B模型确实慢,但不是故障;如果明显卡顿到几十秒不出结果,看看是不是多个模型同时常驻内存,ollama ps能看到当前加载的模型,可以显式ollama stop释放显存。第四个坑是在Docker环境里跑Ollama,Spring Boot应用也在容器里时,localhost指向容器自身,要改用宿主机地址或host.docker.internal。第五个坑是关于中文乱码,接口没指定UTF-8,前端拿到的内容中文会变成乱码,代码里写清楚produces即可。

4. 进阶玩法:结构化输出、RAG与函数调用的落地顺序

4.1 结构化输出:别再做“返回JSON再手撕”的事

聊天接口返回字符串只能算入门,实际业务里我们更常需要模型返回一个Java对象。早期我做这种东西,是在提示词里写“只输出JSON”,然后拿字符串用Jackson解析,模型偶尔多一句解释就会解析失败。Spring AI的BeanOutputConverter把这条链路封装好了,它会在提示词里附加JSON Schema,告诉模型字段名和类型,返回后直接解析成目标对象。

假设需要模型推荐一部剧,并且返回名称、平台和集数,可以定义一个record:

public record Drama(String name, String platform, int episodes) { }

使用方式有两种,一种是显式converter:

BeanOutputConverter<Drama> converter = new BeanOutputConverter<>(Drama.class); String promptText = """ 推荐一部适合周末看的剧,并严格按要求的格式输出。 %s """.formatted(converter.getFormat()); String content = chatClient.prompt() .user(promptText) .call() .content(); Drama drama = converter.convert(content);

另一种是ChatClient直接提供实体转换:

Drama drama = chatClient.prompt() .user("推荐一部适合周末看的剧") .call() .entity(Drama.class);

这两种方式底层都需要模型本身具备一定指令遵循能力,本地7B模型也能做到,但复杂嵌套结构偶尔会出错。建议刚开始只用两层到三层的简单对象,跑通后逐步增加嵌套复杂度。遇到模型返回内容里多出“好的”这类前缀,优先检查提示词里对“只输出JSON”的约束是否够强。

4.2 RAG:把外部资料变成模型的新常识

RAG是我学Spring AI时花时间最多的地方,也是收益最明显的一部分。它解决的问题很常见:模型不知道你公司内部的业务知识,比如产品手册里写了某个配置项只能接受特定值,模型却给你编了一个答案。RAG的完整链路可以拆成六步:读取文档、切分文本、向量化、存入向量库、检索相关片段、把检索结果作为上下文交给模型生成回答。

我用一个本地PDF做了演示,先读取文件并切分:

TikaDocumentReader reader = new TikaDocumentReader( new FileSystemResource("docs/product-manual.pdf") ); List<Document> documents = reader.get(); TokenTextSplitter splitter = new TokenTextSplitter(); List<Document> chunks = splitter.apply(documents);

再存进VectorStore:

vectorStore.add(chunks);

检索时构造查询:

SearchRequest request = SearchRequest.builder() .query(question) .topK(5) .similarityThreshold(0.7) .build(); List<Document> docs = vectorStore.similaritySearch(request);

这里有一个特别容易被忽略的点:索引时用的Embedding模型和查询时用的Embedding模型必须保持一致,否则向量维度和语义空间都对不上,检索效果会变得完全不可用。我踩过一次,切到另一个Embedding模型后忘改查询侧配置,结果搜出来的片段跟问题毫无关联,排查了很久才意识到是模型不一致。

VectorStore的选型上,如果只想本地实验,用内存级别的SimpleVectorStore就够了;如果偏好持久化,推荐pgvector或Redis,前者可以利用关系数据库的运维体系,后者适合已经有Redis基础架构的团队。Spring AI的抽象让这些向量库之间的切换成本变得很低,配置文件换一下,代码基本不动。

4.3 Function Calling:让模型能自己调你的方法

把Function Calling理解成“让模型在自己的回答里临时调用你提供的方法”会比较直观。它并不是模型直接执行Java代码,而是模型根据函数的描述和当前问题,生成一个结构化的调用请求,框架收到后再去执行对应方法,把结果返回给模型继续生成回答。整个过程就像一个老板写了张条子让员工去查数据,员工把结果贴在桌面上,老板再根据结果做最终决策。

我注册一个天气函数作为演示:

public record WeatherRequest(String city) {} public record WeatherResponse(String city, int temperature, String condition) {} @Bean @Description("根据城市中文名查询实时天气") public Function<WeatherRequest, WeatherResponse> weatherFunction() { return request -> new WeatherResponse(request.city(), 26, "晴"); }

然后在ChatClient中启用它:

String answer = chatClient.prompt() .user("北京今天几度?适合穿短袖吗?") .options(ChatOptions.builder() .function("weatherFunction") .build()) .call() .content();

需要注意三点。第一,@Description写得越清晰,模型越容易判断什么时候该调用这个函数;如果描述模糊,模型可能会绕来绕去不调用,或者把用户问题硬套进一个不合适的函数。第二,Function的入参类型和返回类型必须是能被序列化/反序列化的结构,尽量用Java record。第三,底层模型必须支持工具调用,qwen2.5和较新的llama3.x都支持,太过老旧的模型会导致工具调用格式化不正确。

4.4 三个能力组合的客服问答场景

把结构化输出、RAG、函数调用放在一起,就能搭出一个很接近生产环境的客服场景。比如用户问“我的订单为什么还没发货”,先让模型做一次意图识别,用结构化输出判断这是订单查询还是售后投诉;如果是知识类问题,走RAG从产品文档里检索答案;如果涉及实时数据,则通过Function Calling查询订单系统。三个环节各管一段,每个环节的代码都相对独立。

我当时把这个场景拆成三个方法后,最大的感受是:与其把所有逻辑塞进一个大提示词,不如把任务分阶段处理。这样既方便调试,也让每个阶段的效果可度量——哪一步的回答质量出了问题,直接看对应环节的输入输出就能定位,而不是盯着整段提示词猜。

5. 个人学习路径与问题排查清单

5.1 我建议的学习顺序

如果让我重新学一遍,我会按这条路线推进:先跑通基础聊天,再学结构化输出,然后做RAG,接着掌握函数调用,最后按需了解多模态和语音能力。基础聊天用于理解依赖和自动配置;结构化输出能让你真正感受到“模型结果变成业务对象”的价值;RAG解决的问题最贴近企业需求;函数调用则是把AI从纯文本生成推向系统操作的关键一步。

不要一开始就陷入“多模态”“Agent编排”之类的高级话题。这些是在基础能力之上的组合应用,基础没夯实很容易越学越虚。我认识的不少朋友都是从“想做一个Agent框架”开始学,结果被各种概念绕晕,转而回来补最基础的ChatClient调用。先学会走,再学会跑,这个顺序适用于绝大多数框架学习。

5.2 高频问题排查清单

结合自己的实践,我整理了一份排查表,很多问题看起来复杂,实际上原因就那几个:

现象可能原因处理方式
Maven依赖拉不下来使用了未发布到Maven Central的版本改用GA版本,或配置Spring官方仓库
ChatClient注入失败Spring Boot版本与Spring AI不匹配统一升级BOM,对齐Spring Boot版本
启动正常但请求失败Ollama服务未启动或地址配置错误检查base-url,测试Ollama接口连通性
流式接口返回乱码缺少字符编码声明在produces中明确charset=UTF-8
返回内容不是JSON模型指令遵循能力弱或提示词约束不足使用JSON模式,或在提示词中给出示例格式
向量检索结果不相关索引与查询Embedding模型不一致检查两侧EmbeddingModel配置并统一
函数一直没被调用函数描述不清晰或模型不支持工具调用加强@Description,更换兼容模型

排查时要有一个心态:Spring AI本质上还是在帮你构造HTTP请求和解析模型响应,所以你完全可以顺着“请求长什么样、响应长什么样”这条线索去追问题,不用把它当黑盒。打开日志里的HTTP调用细节,或者手动用curl模拟一次接口请求,往往比在代码里加日志更快。

5.3 后续我准备继续深挖的方向

学完这套基础链路后,我给自己列了接下来要补的清单:一是多模型切换的工程实践,因为真正上生产时不太可能只绑定一家模型供应商;二是模型评估,现在的RAG链路到底检索得好不好,需要数据支撑而不是凭感觉;三是系统集成安全性,比如用户输入中的越权指令、提示词注入这类问题,在框架层和业务层应该怎么双保险。

最后再分享一个小技巧:学习Spring AI时,经常打开自动配置报告看看Spring Boot帮我们装配了哪些Bean,理解它在背后做了哪些推断。我在自己项目里把这些推断逻辑写注释记录下来后,再遇到“为什么这个配置没生效”的问题,基本都能一眼看出来。这种追踪机制的理解,比背几个API更值得花时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询