
Java 生态里做 AI 应用过去一年最明显的变化就是不用再绕道 Python 了。Spring AI 把大模型调用、提示词组装、向量检索这些能力直接做成了 Spring Boot 风格的 Starter 和 Bean写起来跟平时注入一个 JdbcTemplate 没什么本质区别。这篇内容面向的是有 Java 和 Spring Boot 基础、但还没真正跑通过一个 AI 应用的开发者我会从零把一个可运行的最小项目搭出来顺带把 ChatClient、Prompt、配置项这些核心概念讲透最后再聊聊从 Demo 走向可用系统时容易踩的坑。整篇的代码都是可以直接复制运行的环境以 Spring Boot 3.x 加 Spring AI 1.0 系列为基准。1. 为什么 Java 开发者值得认真对待 Spring AI1.1 从调个接口到框架化的转变很多人第一次接大模型做法是直接用 HttpClient 拼一个 JSON 发出去拿到响应再手动解析。这个方式在验证阶段没问题但只要项目稍微复杂一点问题就全冒出来了API Key 散落在各个类里、重试逻辑每个调用点写一遍、流式输出要自己处理 SSE、多轮对话的上下文要手动维护、换个模型厂商就得改一遍代码。这些活儿本质上都是重复劳动而 Spring AI 要解决的正是这部分。它的定位可以类比成当年 Spring 对 JDBC 做的事。JDBC 本身能用但样板代码太多Spring 用 JdbcTemplate 把连接管理、异常转换、资源释放都封装掉了你只需要关心 SQL 和结果映射。Spring AI 对聊天模型做的是同一件事把 HTTP 通信、鉴权、重试、序列化、流式解析全部收进框架你只需要关心我要问什么和我拿到什么。这个抽象带来的直接好处是可移植性。Spring AI 定义了一套统一的ChatModel、EmbeddingModel、ChatClient接口底层可以对接不同的模型服务。今天用某家的模型明天想换另一家理论上只需要改配置文件和依赖业务代码基本不动。对于企业项目来说这一点比少写几行代码重要得多因为它意味着技术选型不会被单一供应商锁死。1.2 Spring AI 的核心抽象一览在动手之前先把几个关键角色认清楚后面写代码时就不会迷糊。组件职责类比ChatModel底层模型通信接口负责发请求收响应类似DataSourceChatClient面向开发者的高层门面链式 API类似JdbcTemplatePrompt封装消息列表和模型选项类似一条完整的 SQL 语句Message单条消息区分角色系统/用户/助手类似 SQL 里的一个子句Advisor拦截和增强请求/响应类似 Servlet FilterChatMemory维护多轮对话历史类似 HttpSession理解这张表的关键在于分清层次ChatModel是发动机ChatClient是方向盘。日常开发 90% 的时间你只跟ChatClient打交道只有在需要做深度定制比如自己实现一个模型适配器时才会下沉到ChatModel。1.3 版本与依赖的现实考量Spring AI 目前迭代很快1.0 系列已经进入相对稳定的阶段。这里有个经验不要盲目追最新快照版本。快照版的 API 可能一周一变你今天写的代码下周编译不过排查起来非常浪费时间。建议锁定一个正式的里程碑或 GA 版本等业务跑稳了再考虑升级。另一个现实问题是 JDK 版本。Spring Boot 3.x 要求 JDK 17 起步Spring AI 也继承了这个要求。如果你手上还有 JDK 8 的老项目那基本没法直接引入只能新起一个服务。这一点在做技术方案评审时一定要提前说清楚避免做到一半发现环境不满足。2. 环境搭建从空目录到能跑通的最小工程2.1 用 Spring Initializr 生成骨架最省事的方式是走 Spring Initializr。选 Maven 或 Gradle 都行我这边用 Maven 演示。关键配置项ProjectMavenLanguageJavaSpring Boot3.3.x 或更高Java17 或 21Dependencies先只勾 Spring WebSpring AI 的依赖我们手动加这样能看清到底引入了什么生成后解压用 IDEA 打开。项目结构就是标准的 Spring Boot 布局src/main/java下是主类src/main/resources下是配置文件。2.2 手动引入 Spring AI 依赖打开pom.xml在dependencies里加上 Spring AI 的 BOM 和具体 Starter。用 BOM 的好处是统一管理版本后面加别的模块不用一个个写版本号。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.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-openai-spring-boot-starter/artifactId /dependency /dependencies这里用的是 OpenAI 兼容的 Starter因为市面上大量模型服务都提供 OpenAI 兼容接口换 base-url 就能对接通用性最好。如果你用的是别的模型服务把 artifactId 换成对应的 Starter 即可比如某些云厂商会提供自己的 Spring AI Starter。注意Spring AI 的仓库地址有时不在 Maven 中央仓库如果拉不到依赖需要在pom.xml里额外配置 Spring 的里程碑仓库。这一步经常被忽略导致依赖找不到的报错。2.3 配置文件里那几个必须填的项application.yml里至少要配三样东西API Key、base-url、模型名。spring: ai: openai: api-key: ${AI_API_KEY} base-url: https://api.example.com chat: options: model: gpt-4o-mini temperature: 0.7几个细节值得展开说。API Key 千万不要硬编码在配置文件里提交到代码仓库用环境变量注入是最基本的做法上面${AI_API_KEY}就是读环境变量。base-url指向模型服务的地址不同服务商不一样填错会直接报 401 或 404。temperature控制输出的随机性0 到 2 之间写代码辅助类应用建议调低到 0.2 左右创意类应用可以调到 1.0 以上。model这个字段要填服务商实际支持的模型标识填错了会返回模型不存在。我建议第一次配置时先用服务商文档里明确列出的模型名跑通之后再换。2.4 验证依赖是否生效在写业务代码前先做个最小验证启动类能不能正常起来。如果启动时报No qualifying bean of type ChatModel说明 Starter 没被正确加载八成是依赖坐标写错了或者仓库没配。如果启动成功但调用时报鉴权错误那就是 Key 或 base-url 的问题。把这两类问题分开定位能省很多时间。3. ChatClient把大模型调用写成一行链式代码3.1 自动配置帮你做了什么引入 Starter 之后Spring Boot 的自动配置会帮你创建好ChatModel和ChatClient.Builder这两个 Bean。你不需要自己 new直接在需要的地方注入就行。这是 Spring 生态一贯的风格理解这一点后面所有代码就顺理成章了。ChatClient.Builder是原型 Bean每次注入拿到的是新实例所以你可以放心地基于它构建多个不同配置的ChatClient。比如一个用于客服问答低温度、带记忆一个用于文案生成高温度、无记忆互不干扰。3.2 构建一个可复用的 ChatClient推荐的做法是在配置类里统一构建而不是在每个 Service 里各建各的。Configuration public class AiConfig { Bean public ChatClient chatClient(ChatClient.Builder builder) { return builder .defaultSystem(你是一个严谨的 Java 技术助手回答要准确、简洁代码示例要能直接运行。) .build(); } }这里用defaultSystem设定了系统提示词相当于给模型定了一个人设。系统提示词的作用非常关键它决定了模型回答的风格和边界。写系统提示词有个心得把约束写具体别写空话。回答要准确这种话模型基本无感但代码示例必须包含 import 语句这种具体要求模型是能遵守的。3.3 三种调用方式的实际差异ChatClient提供了几种调用姿势用哪个取决于场景。// 方式一最简单的同步调用返回字符串 String answer chatClient.prompt() .user(用一句话解释什么是依赖注入) .call() .content(); // 方式二返回结构化对象 record JavaTip(String title, String content) {} JavaTip tip chatClient.prompt() .user(给我一条 Java 性能优化建议) .call() .entity(JavaTip.class); // 方式三流式输出 FluxString stream chatClient.prompt() .user(讲讲 Spring 的 Bean 生命周期) .stream() .content();方式一的content()直接拿字符串适合大多数简单场景。方式二的entity()是 Spring AI 很实用的一个能力它会把模型返回的文本自动映射成你定义的 Java 对象底层靠的是把类结构描述给模型让它按格式输出 JSON。这个能力做数据抽取类应用特别香比如从一段非结构化文本里抽出姓名、电话、地址。方式三返回FluxString是响应式流适合做打字机效果的聊天界面。注意用流式时要把 Web 依赖换成 WebFlux或者用 SseEmitter 做桥接否则返回类型对不上。3.4 提示词模板别用字符串拼接新手最容易犯的错是用拼提示词。这样写不仅难维护还容易出注入问题。Spring AI 提供了模板机制String answer chatClient.prompt() .user(u - u.text(请把下面这段代码翻译成 Kotlin\n{code}) .param(code, javaCode)) .call() .content();模板里的{code}是占位符通过param传值。这样做的好处是提示词和业务数据分离提示词可以抽到配置文件或数据库里改提示词不用重新编译。对于需要频繁调优提示词的项目这个设计能省大量时间。提示如果传入的内容里本身含有花括号可能会和占位符语法冲突这时需要做转义处理或者改用别的分隔符。4. Prompt 与 Message理解模型眼里的对话4.1 消息角色的分工大模型 API 本质上是接收一个消息列表每条消息带一个角色。Spring AI 把这些角色抽象成了SystemMessage、UserMessage、AssistantMessage等类型。SystemMessage系统指令设定模型的行为边界通常放最前面UserMessage用户输入AssistantMessage模型之前的回复用于多轮对话时回填历史理解这个结构很重要因为多轮对话的本质就是把历史消息一起发过去。模型本身是无状态的它不记得你上一句说了什么所谓记忆是应用层每次把历史拼进去实现的。4.2 手动组装一个多消息 PromptPrompt prompt new Prompt(List.of( new SystemMessage(你是一个 SQL 优化专家。), new UserMessage(这条 SQL 很慢select * from orders where date(create_time) 2024-01-01), new AssistantMessage(问题在于对字段使用了函数导致索引失效。), new UserMessage(那应该怎么改) )); ChatResponse response chatModel.call(prompt);这个例子展示了如何手动控制对话历史。实际项目里历史消息通常由ChatMemory自动管理不需要你手动拼。但理解底层结构在排查为什么模型答非所问时非常有用——很多时候就是因为历史消息没传对或者系统提示词被后面的消息覆盖了。4.3 ChatMemory多轮对话的记忆管理Bean public ChatMemory chatMemory() { return new InMemoryChatMemory(); } Bean public ChatClient chatClient(ChatClient.Builder builder, ChatMemory memory) { return builder .defaultAdvisors(new MessageChatMemoryAdvisor(memory)) .build(); }InMemoryChatMemory把对话历史存在内存里重启就没了适合开发阶段。生产环境要换成基于 Redis 或数据库的实现否则多实例部署时用户会失忆。这里有个坑内存版默认按会话 ID 隔离如果你不传会话 ID所有用户会共用一份历史那就乱套了。会话 ID 一般用用户 ID 或前端生成的 UUID。4.4 结构化输出背后的原理前面提到的entity()方法原理是 Spring AI 在请求里附加了一段格式说明告诉模型请按这个 JSON Schema 输出。模型返回文本后框架再用 Jackson 反序列化成对象。这个机制不是百分百可靠的。模型偶尔会多输出一段解释文字导致 JSON 解析失败。应对办法有两个一是把temperature调低减少模型的自由发挥二是在系统提示词里明确要求只输出 JSON不要任何额外说明。如果对稳定性要求极高可以加上重试逻辑解析失败就重新请求一次。5. 从 Demo 到可用那些文档里不会写的坑5.1 超时与重试默认配置往往不够用大模型响应慢是常态尤其是生成长文本时十几秒甚至几十秒都正常。Spring AI 底层用的 HTTP 客户端有默认超时如果不调整长回答很容易被掐断。建议在配置里显式设置spring: ai: openai: chat: options: timeout: 60s重试也要谨慎。模型调用不是幂等的盲目重试可能产生重复计费。我的做法是只对网络类异常连接超时、5xx重试对 4xx参数错误、鉴权失败直接失败重试也没用。5.2 Token 成本看不见的账单每次调用都消耗 Token输入和输出都算钱。一个容易被忽略的点是系统提示词和历史消息每次都算输入 Token。如果你的系统提示词写了两千字每轮对话都带上成本会迅速累积。优化思路有几个系统提示词精简到必要信息历史消息做窗口截断只保留最近 N 轮对长文档先做摘要再喂给模型。这些策略在项目初期就该考虑等账单出来了再改就晚了。5.3 提示词被拦截内容审核的现实实际调用中可能遇到请求被服务商拦截的情况返回类似内容不合规的错误。这通常是因为输入里包含了敏感词或触发了服务商的安全策略。应对方式是在应用层做输入预处理过滤明显违规内容对拦截错误做友好提示而不是把原始错误抛给用户保留日志便于排查是哪个环节触发的。5.4 并发与限流别把服务商打挂默认情况下你的应用可能瞬间发出大量请求触发服务商的速率限制返回 429。生产环境必须做限流可以用 Resilience4j 或 Sentinel 在调用层加令牌桶。同时要设置合理的并发上限别让一个批量任务把配额吃光影响其他功能。5.5 日志与可观测性AI 应用的调试比传统应用难因为输出是不确定的。建议把每次请求的提示词、响应、耗时、Token 用量都记下来。Spring AI 提供了 Advisor 机制可以写一个自定义 Advisor 统一记录这些信息不用在每个调用点重复写。这些日志在排查为什么这次回答质量差时是唯一的线索。6. 一个完整可运行的小例子把前面的东西串起来做一个Java 面试题解答助手。用户输入一个问题返回结构化的解答。RestController RequestMapping(/api/ai) public class InterviewController { private final ChatClient chatClient; public InterviewController(ChatClient chatClient) { this.chatClient chatClient; } record Answer(String question, String keyPoints, String sampleCode) {} PostMapping(/ask) public Answer ask(RequestBody MapString, String body) { String question body.get(question); return chatClient.prompt() .user(u - u.text( 请回答下面的 Java 面试题要求 1. keyPoints 用分点列出核心考点 2. sampleCode 给出可运行的示例代码 题目{q} ).param(q, question)) .call() .entity(Answer.class); } }启动后用 curl 测一下curl -X POST http://localhost:8080/api/ai/ask \ -H Content-Type: application/json \ -d {question:HashMap 的扩容机制是怎样的}如果返回的 JSON 里三个字段都填好了说明整条链路通了。如果sampleCode是空的多半是提示词里对代码的要求不够明确回去把要求写具体一点。这个例子里有几个设计选择值得说明。用record定义返回结构是因为它天然适合做不可变的数据载体Jackson 也能直接反序列化。把提示词写在方法里而不是配置文件是为了演示方便真实项目建议抽出去。用Map接收请求体是为了简化正式项目应该定义专门的请求 DTO 并加校验注解。7. 接下来可以往哪些方向走跑通最小例子之后Spring AI 还有几块能力值得继续深入。RAG检索增强生成是最实用的一个方向思路是把企业内部的文档向量化存进向量库用户提问时先检索相关片段再连同问题一起发给模型。这样模型就能回答它训练数据里没有的私有知识。Spring AI 提供了VectorStore抽象和对应的 Advisor接入流程和聊天调用一样顺滑。工具调用Function Calling是另一个方向。你可以把 Java 方法注册成工具模型在需要时会主动调用这些方法比如查数据库、调外部 API。这让 AI 应用从只会聊天变成能干活。Spring AI 用Tool注解标记方法注册到 ChatClient 即可。可观测性方面Spring AI 支持把调用指标接入 Micrometer配合 Prometheus 和 Grafana 能看到调用量、延迟、错误率这些指标。对于要上生产的系统这块不能省。我个人在实际项目里的体会是Spring AI 把门槛降得很低跑通 Demo 可能只要半小时但真正决定项目成败的不是框架本身而是提示词设计、成本控制、异常处理这些脏活。框架帮你省掉的是重复劳动省不掉的是对业务的理解和对边界的把控。建议新手先把一个真实的小需求做完整从输入校验到错误提示到日志记录都走一遍比看十篇教程都管用。