1. 时代背景:为什么是 Spring AI + DeepSeek?
2025 年是大模型落地的元年。DeepSeek 凭借极高的性价比和出色的推理能力,成为了国内开发者的首选。然而,对于习惯了 Spring 生态的 Java 程序员来说,如何优雅地接入大模型,而不是到处写 HttpClient 拼 JSON,成了一个现实问题。
Spring AI 应运而生。它是 Spring 官方推出的 AI 框架,旨在统一不同 AI 厂商的接口。更重要的是,它完美适配了 Spring Boot 3.x 和 响应式编程(WebFlux),这正是构建高并发 AI 应用的基石。
本文将带你通过"工程化"的手段,集成 DeepSeek,并重点解决在响应式环境下的 线程阻塞 报错问题。
2. 核心架构设计
在构建一个真正的 AI 应用时,我们不能简单地将 Prompt 写死在代码里。下表展示了本文推荐的架构演进:
|---------------|---------------|------------------------|
| 维度 | 传统写法 (初级) | 工业级写法 (本文推荐) |
| Prompt 管理 | 硬编码在 String 中 | 外部 .st 资源文件,逻辑与内容解耦 |
| 通信模型 | 阻塞式 call() | 非阻塞流式 stream() (SSE) |
| 参数校验 | 无或简单校验 | 严格的响应式校验与错误捕获 |
| 线程模型 | Tomcat 阻塞线程 | Reactor 非阻塞线程池 (Netty) |
3. 环境准备与项目配置
3.1 基础版本要求
-
JDK: 17 或 21 (Spring Boot 3.x 的基石)
-
Maven: 3.8+
-
DeepSeek API Key : 从 DeepSeek 开放平台 获取。
3.2 依赖管理 (pom.xml)
Spring AI 目前处于快速迭代期,建议使用最新的里程碑版本。由于 DeepSeek API 与 OpenAI 协议 100% 兼容,我们直接使用 OpenAI 的 Starter。
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>1.0.0-M1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- WebFlux 响应式服务器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<!-- Spring AI OpenAI Starter (兼容 DeepSeek) -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
<!-- Lombok & Log -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
3.3 配置文件 (application.yml)
千万不要把 API Key 直接上传到 Git,建议使用环境变量。
spring:
ai:
openai:
api-key: ${DEEPSEEK_API_KEY}
base-url: https://api.deepseek.com
chat:
options:
# 使用 deepseek-chat 或 deepseek-reasoner
model: deepseek-chat
temperature: 0.7
4. 深度进阶:Prompt 工程化解耦
在生产环境下,Prompt 往往需要频繁调优。如果写在代码里,每次修改都要重新打包发布。
4.1 创建 Prompt 资源文件
在 src/main/resources/prompts/ 下创建 code-expert.st。.st 文件遵循 StringTemplate 语法。
# 角色
你是一名资深的 Java 架构师,擅长 Spring Boot 和微服务架构。
# 任务
请针对用户提供的技术主题 {topic},完成以下任务:
1. 深入浅出地解释该技术的核心底层原理;
2. 给出一段符合阿里巴巴 Java 开发规范的代码示例;
3. 列出 3 个在生产环境下常见的"坑"以及避坑方案。
# 约束
请使用 Markdown 格式输出,并确保代码部分有详细注释。
5. 核心代码实现:ChatController
针对你遇到的 ResourceAccessException (block() 报错),下方的代码给出了最完美的响应式解决方案。
@RestController
@RequestMapping("/ai")
@Slf4j
public class ChatController {
private final ChatClient chatClient;
// 注入外部 Prompt 资源文件
@Value("classpath:/prompts/code-expert.st")
private Resource expertPromptResource;
/**
* Spring AI 推荐使用 ChatClient.Builder 来构建客户端
* 它会自动根据 application.yml 的配置进行初始化
*/
public ChatController(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
/**
* 【方案一:普通对话】
* 痛点:WebFlux 环境下禁止阻塞。
* 解决:通过 stream() 异步收集结果,避免 block()。
*/
@GetMapping("/chat")
public Mono<String> chat(@RequestParam("message") String message) {
log.info("接收到普通对话请求: {}", message);
return chatClient.prompt()
.user(message)
.stream() // 采用流式获取,确保不阻塞 I/O 线程
.content()
.collectList()
.map(parts -> String.join("", parts))
.doOnError(e -> log.error("对话发生异常", e));
}
/**
* 【方案二:流式对话(SSE)】
* 场景:类似 ChatGPT 逐字显示。
* 优点:极高的响应速度,用户体验极佳。
*/
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> chatStream(@RequestParam("message") String message) {
return chatClient.prompt()
.user(message)
.stream()
.content();
}
/**
* 【方案三:专家模式 - 外部 Prompt + 严格异常处理】
* 重点:展示了如何动态填充外部模板并处理流式异常。
*/
@GetMapping(value = "/expert", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> askExpert(@RequestParam String topic) {
// 1. 严格的参数校验
if (topic == null || topic.trim().isEmpty()) {
return Flux.just("警告:问答主题不能为空!");
}
try {
// 2. 加载并填充提示词模板
PromptTemplate template = new PromptTemplate(expertPromptResource);
Map<String, Object> model = Map.of("topic", topic.trim());
Prompt prompt = template.create(model);
log.info("专家模式启动,正在生成关于 [{}] 的深度分析...", topic);
// 3. 核心:返回非阻塞响应流
return chatClient.prompt(prompt)
.stream()
.content()
.doOnNext(part -> log.debug("Receiving chunk..."))
.onErrorResume(e -> {
log.error("DeepSeek 调用失败: {}", e.getMessage());
return Flux.just("【系统异常】AI 暂时掉线了,请稍后再试。详情:" + e.getMessage());
});
} catch (Exception e) {
log.error("Prompt 模板解析异常", e);
return Flux.just("提示词解析失败,请检查配置文件。");
}
}
}
6. 技术内幕:为什么会报 block() 错误?
你在报错中提到的:
block()/blockFirst()/blockLast() are blocking, which is not supported in thread reactor-http-nio-3
深度解析:
-
Reactor 线程模型:WebFlux 运行在 Netty 之上,它的 NIO 线程池(如 reactor-http-nio-3)数量非常少(通常等于 CPU 核心数)。
-
禁令:为了保证高吞吐,Reactor 严禁在这些线程中执行任何"阻塞"操作(比如等待网络返回、查询数据库)。
-
Spring AI 的特殊性:chatClient.call() 是同步阻塞的,它会强行霸占 NIO 线程。Spring 框架检测到这一行为后,为了防止系统卡死,会主动抛出异常"自杀"。
-
修复策略:使用 .stream() 返回 Flux。流式操作是异步的,当 DeepSeek 还没返回数据时,NIO 线程可以去干别的事,等数据到了再回来处理。
7. 生产环境避坑指南
-
超时时间设置:AI 响应通常很慢。在 WebFlux 中,确保你的 WebClient 或相关配置中有足够的超时时间,否则会频繁触发 504 错误。
-
API 额度监控:DeepSeek 可能会因为欠费或达到限制而返回 429 错误。在 onErrorResume 中一定要做好降级逻辑。
-
敏感词过滤:在将用户输入送往大模型之前,建议通过接入公司内部的敏感词库做一层逻辑过滤,防止 Prompt 注入攻击。
-
资源解耦:像本文一样,坚持将 .st 文件与代码分离,这会让你在处理复杂 Prompt 时从容得多。
8. 总结
Spring AI 不仅仅是简单的接口封装,它通过响应式支持、Prompt 模板化管理,为 Java 开发者铺平了通往 AI 应用的道路。
通过本文的实战,我们不仅实现了 DeepSeek 的无缝接入 ,还解决了 WebFlux 线程阻塞 这一核心痛点。希望这篇文章能帮你构建出更加健壮的 Java AI 应用!
博主注:
如果觉得文章有用,欢迎 点赞、收藏、关注 !
完整代码已上传至 Gitee:https://gitee.com/zhang-jinlong1/spring-ai-deepseek-demo.git
如有疑问,评论区见!