1. 为什么 Java 开发者现在该认真看一眼 Spring AIJava 生态里做 AI 集成这件事过去两年一直有点尴尬。Python 那边 LangChain、LlamaIndex 玩得风生水起Java 开发者想接个大模型要么自己手写 HTTP 客户端拼 JSON要么在项目里塞一个 Python 微服务做中转维护成本高得离谱。Spring AI 出现之后这个局面算是有了一个官方味道的解法——它把大模型调用抽象成了 Spring 风格的 API跟JdbcTemplate、RestTemplate一个思路用依赖注入和自动配置把底层差异抹平。这篇内容面向的是有 Java 和 Spring Boot 基础、但还没碰过 Spring AI 的开发者。我会从零开始把构建第一个 Java AI 应用这件事拆开讲透为什么这么设计、ChatClient到底怎么用、Prompt 怎么写才不踩坑、配置项怎么填、遇到报错怎么排查。看完你应该能自己跑起来一个能对话、能带上下文、能切换模型的 Spring Boot 应用而不是停留在抄了个 demo 但不知道为什么的状态。需要先说明一点Spring AI 迭代非常快1.0 之前的版本 API 变动频繁网上很多教程的包名和类名已经对不上了。我下面讲的内容以当前主流的 1.0.x 稳定线为准如果你用的是里程碑版本类路径可能略有差异遇到对不上的地方优先看官方仓库的当前文档别硬套老教程。2. 动手之前把 Spring AI 的定位和核心概念理清楚2.1 Spring AI 到底解决了什么问题先打个比方。没有 Spring AI 的时候你调用大模型就像每次做饭都要自己去菜市场挑菜、砍价、洗切——每个厂商的接口格式、鉴权方式、返回结构都不一样OpenAI 一套、通义一套、Ollama 本地又一套。你写一次业务逻辑换模型就得改一遍代码。Spring AI 干的事相当于给你配了个中央厨房。它定义了一套统一的抽象层ChatModel负责底层模型通信ChatClient负责上层对话交互Prompt封装输入ChatResponse封装输出。你面向接口编程换模型只需要换配置和依赖业务代码基本不动。这就是 Spring 一贯的面向抽象、依赖注入哲学在 AI 场景的延伸。它主要覆盖这几块能力同步和流式的对话调用、Prompt 模板化、对话记忆多轮上下文、结构化输出把模型返回映射成 Java 对象、函数调用让模型触发你的 Java 方法、向量库集成和 RAG 检索。对绝大多数业务应用来说前四项就已经能撑起 80% 的场景了。2.2 几个必须搞懂的核心概念ChatModel 与 ChatClient 的分工。ChatModel是底层接口直接对接具体厂商返回的是原始的ChatResponse用起来比较裸。ChatClient是上层门面提供prompt().user(...).call().content()这种链式写法还内置了记忆、模板、默认系统提示等能力。日常开发优先用ChatClient只有在需要精细控制底层参数时才直接碰ChatModel。Prompt 的构成。一个 Prompt 通常包含三部分系统消息System Message设定角色和行为边界、用户消息User Message本次输入、以及可选的助手历史消息Assistant Message多轮对话时带上。很多人第一次用只写用户消息结果模型行为飘忽不定问题就出在没给系统消息定调。对话记忆Chat Memory。大模型本身是无状态的它不记得你上一句说了什么。所谓多轮对话本质是每次请求都把历史消息一起发过去。Spring AI 用ChatMemory帮你管理这个历史窗口避免你手动拼接。但要注意历史越长 token 消耗越大所以它提供了窗口大小限制。结构化输出。让模型返回一段 JSON再自动映射成 Java 的 record 或 POJO这是 Spring AI 很实用的一个能力。底层靠的是在 Prompt 里注入格式约束再配合转换器解析。用好了能省掉大量手工解析字符串的脏活。2.3 环境与依赖准备基础环境就三样JDK 17 或以上Spring Boot 3.x 的硬性要求、Maven 或 Gradle、一个可用的模型服务云端 API 或本地 Ollama 都行。我建议新手先用本地 Ollama 跑通流程不花钱、不依赖网络、调试快等逻辑通了再换云端模型。Maven 里核心依赖是 Spring AI 的 BOM 加具体模型 starter。用 BOM 的好处是统一版本避免各个 starter 版本打架dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version1.0.0/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-ollama/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency /dependencies注意Spring AI 的 starter 命名在 1.0 前后改过。老版本叫spring-ai-ollama-spring-boot-starter新版本统一成了spring-ai-starter-model-ollama这种格式。如果你复制老教程的依赖发现拉不下来八成是命名对不上去中央仓库搜一下当前 artifactId 即可。3. 第一个 AI 应用从配置到跑通对话3.1 配置文件怎么写才不出错application.yml里主要是配模型服务的地址、模型名和参数。以本地 Ollama 为例spring: ai: ollama: base-url: http://localhost:11434 chat: options: model: qwen2.5:7b temperature: 0.7 num-ctx: 4096这里几个参数值得说清楚。temperature控制随机性0 到 2 之间写代码、做抽取这类要稳定的任务调到 0.1~0.3做创意文案可以到 0.8 以上。num-ctx是上下文窗口大小决定了模型一次能看到多少 token设太小会导致长对话被截断设太大又吃内存7B 模型一般 4096 够用。model必须是你本地已经ollama pull下来的模型名写错了启动不报错但调用时会返回模型不存在的错误。如果换成云端服务配置结构类似只是把ollama换成对应厂商的节点鉴权信息通常走api-key。我强烈建议把 key 放到环境变量里别硬编码进 yml 提交到仓库这是最基本的安全习惯。3.2 注入 ChatClient 并发出第一次调用Spring AI 的自动配置会帮你把ChatModel和ChatClient.Builder都注册成 Bean你直接注入就能用。最简写法RestController public class ChatController { private final ChatClient chatClient; public ChatController(ChatClient.Builder builder) { this.chatClient builder .defaultSystem(你是一个严谨的 Java 技术助手回答简洁代码示例优先。) .build(); } GetMapping(/chat) public String chat(RequestParam String message) { return chatClient.prompt() .user(message) .call() .content(); } }启动应用访问/chat?message什么是依赖注入就能拿到模型回复。这段代码里有几个设计点值得琢磨defaultSystem设定了全局角色避免每次调用都重复写系统提示prompt().user().call().content()这条链是 Spring AI 的标准调用姿势call()是同步阻塞content()取出纯文本。如果你想要流式输出把call()换成stream()返回FluxString配合 SSE 就能做打字机效果。3.3 Prompt 模板化别把字符串拼接到处写直接在代码里用拼 Prompt 是新手最常见的坏习惯一旦要改格式就得满项目找。Spring AI 提供了PromptTemplate用占位符管理PromptTemplate template new PromptTemplate( 请用{style}的风格解释下面这个概念控制在{words}字以内 概念{concept} ); Prompt prompt template.create(Map.of( style, 通俗, words, 100, concept, 响应式编程 )); String result chatClient.prompt(prompt).call().content();模板的好处是格式和内容分离改提示词不用动 Java 代码还能把模板放到配置文件或数据库里做动态管理。实际项目里我习惯把常用模板集中到一个prompts目录按业务命名方便版本管理和复用。3.4 加上对话记忆让它记住上下文单轮问答跑通后下一步就是多轮。Spring AI 用ChatMemory管理历史配合MessageChatMemoryAdvisor自动把历史注入每次请求Bean ChatMemory chatMemory() { return MessageWindowChatMemory.builder() .maxMessages(20) .build(); } Bean ChatClient chatClient(ChatClient.Builder builder, ChatMemory memory) { return builder .defaultSystem(你是一个耐心的技术顾问。) .defaultAdvisors(MessageChatMemoryAdvisor.builder(memory).build()) .build(); }maxMessages(20)表示只保留最近 20 条消息超出的自动丢弃。这个值要结合模型上下文窗口和单条消息长度来定设太大容易超 token 限制设太小又记不住关键信息。生产环境里更稳妥的做法是按 token 数而非消息条数来裁剪或者用向量库做长期记忆检索这个后面进阶再展开。提示MessageWindowChatMemory默认是内存实现应用重启历史就没了。多实例部署时每个实例的记忆是独立的用户请求打到不同实例会失忆。要跨实例共享得换成基于 Redis 等外部存储的ChatMemoryRepository实现。4. 结构化输出与函数调用让 AI 真正接入业务4.1 把模型返回映射成 Java 对象让模型返回一段自由文本再自己写正则去解析是件很痛苦的事。Spring AI 的.entity()方法能直接把返回映射成 Java 类型record BookInfo(String title, String author, int year, ListString tags) {} BookInfo info chatClient.prompt() .user(介绍一下《Effective Java》这本书) .call() .entity(BookInfo.class);底层原理是 Spring AI 会根据目标类型生成格式说明注入到 Prompt 里约束模型输出 JSON再用转换器反序列化。这里有个坑模型不一定每次都严格返回合法 JSON尤其是小参数模型。所以生产代码里一定要对解析失败做兜底比如捕获异常后重试一次或者降级返回默认值。另外字段类型尽量用包装类型和List避免模型返回 null 时拆箱报错。4.2 函数调用让模型触发你的 Java 方法函数调用Function Calling是让 AI 从聊天走向干活的关键。思路是你把一个 Java 方法注册给模型模型判断需要时返回一个调用意图Spring AI 帮你执行方法并把结果回传给模型继续推理。Bean Description(根据城市名查询当前天气) FunctionWeatherRequest, WeatherResponse weatherFunction() { return request - weatherService.query(request.city()); }注册后在调用时通过.tools()挂上模型遇到北京今天天气怎么样这类问题就会自动触发你的方法。这里的设计精髓在于模型只负责决定调哪个函数、传什么参数真正的业务逻辑还是你的 Java 代码在跑安全边界清晰。要注意的是函数描述Description写得越清楚模型判断越准含糊的描述会导致它该调不调、不该调乱调。4.3 流式输出与前端配合聊天类应用基本都要打字机效果。Spring AI 的流式接口返回FluxString配合 Spring WebFlux 或 Spring MVC 的 SSE 都能实现GetMapping(value /chat/stream, produces MediaType.TEXT_EVENT_STREAM_VALUE) public FluxString stream(RequestParam String message) { return chatClient.prompt() .user(message) .stream() .content(); }前端用EventSource接收即可。实测下来流式不仅体验好首字延迟也明显更低——用户不用等整段生成完才看到内容。但要注意流式模式下拿不到完整的ChatResponse元数据比如 token 用量统计如果业务需要计费或监控得在流结束时单独处理。5. 常见报错与排查速查实际跑起来新手最容易卡在几个地方。我把高频问题和排查思路整理成表遇到问题先对照现象可能原因排查方向启动报找不到 ChatModel Bean依赖没引对或模型服务未配置检查 starter artifactId 是否为当前版本命名确认 yml 中模型节点存在调用返回 404 或连接拒绝模型服务地址错误或未启动本地 Ollama 确认ollama serve在跑base-url端口是否为 11434提示模型不存在模型名拼写错误或未拉取执行ollama list核对模型名注意带不带 tag返回内容被截断上下文窗口或输出长度限制调大num-ctx检查是否有 max-tokens 限制结构化输出解析失败模型返回非法 JSON换更大参数模型或在 Prompt 中强化格式约束加异常兜底多轮对话失忆记忆未配置或实例不共享确认 Advisor 已挂载多实例场景换外部存储中文乱码编码未统一确认请求和响应均为 UTF-8除了表里的还有两个我踩过的坑值得单独说。一是依赖版本冲突Spring AI 对 Spring Boot 版本有要求混用不兼容版本会出现各种诡异的 Bean 创建失败用 BOM 统一管理能规避大部分问题。二是Prompt 被内容安全策略拦截某些云端服务会对输入做合规检查返回类似prompt 被标记为可能违规的提示。这种情况通常是输入里带了敏感词或特殊符号换个表述、拆解输入往往能解决别急着怀疑代码。注意调试阶段建议把日志级别调到 DEBUGSpring AI 会打印实际发送的 Prompt 和收到的原始响应。很多模型不听话的问题一看实际 Prompt 就明白了——往往是你以为传进去的内容和真正发出去的不一样。6. 一些实操心得和后续扩展方向跑通第一个应用只是起点。我在实际项目里积累了几条经验分享给准备深入的人。第一别迷信大模型能搞定一切。涉及精确计算、强一致性的逻辑老老实实写 Java 代码让模型只做它擅长的语言理解和生成。函数调用就是干这个的——把确定性逻辑留在代码里把模糊判断交给模型。第二Prompt 要当代码管理。版本化、可回滚、有测试。我习惯给关键 Prompt 写几个固定输入和期望输出的用例改 Prompt 后跑一遍避免改了一处崩了另一处。第三成本要提前算。云端模型按 token 计费多轮对话历史越长越贵。合理设置记忆窗口、对长文本做摘要压缩、能缓存的别重复请求这些都是省钱的关键。后续想继续深入可以往这几个方向走接入向量库做 RAG让模型基于你的私有文档回答用spring-ai-alibaba对接国内模型生态把 AI 能力封装成独立的 Agent 服务通过 gRPC 或 HTTP 给其他系统调用。Spring AI 的抽象层设计得比较干净这些扩展基本都是在现有基础上加组件不用推翻重来。最后分享一个小技巧本地开发时把模型响应缓存起来写单元测试时用缓存回放既快又稳定还不用每次测试都真调模型烧钱。这个习惯在 CI 环境里尤其重要。
阅读完成 · 觉得有帮助?