1. 项目概述与方案选型
1.1 为什么要把 DeepSeek 接进 Spring AI
先说说我自己的经历。去年年底接手一个餐饮 SaaS 项目,老板要求给商家后台加一个智能助手,能根据菜品销量、库存、门店评价自动生成经营建议。供应商报价一出,我们就放弃了大厂云服务,理由很简单:按 token 计费的中型商用模型,一个月光对话成本就能吃掉小半台服务器钱。
后来我们决定自研接入。当时团队里摆着两条路:一是在业务代码里直接用 HTTP 调用各家大模型的 API,二是引入 Spring AI 这种框架做统一抽象。我毫不犹豫选了后者。原因很实际——项目本身是 Spring Boot 技术栈,业务代码里有大量的 Service、Repository 和消息队列,如果直接裸调 API,后续切换模型、加提示词缓存、做流式输出,全部要自己造轮子。Spring AI 的好处恰恰是把“对话”这件事抽象成了 Spring 风格的工具,它提供ChatClient、ChatModel、Prompt这一套接口,和数据源操作类似,你换模型供应商只需要改配置和依赖,业务逻辑基本不用动。
至于为什么选 DeepSeek,除了成本因素(当时 DeepSeek 的 API 价格确实比 GPT 系列低一大截),还有一个很关键的点:DeepSeek 的 API 接口兼容 OpenAI 的格式。这意味着 Spring AI 的 OpenAI 适配器可以几乎“零成本”地指向 DeepSeek 使用,不需要等待官方专门出适配包,也不用自己撸一个ChatModel实现类。这对我们这种需要快速落地的团队来说,太重要了。
当然,市面上也有 LangChain4j 这类 Java 方向的 AI 框架,它功能也很全。不过我们团队的 Spring 功底更深,Spring AI 从 1.0 开始 API 稳定,而且 Wiki 里对模型接入、工具调用、结构化输出都有现成的解决方案,后续维护压力小。这篇实战指南就是基于我们在 Spring Boot 3.3 项目里把 DeepSeek 聊起来、跑起来、用起来的完整记录。
1.2 主流整合方案对比
在正式动手之前,我把目前 Java 生态里能用的几种接入方式拉出来做了个对比,方便你自己判断怎么选。
| 方案 | 接入成本 | 模型切换灵活性 | 流式/工具调用支持 | 适合场景 |
|---|---|---|---|---|
| 裸调 DeepSeek HTTP API | 低 | 低,需自己改装 | 需手写 SSE 解析 | 一次性小工具、脚本 |
| Spring AI OpenAI 兼容适配 | 中 | 高,配置文件切换 | 原生支持 | 常规 Web 服务、需要功能扩展 |
| Spring AI Alibaba | 中高 | 中 | 支持 | 已经有 Spring Cloud Alibaba 体系的团队 |
| LangChain4j 自定义接入 | 中高 | 中 | 支持 | 对 Agent、复杂 Chain 有需求的团队 |
如果你只是写个三五天的 Demo,裸调 API 没问题。但如果是要放进生产环境、和 Spring Boot 的事务、异步、缓存体系融合,我建议直接上 Spring AI。这篇指南后面所有内容,都是基于 Spring AI 1.0.0 + Spring Boot 3.3 这套组合来讲的。
注意:Spring AI 版本更新很快,不同版本的包名和配置项可能略有差异。请以你使用的具体版本官方文档为准,我在下文里会标注我实测用的版本。
2. 环境准备与依赖配置
2.1 版本选型与 Maven 依赖引入
我们项目的基础环境是这样的:
- JDK 17(Spring Boot 3.3 要求最低 JDK 17)
- Spring Boot 3.3.4
- Spring AI 1.0.0
- Maven 3.9+
如果你的 Spring Boot 版本不是 3.3.x,也没关系,只要保证 Spring Boot 和 Spring AI 的版本兼容即可。Spring AI 的 release 版本对 Spring Boot 有对应的适配矩阵,建议直接去 Spring AI 官网的版本页面对照一下。
Maven 依赖方面,核心就两个:
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-openai-spring-boot-starter</artifactId> <version>1.0.0</version> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-chat-client</artifactId> <version>1.0.0</version> </dependency>这里解释一下为什么用spring-ai-openai这个 starter。因为 DeepSeek 的 API 是兼容 OpenAI 格式的,Spring AI 官方对 OpenAI 协议的适配最完善,包括聊天补全、流式响应、Embedding、Function Calling 都做了完整的封装。我们通过配置把 base-url 指向 DeepSeek 的接口地址,就能让 Spring AI 以为自己在和 OpenAI 对话,实际上每次请求都发给了 DeepSeek。这个思路在社区里是通用做法,并不是什么 hack。
2.2 配置文件里的关键参数
依赖引入之后,接下去就是配置文件。DeepSeek 官方 API 的 Base URL 有两个:https://api.deepseek.com和https://api.deepseek.com/v1,实测两者都可以用。官方文档里推荐用https://api.deepseek.com,不过我实际测试下来,Spring AI 的 OpenAI 客户端会自动拼接/chat/completions路径,所以无论配哪一个,只要 DeepSeek 那边路由正确就行。我在生产中用的是https://api.deepseek.com。
下面是完整的application.yml配置:
spring: ai: openai: api-key: ${DEEPSEEK_API_KEY} base-url: https://api.deepseek.com chat: options: model: deepseek-chat temperature: 0.7 max-tokens: 2048这里有几个坑要提醒一下:
第一,api-key别硬编码。我们在开发环境用本地.env,测试和生产用配置中心或环境变量,绝对不要提交到 Git。这点看起来是老生常谈,但我见过太多人因为 key 泄露被刷爆账单。
第二,model参数填deepseek-chat还是deepseek-reasoner,决定了你到底用哪个模型。deepseek-chat对应 DeepSeek-V3 系列,适合通用对话场景,响应快、成本低。deepseek-reasoner对应深度推理系列,适合需要思考链路的复杂问题,但响应时间明显变长,价格也贵。我们餐饮 SaaS 的智能助手用的是deepseek-chat。
第三,base-url和model这两个参数的组合,是这次整合的“命门”,因为 Spring AI 底层是拿 model 字段去请求/chat/completions接口的,如果你配错了模型名,比如写成了deepseek-v3这种不存在的 alias,DeepSeek 会直接返回 400 错误,而且报错信息不太友好。
2.3 Spring AI 的自动配置原理
Spring AI 的 starter 会基于上面的配置自动装配一个OpenAiChatModelBean,这个 Bean 实现了ChatModel接口。也就是说,我们不需要手动 new 任何客户端对象,直接注入ChatModel或者ChatClient.Builder就能用了。
从原理上看,Spring AI 把一次大模型请求分成了三层:
ChatModel:负责和上游 API 做 HTTP 交互,把请求体组装成 OpenAI 协议格式,再解析响应。Prompt:封装用户消息、系统消息、参数配置。ChatClient:给开发者提供的门面,提供了链式调用的 DSL。
这种分层带来的好处是,你的业务代码只依赖ChatClient,后续如果 DeepSeek 不可用,你换一个模型供应商,只需要换依赖和配置,业务代码一行不用改。
3. 跑通第一个对话接口
3.1 最简实现:ChatClient DSL 调用
配置完成之后,代码里实现一个最简对话只需要几十行。我直接贴我们项目里第一个 Demo 的代码:
@Service public class DeepSeekChatService { private final ChatClient chatClient; public DeepSeekChatService(ChatClient.Builder builder) { this.chatClient = builder.build(); } public String chat(String message) { return chatClient.prompt() .user(message) .call() .content(); } }在 Controller 里暴露一个接口:
@RestController @RequestMapping("/api/ai") public class AiController { private final DeepSeekChatService chatService; public AiController(DeepSeekChatService chatService) { this.chatService = chatService; } @PostMapping("/chat") public Result<String> chat(@RequestBody ChatRequest request) { String reply = chatService.chat(request.getMessage()); return Result.success(reply); } }前端POST /api/ai/chat,body 里传{"message": "你好"},就能拿到 DeepSeek 的回复了。
这里我特别想说一下ChatClient.Builder的注入方式。在 Spring AI 1.0 里,ChatClient.Builder是由自动配置提供的,但你每次调用builder.build()时,可以传入自己想要的系统提示词或者默认参数,比如:
ChatClient chatClient = builder .defaultSystem("你是一个餐饮行业运营助手,回答尽量简洁、专业") .defaultOptions(OpenAiChatOptions.builder() .withTemperature(0.5) .build()) .build();这样构建出来的 client,每个请求都会自动带系统提示词,不用每次调用都重复写。我们生产环境就是这么干的——给智能助手、差评分析、菜品推荐分别创建了不同 system prompt 的 ChatClient 实例,互不干扰。
3.2 流式响应:打字机效果的实现
如果只是“请求-响应”模式,用户要等大模型把整段话生成完才能看到结果。DeepSeek 生成几百字可能要好几秒,这个等待体验太差了。所以我们第二个需求就是流式输出。
Spring AI 对流式响应的支持很成熟,核心方法是.stream():
public Flux<String> chatStream(String message) { return chatClient.prompt() .user(message) .stream() .content(); }Controller 那边用 SSE 往外推:
@PostMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux<String> chatStream(@RequestBody ChatRequest request) { return chatService.chatStream(request.getMessage()); }前端用 EventSource 或 fetch + ReadableStream 就能实现打字机效果。这里提醒一下,SSE 的produces一定要写成TEXT_EVENT_STREAM_VALUE,否则 Spring MVC 不知道怎么处理Flux<String>的返回。
还有一个流式场景必须要注意:超时设置。DeepSeek 的流式响应是逐 token 返回的,通常第一个 token 在 1-2 秒内就会到,但如果你本地网络到 DeepSeek 的延迟很高,或者 API Key 配错了,连接可能会长时间挂起。我们在网关层和 Spring 的WebClient都增加了超时配置:
spring: ai: openai: client: connect-timeout: 10s read-timeout: 60sread-timeout不建议设太短,因为 deepseek-reasoner 这类推理模型思考时间可能比较长,60 秒比较保险。
3.3 结构化输出:让模型返回 JSON 而不是散文
对话功能上线两周后,产品提了一个新需求:让 AI 从用户留言中抽取门店信息,比如店名、联系人、诉求分类,然后直接写入工单系统。这种场景如果让模型返回纯文本,还得写正则去解析,麻烦且不稳定。更好的做法是让 Spring AI 借助“结构化输出”直接把模型回复转场为 Java 对象。
Spring AI 的BeanOutputConverter就是干这个的。用法如下:
public StoreTicket extractTicket(String userMessage) { BeanOutputConverter<StoreTicket> converter = new BeanOutputConverter<>(StoreTicket.class); String content = chatClient.prompt() .user(userMessage) .system(converter.getFormatInstruction()) .call() .content(); return converter.convert(content); }public record StoreTicket( String storeName, String contact, String category, String description) {}这里面的原理是:converter.getFormatInstruction()会生成一段提示词,告诉大模型“你输出的内容必须是一个符合以下 JSON Schema 的对象”,然后 DeepSeek 就会严格按照这个格式输出 JSON。之后converter.convert()负责把 JSON 字符串转成 Java 对象。
实际测试下来,DeepSeek 对中文场景下的 JSON 格式跟随能力很强,我们跑了上千条测试样本,只有极少数情况会出现字段缺失或类型错误。建议你在 convert 失败时加一层降级逻辑,比如解析失败就返回 null 并记录日志,而不是让接口直接 500。
3.4 联网搜索能力
在实际运行中,我们的智能助手经常被问到实时库存在线状态查询。好在 Spring AI 提供了 API 级别的联网搜索能力(Web Search API),可以配置一个工具让模型在回答时先搜索更新信息再回复。使用方式:
Tool webSearchTool = Tool.builder() .name("web_search") .description("搜索最新的公开信息") .inputSchema("{ \"query\": { \"type\": \"string\" } }") .invoke(query -> searchService.search(query)) .build(); ChatClient chatClient = builder.defaultTools(webSearchTool).build();调用时,模型如果认为需要搜索,会自动调用这个工具,然后带着搜索结果继续生成回答。这个功能对餐饮行业尤其有用,比如“最近哪个菜系在本地比较火”,模型就能脱离静态训练数据,给出带时效性的答案。不过要注意,启用 Web Search 会多一次外部请求,响应延迟和成本都会有所上升,建议只在特定场景开启。
4. 进阶实战:多轮对话与工具调用
4.1 多轮对话:别让模型“失忆”
对话系统绕不开多轮记忆。DeepSeek API 本身是无状态的,它不记住你上一轮说了什么。所谓“多轮对话”,其实是你把历史消息都塞回请求里,让模型自己理解前面的上下文。
Spring AI 里处理多轮对话的方式有两种:一种是手动构造历史消息列表,另一种是用ChatMemory。
手动方式很简单:
public String chatWithHistory(String userId, String message) { List<Message> history = messageHistoryService.getHistory(userId); Prompt prompt = new Prompt(List.of( new SystemMessage("你是一个智能助手"), history.toArray(new Message[0]), new UserMessage(message) )); return chatClient.prompt(prompt).call().content(); }这里的messageHistoryService需要你自己实现,可以把历史消息存 Redis 或数据库。有一个细节要注意:UserMessage和AssistantMessage是交替排列的,顺序必须是 user -> assistant -> user -> assistant,如果连续两条 user 消息,模型的理解会变得混乱。
用ChatMemory的方式更 Spring AI 风格。Spring AI 提供了ChatMemory接口和InMemoryChatMemory实现,支持基于会话 ID 存储历史消息:
@Bean public ChatMemory chatMemory() { return new InMemoryChatMemory(); }ChatClient chatClient = ChatClient.builder(chatModel) .defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory)) .build();之后每次调用,advisor 会自动把会话历史拼接到请求里。不过InMemoryChatMemory仅适合单实例开发和测试,多实例部署时必须换成 Redis 或其他分布式存储实现。
提醒:多轮对话会显著增加 token 消耗。DeepSeek 的上下文窗口有 64K,但每次请求都会把全部历史重新发送一遍,历史越长,费用越高、响应越慢。建议只保留最近 10-20 轮消息,或者做一些摘要压缩。我们在生产里是 20 轮 + 超过 8 轮就触发一次摘要重写,成本节省了差不多三成。
4.2 Function Calling:让 AI 替你查库存
餐饮 SaaS 里最常用的功能是 AI 查库存、查订单、查菜品销量。大模型本身不知道你的数据库里有什么,但 Function Calling(工具调用)机制可以让模型在需要的时候“请求”调用你的方法。
Spring AI 1.0 里注册一个工具方法非常简单,用@Tool注解:
@Component public class InventoryTools { @Tool(name = "query_stock", description = "查询指定门店的菜品库存") public String queryStock(String storeId, String dishName) { // 调用你的库存服务 return inventoryService.queryStock(storeId, dishName); } }然后在构建 ChatClient 时把这个工具类传进去:
@Bean ChatClient chatClient(ChatClient.Builder builder, InventoryTools inventoryTools) { return builder .defaultTools(inventoryTools) .build(); }当用户问“宫保鸡丁还有多少库存”时,模型的完整思考过程是这样的:用户问题里包含“查询库存”的意图 → 模型从工具描述中匹配到query_stock→ 模型提取出参数storeId和dishName→ 生成一个工具调用请求 → Spring AI 在服务端帮我们执行这个方法 → 把结果返回给模型 → 模型再把结果组织成自然语言回答。
这里有一个在接入 DeepSeek 时需要特别注意的点:Function Calling 的能力依赖模型本身是否支持。DeepSeek-V3 是支持工具调用的,实测deepseek-chat模型对@Tool方法参数的 JSON Schema 生成和调用都挺稳定。但如果某天你换了一个不支持工具调用的模型,Spring AI 会自动把@Tool当成提示词的一部分发给模型,输出格式就不受控了。所以换模型前一定要确认这一点。
另外,工具方法尽量做到“无副作用”,也就是做查询类操作,别在工具里直接改数据库。因为在大模型的多次重试中,工具方法可能被重复执行,如果里面有写操作,很容易出现脏数据。
4.3 与 Spring Boot 业务体系融合的实践
把 DeepSeek 接进业务项目,不只是搞一个 Controller 调通 API。我们最后是把 AI 能力接到了餐饮 SaaS 的工单模块和报表模块里。
工单模块的做法是:用户提交一条差评留言,系统先把留言送到 DeepSeek,让模型提取门店、菜品、问题分类,然后自动生成工单。整个过程走的是前面说的结构化输出 + 工具调用组合,准确率相当高。
报表模块则利用了 Spring AI 的 prompt 模板能力。系统提示词里带上销量数据、库存数据和近期活动,让模型生成一段经营分析摘要。这个场景我们用的是spring-ai-rag相关的简单思路——先把数据拼到 prompt 里,再让模型总结,没有上太重的向量检索。原因很简单:餐饮数据大多是结构化的,查数据库拿结果比做 RAG 检索划算得多。
String analysisPrompt = """ 门店:{storeName} 本周总销售额:{weeklySales} 热销菜品:{topDishes} 库存预警:{stockWarnings} 请基于以上数据生成一段不超过200字的经营分析摘要,指出问题和建议。 """;用 Spring AI 的PromptTemplate很容易做变量替换:
PromptTemplate template = new PromptTemplate(analysisPrompt, Map.of("storeName", "杭州西湖店", "weeklySales", "128000", ...));这种方式比字符串拼接更安全,也方便维护。如果你后面想把这些模板抽出来放到数据库或配置中心,PromptTemplate也能直接加载模板内容。
5. 常见问题与避坑指南
5.1 API 接入最常见的报错
我把这几个月遇到的坑整理成了表格,按出现频率排的序:
| 错误现象 | 根因 | 解决方案 |
|---|---|---|
401 Unauthorized | API Key 配错或未生效 | 检查环境变量;确认 key 没有多余空格;DeepSeek 控制台确认账户余额不为 0 |
400 Invalid model | 模型名写错,比如deepseek-v3 | 改成官方文档里的deepseek-chat或deepseek-reasoner |
404 Not Found | base-url 路径配置不对 | Spring AI 会自动追加/chat/completions,确认 base-url 写到域名级别即可 |
| 接口超时 | 网络问题或请求内容太长 | 调大 connect/read timeout;压缩历史消息 |
| 返回内容截断 | max-tokens设置太小 | 调大max-tokens,或改用流式响应 |
| 偶尔返回乱码/重复内容 | 温度参数过高 | 把temperature降到 0.5 左右 |
其中 400 那个坑最隐蔽。因为 Spring AI 里配置的model字段,很多人会想当然填产品名deepseek-v3,但 DeepSeek 的 API 目前只认deepseek-chat和deepseek-reasoner这两个 alias。我也是被 400 坑了快半小时,才去官方文档里挨个对参数,最后才发现是模型别名的问题。
5.2 并发与性能调优经验
接入生产环境后,第一个性能问题是连接池。默认情况下,Spring AI 的OpenAiChatModel内部用的是WebClient,底层是 Reactor Netty,默认连接池不大。我们业务高峰期有几十个并发对话请求,连接池不够用就会排队,表现为响应时间明显变长。
解决方法一是加大连接池,在配置文件里调整:
spring: ai: openai: client: max-connections: 100方法二是在业务侧做并发控制,用一个线程池包装 AI 调用:
@Bean public ExecutorService aiExecutor() { return Executors.newFixedThreadPool(20); }AI 调用是 IO 密集型任务,不要把它放进 Tomcat 的工作线程直接跑,最好通过@Async或自定义线程池隔离开,避免 AI 接口变慢拖垮整个 Web 服务。
第二个性能问题是 token 消耗失控。主要是多轮对话历史无限增长导致的。我在 4.1 节已经说过了,这里再强调一遍:一定要对历史消息做截断或摘要,否则用户聊 50 轮之后,每次请求的 token 数是第一轮的十倍,费用跟着线性涨。
还有一个经验是:给不同的业务场景设置不同的max-tokens。像“提取工单信息”这种任务,模型只需要输出一个几十字的 JSON,max-tokens设 500 完全够用,但如果你不设,默认跟着全局 2048 走,虽然在结果没影响,但万一模型抽风输出一大段废话,也是白白烧 token。
5.3 关于模型选择的实测对比
我在项目中把deepseek-chat和deepseek-reasoner都试过一段时间,说说直观感受:
deepseek-chat的响应速度很快,普通问题基本两三秒内就能输出完,价格也便宜,适合绝大多数业务场景。deepseek-reasoner在复杂逻辑推理、代码生成方面的表现更强,但首 token 延迟明显高,而且输出里经常带着长长的思考痕迹(虽然 API 返回的内容里默认不含推理链,但它思考期间会让用户等得很焦躁)。
所以我的建议是:默认场景用deepseek-chat,只有在你确认当前任务确实需要复杂推理(比如生成 SQL、分析多表关联数据、做复杂规则判断)时,再单独创建一个用deepseek-reasoner的 ChatClient。两个实例并存,靠配置切换,非常方便。
另外提一个省钱的小技巧:DeepSeek 的计费在非高峰时段(官方说明的是夜间时段)会有折扣,如果你的业务不是实时客服类型,可以把一些非紧急的 AI 任务(比如批量生成日报、批量分析评论)放到夜里跑,成本能降不少。
6. 从整合到落地:补充几点个人体会
这段算是“免费附赠”的实操心得。如果你跟着前面的步骤把 Spring AI 和 DeepSeek 接通了,基本上已经解决了 80% 的工作。剩下 20% 是那些文档里不会写、但对生产很关键的事。
第一件事,日志一定要打全。我们刚开始调试时,AI 接口返回异常,日志里只有一句“调用 OpenAI API 失败”,根本看不出是请求超时、鉴权失败还是模型名错误。后来我在代码里给 ChatClient 调用加了一层切面,把请求的消息数量、token 估算、响应耗时全部记录下来,排查问题就高效多了。OpenAiChatOptions里还可以开启withStreamUsage(true),拿到流式响应里的 token 用量,方便做成本核算。
第二件事,AI 功能要做好降级方案。大模型 API 再稳定也可能抽风,比如 DeepSeek 高峰期偶发限流。我的做法是:在 AI 服务外面包一层降级逻辑,如果调用失败,先尝试降级到缓存的历史回答,再不行就返回一个预设的话术,比如“目前智能助手暂时不可用,请稍后再试”。不能因为 AI 挂了就让整个工单系统不可用,这在餐饮客户现场是没法接受的。
第三件事,安全过滤别依赖大模型自觉。即便 DeepSeek 本身有内容安全机制,业务方也要有自己的敏感词过滤层,接入之前就把平台上允许的内容规则理清楚,多一层防护没坏处。
我在这个项目上最大的感受是:Spring AI + DeepSeek 这套组合,对于 Java 团队来说,属于“性价比极高”的落地路线。框架替你管好了连接、流式、结构化和工具调用这些脏活,模型方负责压低成本,你要做的就是把 AI 能力和业务场景拼起来。如果你们团队正准备在 Spring Boot 项目里增加 AI 对话能力,或者想在现有系统上快速试水大模型,按这篇指南的路线走,应该能避开我踩过的那些坑。