☰
Spring AI 接入 DeepSeek 实战:从环境配置到工具调用
2026/10/2 9:06:52 网站建设 项目流程

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: 60s

read-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 UnauthorizedAPI Key 配错或未生效检查环境变量;确认 key 没有多余空格;DeepSeek 控制台确认账户余额不为 0
400 Invalid model模型名写错,比如deepseek-v3改成官方文档里的deepseek-chat或deepseek-reasoner
404 Not Foundbase-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 对话能力,或者想在现有系统上快速试水大模型,按这篇指南的路线走,应该能避开我踩过的那些坑。

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

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

立即咨询