1. 从一个真实需求说起
先聊个具体的场景。我手头有个内部知识库问答系统,之前接的是国外某家大模型厂商的接口,效果还行,但有两个问题始终绕不过去:一是延迟不稳定,尤其是高峰期;二是成本核算起来头疼,按 token 计费的模式在团队内部复盘时经常对不上账。后来团队里有人提议试试 DeepSeek,理由很直接——中文语义理解扎实、定价透明、而且兼容 OpenAI 风格的接口协议,迁移成本低。但真到了动手改代码的阶段,问题就冒出来了:我们项目里已经用了 Spring AI 这套抽象层,总不能为了换模型再把整个对接逻辑推翻重写吧?
这就是这篇文章想解决的问题。Spring AI 作为 Java 生态里专门封装 AI 调用能力的框架,本身提供了一套统一的客户端抽象,理论上换模型只需要换配置。但 DeepSeek 的接口虽然兼容 OpenAI 格式,实际接入时仍然有一些细节需要处理,比如基础 URL 的指向、模型名称的映射、超时参数的调整、以及响应解析的兼容性。这篇实战指南会从一个可运行的最小示例出发,逐步拆解整合步骤,同时把我在真实项目中踩过的坑和排查思路一并整理出来。无论你是刚接触 Spring AI 的初学者,还是已经在生产环境里维护 AI 服务的开发者,这篇文章都值得你花几分钟过一遍。
2. 为什么选择 Spring AI 作为接入层
2.1 Spring AI 在 Java 生态里的定位
先理清一个概念:Spring AI 并不是某个具体模型厂商的 SDK,它更像是一个适配层。类比一下,JDBC 定义了 Java 操作数据库的统一规范,各家数据库驱动实现这套规范,应用层不用关心底层是 MySQL 还是 PostgreSQL。Spring AI 做的事情类似,它定义了 ChatClient、EmbeddingClient 这些抽象接口,而 DeepSeek、OpenAI、通义千问等模型的接入,都被封装成不同的 AutoConfiguration。开发者面向 Spring AI 的接口编程,底层模型是谁,对业务代码来说基本透明。
这个设计带来的直接好处有两个。
一是换模型的成本大幅降低。假设今天你用 DeepSeek 做对话,明天想换成另一个兼容 OpenAI 协议的模型,大多数情况下只需要修改配置文件里的 base-url 和 model 名称,业务代码几乎不用动。二是测试变得方便。Spring AI 允许你轻松替换为mock实现,在单元测试里模拟 AI 响应,不需要真实调用外部接口,既省钱又稳定。
2.2 对比直接调用 HTTP 接口的优劣
有人可能会问,DeepSeek 官方文档里明明给了 HTTP 调用的示例,用 RestTemplate 或者 WebClient 直接请求不就行了?为什么非要引入 Spring AI?
直接调用当然可以,而且对极其简单的场景来说,它反而更直观。但一旦项目里出现这几个需求,直接调用的弊端就暴露了:
- 多模型支持:你不想把代码写死成某一家厂商的格式。
- 结构化输出:希望 AI 返回 JSON 并映射到 Java 对象。
- 流式响应:需要打字机式地逐字输出,同时还要能处理取消、超时等异常情况。
- 可观测性:想统一记录 token 消耗、响应耗时、调用链路。
这些需求如果全部自己实现,工作量并不小。尤其是流式响应,直接用 WebClient 处理 Flux 数据流,对不熟悉响应式编程的同事来说,维护成本会明显上升。Spring AI 把这些细节包装好了,你只需要调用 chatClient.prompt().stream() 就能拿到流式结果,背后如何处理 SSE 协议、如何解析增量数据,框架帮你扛了。
不过 Spring AI 也并非没有缺点。它的抽象层意味着你失去了对底层 HTTP 请求的完全控制,某些极端定制需求可能绕不开框架的封装。此外,Spring AI 的版本迭代速度较快,API 在 1.0 前后有过不兼容调整,如果你用的是旧版本,参考网上教程时需要格外注意版本差异。
2.3 当前 Spring AI 版本与兼容性说明
我写这篇文章时,Spring AI 的最新稳定版本是 1.0.0 GA(之后可能会有补丁版本)。这个版本提供了比较完整的 OpenAI 兼容支持,包括 Chat、Embedding、Image 等能力。但对 DeepSeek 而言,Spring AI 官方并没有专门的 starter——毕竟 DeepSeek 在海外 Java 社区的声量没有 OpenAI 大,所以整合方式是利用 OpenAI 协议兼容性,把 DeepSeek 的接口地址和模型名配置到 Spring AI 的 OpenAI 客户端里。
这里有一个容易踩坑的地方:Spring AI 1.0 的 OpenAI starter 默认会往 https://api.openai.com 发送请求,同时会在请求头里带上 Authorization 和 OpenAI-Organization 等信息。DeepSeek 官方接口虽然兼容大部分 OpenAI 约定,但对某些特殊请求头可能不那么严格,这就需要我们在配置时手动覆盖 base-url,并确保 API Key 的传递方式符合 DeepSeek 的要求。后面我会给出完整的配置清单。
提示:如果你之前用的是 Spring AI 0.8.x 或更早版本,请先确认项目里 spring-ai 相关依赖的版本号。0.8.x 的配置项与 1.0.x 有不小的差异,直接照抄旧教程大概率会报错。
3. 环境准备与依赖引入
3.1 创建一个最小的 Spring Boot 工程
先准备一个干净的 Spring Boot 项目。我这里用的基础环境是:
- JDK 17
- Spring Boot 3.3.x
- Maven 3.8+
- IDE 随意,个人习惯用 IntelliJ IDEA
选择 JDK 17 是因为 Spring Framework 6 和 Spring Boot 3 都要求最低 JDK 17,如果你还在用 JDK 8,那需要先解决基础环境升级的问题。Spring Boot 版本建议选 3.3 或 3.4,太老的版本对 Spring AI 1.0 的兼容性不好。
创建工程的方式很随意,Spring Initializr 网页生成、IDE 内置模板、或者直接手写 pom.xml 都可以。我用的是 Spring Initializr 生成,然后手动修改 pom 文件。
3.2 引入 spring-ai 相关依赖
Spring AI 的依赖并没有默认集成在 Spring Boot 的依赖管理里,所以第一步是在 pom.xml 的 dependencyManagement 中添加 Spring AI BOM:
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>1.0.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>接着添加核心依赖:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-openai-spring-boot-starter</artifactId> </dependency> </dependencies>注意这里用的是 openai 的 starter,而不是 deepseek 的。原因之前已经说过:Spring AI 没有 DeepSeek 专用 starter,而 DeepSeek 的接口协议兼容 OpenAI,所以通过 OpenAI starter 配置自定义 base-url 即可实现接入。
另外还有一个细节:spring-ai-openai-spring-boot-starter 会传递依赖 spring-ai-openai-client 和 spring-ai-core,通常情况下不需要额外引入其他包。如果你需要处理 JSON 结构化输出,还可以引入 spring-ai-json 相关依赖,但那不是必须的,Jackson 已经够用了。
注意:如果你的项目是国内部署,访问 Maven 中央仓库可能比较慢。推荐使用阿里云镜像或者其他国内镜像源,把 settings.xml 里的 mirror 配置好。之前就遇到过同事在拉取 spring-ai 依赖时等了几分钟没反应,还以为是网络问题,结果换镜像后秒下。
3.3 配置文件里的关键项
依赖引入完成后,最核心的配置集中在 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 从环境变量读取,避免硬编码在配置文件里提交到仓库。
- base-url 必须覆盖为 DeepSeek 的官方接口地址。DeepSeek 接口兼容 OpenAI 格式,路径会自动拼接为 https://api.deepseek.com/v1/chat/completions。如果你在官方文档里看到的地址是 https://api.deepseek.com,不用担心,Spring AI 的 OpenAI 客户端会自动处理
/v1后缀。 - model 填 deepseek-chat,对应 DeepSeek 的对话模型。DeepSeek 还有一个 deepseek-reasoner 模型,用于复杂推理场景,后面我会专门讲它的接入区别。
- temperature 和 max-tokens 都是可选的,Spring AI 有默认值,但建议显式声明,避免不同版本默认值变化导致行为不一致。
配置完成后,写一个简单的 Controller 验证连通性:
@RestController public class ChatController { private final ChatClient chatClient; public ChatController(ChatClient.Builder builder) { this.chatClient = builder.build(); } @GetMapping("/chat") public String chat(@RequestParam String message) { return chatClient.prompt(message).call().content(); } }启动项目后,浏览器访问http://localhost:8080/chat?message=你好,如果配置正确,会返回一段 DeepSeek 生成的文本。这一步能通,说明基础整合已经完成。
4. 核心功能接入与参数调优
4.1 对话补全:从简单问答到多轮对话
上面的例子是最简单的单轮问答。实际业务中,几乎不会只有一问一答,更多场景是多轮对话——用户说“帮我总结一下这段文字”,AI 需要结合前文内容理解意图。Spring AI 中实现多轮对话的方式是通过 ChatMemory 和 Message 窗口管理。
在多轮对话实现上,有两种常见方案。
方案一:自己维护历史消息列表,每次请求时把历史消息拼接到 prompt 里。这种方式实现直观,但需要手动处理历史消息的窗口截断、token 超限等问题。
方案二:利用 Spring AI 提供的 ChatMemory 抽象,通过不同的内存实现管理历史消息。比如InMemoryChatMemory适合单机场景,VectorStoreChatMemory适合需要持久化的场景。对大多数中小型项目,方案一已经足够,方案二更适合需要跨会话共享记忆的场景。
我看过一些团队的代码,为了“省事”直接把所有历史消息全部发给模型,结果 token 数量快速增长,接口报错或者费用飙升。一个最基本的经验法则是:不要盲目把全部历史都带上,要基于模型的上下文窗口和成本权衡。
如果只是想实现一个 token 数受限的滑动窗口,可以在业务层维护一个固定大小的历史列表:
@Component public class ChatHistoryHolder { private final Deque<Map<String, String>> history = new LinkedList<>(); public void add(String role, String content) { history.addLast(Map.of("role", role, "content", content)); // 限制历史消息数量 if (history.size() > 10) { history.removeFirst(); } } public List<Map<String, String>> getAll() { return new ArrayList<>(history); } }然后在调用 ChatClient 时,把历史消息一起传入:
public String chatWithHistory(String userMessage) { historyHolder.add("user", userMessage); List<Map<String, String>> messages = historyHolder.getAll(); return chatClient.prompt() .messages(messages) .call() .content(); }这里需要注意:Spring AI 的prompt().messages()方法需要传入Message对象列表,而不是简单的 Map。上面的写法只是示意,实际使用时你需要把历史记录转换为SystemMessage、UserMessage、AssistantMessage等类型。具体转换方式可以在 Spring AI 的 API 文档里找到,核心思想是保持角色的正确映射。
4.2 流式输出:像 ChatGPT 一样逐字显示
流式输出是很多聊天应用标配的能力。Spring AI 实现流式非常简单,只需要把call()替换为stream():
@GetMapping(value = "/chat/stream", produces = "text/event-stream") public Flux<String> chatStream(@RequestParam String message) { return chatClient.prompt(message).stream().content(); }这里的返回类型是Flux<String>,Spring WebFlux 会自动按照 SSE 格式推送数据。前端用 EventSource 或者 fetch 的流式读取接口就能拿到逐字输出的内容。
实际使用中,我建议在流式请求的超时处理和取消机制上多做一点工作。因为 DeepSeek 生成长文本时可能耗时超过默认的 60 秒,而常见的网关(比如 Nginx)可能默认 60 秒就会断开连接。如果你在生产环境中遇到“流式输出中断”“响应突然结束”的情况,优先检查以下三个位置:
- 网关层面:
proxy_read_timeout和proxy_send_timeout是否配置合理。 - Spring MVC 的异步请求超时:
spring.mvc.async.request-timeout是否足够大。 - DeepSeek 客户端的
max-tokens是否限制过短,导致模型在预期之外提前结束。
我之前就踩过这个坑:本地调试流式一切正常,部署到测试环境后输出几百字就断了,排查半天发现是 Nginx 默认的proxy_read_timeout 60s在作怪。长对话场景下,模型思考的时间加上输出时间很容易超过 60 秒,调大超时时间后问题立即解决。
4.3 结构化输出:让 AI 返回 JSON 而不是散文
调用 LLM 时,最痛苦的事情之一就是解析非结构化的文本。比如你让 AI“抽取这段文字里的所有日期和地点”,如果 AI 返回一段散文,你还需要用 NLP 工具二次处理。更好的做法是让 AI 直接输出 JSON,然后用 Jackson 反序列化为 Java 对象。
Spring AI 1.0 提供了一种优雅的实现方式——BeanOutputConverter:
public record MeetingInfo(String title, String date, String location, List<String> attendees) { } @GetMapping("/meeting-info") public MeetingInfo extractMeetingInfo(@RequestParam String text) { var converter = new BeanOutputConverter<>(MeetingInfo.class); return chatClient.prompt() .user("请从以下会议纪要中提取信息: " + text) .options(OpenAiChatOptions.builder() .model("deepseek-chat") .responseFormat(converter.getResponseFormat()) .build()) .call() .entity(converter.getType()); }背后原理是:Spring AI 会构造一个特殊的 System Prompt,告诉模型“必须按照指定 JSON Schema 输出”,然后把模型的文本响应解析成对应的 Java 类型。这样就不需要你在业务代码里写正则或者手写 JSON 解析了。
不过这里有一个值得注意的地方:DeepSeek 对 JSON 格式的支持虽然不错,但偶尔会输出额外的解释文字,导致 JSON 解析失败。Spring AI 的 BeanOutputConverter 内部做了一些容错处理,会尝试从响应中提取 JSON 部分,但如果模型输出的格式偏差实在太大,解析还是会失败。生产环境中,建议在调用后加一个 try-catch,并做好重试或降级方案。
4.4 DeepSeek 特有参数与长上下文处理
聊到参数,Spring AI 的 OpenAiChatOptions 暴露了常用的参数,包括 temperature、top_p、max_tokens、presence_penalty、frequency_penalty 等。DeepSeek 的接口对这些参数大多支持,但有几个细节你需要注意。
第一,DeepSeek 官方对temperature的范围是 0 到 2,但具体模型可能有更严格的限制。实测下来,deepseek-chat 模型在 temperature 接近 2 时,输出会明显更加发散,容易出现逻辑断裂。对大多数业务场景,建议设置在 0.3 到 0.8 之间。如果你做的是代码生成或者知识抽取,可以更低,比如 0.2 左右。
第二,max_tokens控制的是输出最大长度,而不是输入。DeepSeek 的上下文窗口相对宽裕,如果你的业务需要处理很长的输入(比如整篇文档),要注意的是输入长度可能超过模型的上下文上限。此时需要做截断或分段处理。Spring AI 没有内置的自动分块能力,你需要自己实现分割逻辑。
第三,关于deepseek-reasoner。这是 DeepSeek 的推理模型,在数学、逻辑推理类任务上表现出色,但它有两个特殊点:一是它不支持 temperature 和 top_p 参数,传入会被忽略;二是它的输出中包含 reasoning_content 字段,与正常 content 字段不同。我在与 DeepSeek 官方接口异动对比时发现,reasoner 模型的响应结构和 chat 模型存在差异,Spring AI 默认的 OpenAI 响应解析器可能无法正确处理 reasoning_content,所以目前建议优先使用 deepseek-chat 接入,reasoner 模型等待 Spring AI 后续版本完成兼容后再考虑。
注意:如果你确实需要使用 deepseek-reasoner,需要自定义响应解析器,或者先通过 HTTP 接口验证响应格式,再决定是否值得为此修改 Spring AI 的配置。
5. 进阶技巧:向量嵌入与知识库结合
5.1 为什么需要嵌入模型
聊完聊天,再聊一个实际项目里经常遇到的组合场景:把 Spring AI 和 DeepSeek 接入到知识库问答系统。这类系统的核心流程是:先对文档做切片,通过嵌入模型把切片向量化,存入向量数据库;用户提问时,把问题向量化,在向量数据库中检索相似片段,再把命中的片段和问题一起交给对话模型生成答案。
这里涉及两个模型:嵌入模型负责把文本转为向量,对话模型负责生成回答。DeepSeek 官方 API 目前不提供嵌入模型,所以需要选择其他的嵌入服务。常见的选择有通义千问的 text-embedding-v3、OpenAI 的 text-embedding-3-small,还有开源跑在本地的 bge-m3 等。
5.2 Spring AI 接入嵌入模型的配置
以使用 OpenAI 兼容协议的通义千问嵌入模型为例,配置如下:
spring: ai: openai: api-key: ${DASHSCOPE_API_KEY} base-url: https://dashscope.aliyuncs.com/compatible-mode/v1 embedding: options: model: text-embedding-v3注意这里的 base-url 是 DashScope 的兼容模式地址,和 DeepSeek 的地址不同。Spring AI 允许你同时配置多个 OpenAI 兼容客户端吗?坦白说,在 1.0 版本里,同一组配置只会创建一个 OpenAiChatModel 和一个 OpenAiEmbeddingModel。如果你需要同时使用两家服务,更稳妥的做法是手动定义两个不同的 Bean,分别绑定各自的配置。
举个例子,假设对话模型用 DeepSeek,嵌入模型用本地部署的 bge-m3(通过 Ollama 暴露 OpenAI 兼容接口),你可以这样手动创建:
@Configuration public class AiModelConfig { @Bean public OpenAiChatModel deepSeekChatModel() { var options = OpenAiChatOptions.builder() .model("deepseek-chat") .build(); return new OpenAiChatModel(openAiChatApi(), options); } @Bean public OpenAiApi deepSeekApi() { return OpenAiApi.builder() .baseUrl("https://api.deepseek.com") .apiKey(System.getenv("DEEPSEEK_API_KEY")) .build(); } @Bean public OpenAiEmbeddingModel localEmbeddingModel() { return OpenAiEmbeddingModel.builder() .openAiApi(OpenAiApi.builder() .baseUrl("http://localhost:11434/v1") .apiKey("ollama") .build()) .build(); } }这样在业务代码里就可以通过@Qualifier指定使用哪个模型了。实际项目中,这种多模型共存的场景非常常见,尤其是降级设计时——主模型挂了可以切换到备用模型,不至于整个服务不可用。
5.3 一个简单的 RAG 流程示例
完整搭建知识库需要引入 VectorStore(比如 Redis、Pgvector、Milvus),代码量相对较大。这里只展示最核心的思路,帮大家理解 Spring AI 如何支撑 RAG。
首先把文档切片转成向量并存储:
public void initKnowledgeBase(String docId, String content) { // 1. 文档切片 List<String> chunks = splitText(content, 500, 100); // 2. 转成 Document List<Document> documents = chunks.stream() .map(chunk -> new Document(docId + "-" + UUID.randomUUID(), chunk)) .toList(); // 3. 向量化并存入向量数据库 vectorStore.add(documents); }查询时,先检索再生成:
public String ask(String question) { // 1. 检索 List<Document> docs = vectorStore.similaritySearch(SearchRequest.query(question).withTopK(3)); // 2. 构建上下文 String context = docs.stream() .map(Document::getText) .collect(Collectors.joining("\n")); // 3. 调用对话模型 return chatClient.prompt() .user("请根据以下资料回答问题,如果资料中没有相关信息,请明确回答不知道。\n\n资料:\n" + context + "\n\n问题:" + question) .call() .content(); }这套流程虽然简单,但已经具备 RAG 的基本骨架。在真实项目中你还需要考虑切片粒度、检索阈值、重排序、多轮问答记忆等问题,这些后续可以单独写一篇展开。
6. 生产环境落地中的常见问题与排查
6.1 框架配置导致的连接失败
接 DeepSeek 最常见的错误之一是连接报 401 或 404。401 代表 API Key 不正确或未传递,404 代表接口地址不对。排查步骤很简单:
第一步,用命令行直接测一次 HTTP 接口:
curl https://api.deepseek.com/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $DEEPSEEK_API_KEY" \ -d '{"model":"deepseek-chat","messages":[{"role":"user","content":"你好"}]}'如果 curl 能通,说明网络和 Key 都没问题,问题一定出在 Spring AI 配置上。最可能的情况是 base-url 写错了。注意 Spring AI 的 OpenAI 客户端会自动拼接/v1路径,所以如果你在配置里写的是https://api.deepseek.com/v1,反而会拼接成https://api.deepseek.com/v1/v1/chat/completions,导致 404。base-url 里只需要写到https://api.deepseek.com,不要带v1。
另一个坑是自定义 base-url 时忘记覆盖默认值。Spring AI 1.0 里,spring.ai.openai.base-url默认是https://api.openai.com,如果你只配置了 api-key 而没有配置 base-url,请求会发到 OpenAI 而不是 DeepSeek。
6.2 响应内容乱码与编码问题
中文场景下偶尔遇到乱码问题。原因通常有两类:
一是 HTTP 客户端在解析响应时使用了错误的字符集。Spring AI 底层使用 RestClient,默认应该能正确处理 UTF-8,但如果你的项目里手动配置了StringHttpMessageConverter,可能会覆盖默认的编码。检查全局的 HttpMessageConverter,确保没有把默认编码改成 ISO-8859-1。
二是数据库存储乱码。如果 AI 返回的内容要写入数据库,建议建表时统一使用 utf8mb4 字符集。之前帮一个朋友排查问题,AI 接口返回一切正常,但入库后中文变成问号,最后发现是数据库连接串里没加characterEncoding=utf8。
6.3 token 数超限与费用控制经验
编程任务和长文本处理中,token 超限是最常见的报错。DeepSeek 的上下文窗口虽然大,但如果你频繁传入超长文本,接口会返回类似“maximum context length exceeded”的错误。
我的处理策略是:
- 在送入 prompt 之前先做长度预估。可以用
tiktoken或者 DeepSeek 自己提供的 tokenizer 接口估算 token 数。对中文文本来说,粗略估计 1 个汉字约等于 1 到 1.3 个 token,但这个值不精确,最好用官方工具实测。 - 用滑动窗口裁剪历史消息。只保留最近几轮对话,或者把更早的消息摘要压缩成一段文本。
- 开启调用量监控。每次请求完成之后,从响应中读取 usage 字段,记录 prompt_tokens 和 completion_tokens,并设置告警阈值。Spring AI 的
ChatResponse对象里包含getMetadata()信息,可以拿到 token 用量。如果团队已经有监控平台,把这两个数据上报进去,结合成本单价就能算出每个接口的平均成本。
费用控制方面,DeepSeek 的定价在同类模型里比较有竞争力,但仍需注意:一是流式请求按生成量计费,不要设置过大的 max-tokens;二是重试机制如果写得不小心,会在模型持续返回错误时反复发起调用,产生不必要的费用。建议在重试代码里加入最大次数限制和退避策略。
6.4 常见问题速查表
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 启动报错 UnsatisfiedDependency | 缺少 ChatClient.Builder Bean | 确认已引入 spring-ai-openai-spring-boot-starter 并配置 api-key 和 base-url |
| 请求 401 Unauthorized | API Key 错误或未设置 | 检查环境变量是否注入正确,直接在配置中临时填入 Key 测试 |
| 请求 404 Not Found | base-url 多加了 /v1 路径 | 确保 base-url 只写到根地址,不带路径后缀 |
| 返回内容为空 | 模型 name 配置错误或 prompt 为空 | 确认 model 为 deepseek-chat,检查请求参数 |
| 流式输出中断 | 网关超时、异步超时时间过短 | 调大 Nginx 的 proxy_read_timeout 和 MVC 的 async request-timeout |
| 中文乱码 | 编码设置不一致 | 统一 UTF-8,检查 HttpMessageConverter 和数据库连接 |
| JSON 解析失败 | 模型返回了非 JSON 内容 | 加强响应容错,用 BeanOutputConverter 或自定义解析逻辑 |
6.5 一个典型的排查实录
前段时间,我把一套基于 Spring AI 的服务从其他模型切换到 DeepSeek,切换完成后发现一个问题:普通对话一切正常,但凡是涉及长文本总结的请求,响应速度就慢得离谱,甚至偶尔超时。排查过程如下:
先看日志,发现报错信息是“connect timed out”。初步怀疑是网络问题,于是直接用 curl 测试同一条请求,响应时间不到 5 秒,排除网络因素。再看配置,发现我为了赶工期,直接复制了网上某篇教程里的超时配置:
spring: ai: openai: client: connect-timeout: 5s read-timeout: 5s问题就出在这里。DeepSeek 处理长文本时,模型推理耗时可能超过 10 秒,5 秒的 read-timeout 必然导致超时。把超时时间改为 60 秒后,问题消失。
这个案例说明,网上流传的配置不一定适合你的场景,务必结合自己的业务特点调整参数。
7. 我的一点实操感悟
Spring AI 整合 DeepSeek 这件事,本质上并不复杂,复杂的是隐藏在各种细节中的兼容性问题。我在做过几个项目的接入后,最大的感受是:框架的抽象层带来便利的同时,也要求开发者对底层协议有足够的理解。你在配置里写下的每一个参数,背后都对应着一次 HTTP 请求的具体行为。只有当你能解释清楚“这个参数为什么这么设置”时,才算真正掌握了这套整合方案。
如果你打算直接把这套方案用到自己的项目里,我的建议是先从最小示例跑通,不要一上来就追求复杂的功能组合。跑通之后,再做流式输出、结构化输出、多轮对话,一步一步叠加。这样即使出问题,也容易定位。另外,日志里把请求和响应体打印出来是非常重要的调试手段。Spring AI 默认可能不会打印完整请求体,你可以在启动类里临时加上日志级别调整,也可以直接用 curl 对比接口行为,这样能快速确认到底是框架的问题还是模型服务的问题。
这个方向后续可以扩展的内容还很多,比如把 DeepSeek 作为向量模型的补充实现多模型协同、构建可观测的 AI 网关层、以及针对长上下文场景做自动摘要压缩。我会在后续的实践中继续整理经验,分享出来。希望这篇实战指南对你有帮助。