news 2026/9/9 5:33:31

Spring Boot 3.4 + Spring AI 1.0 接入 DeepSeek 完整实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Boot 3.4 + Spring AI 1.0 接入 DeepSeek 完整实战指南

DeepSeek 的接口文档其实写得挺清楚:一个 HTTP POST 请求,把消息丢给 /chat/completions,几秒钟之后拿到回复。但要把这条链路接进 Spring Boot 工程,再让 Spring AI 帮你干活,事情就没那么简单了。谁来拼请求体、谁处理流式、多轮对话的上下文放哪、模型返回的文本怎么变成业务对象,这些问题一个个都要回答。我今年把一个实际项目从裸调用改成了 Spring Boot 3.4 + Spring AI 1.0 调 DeepSeek 的方案,这篇就把整个过程——从搭项目、配依赖、写第一个接口,到上线前踩过的坑——完整写出来。目标是让准备做 Spring Boot + SpringAI + DeepSeek 接口接入的同学,照着做就能跑通,并且少走我走过的弯路。

1. 先说结论:Spring AI 不是必需,但能让代码少一半

先回答一个很多人会纠结的问题:Spring AI 是必须的吗?不是。DeepSeek 的接口本质上就是一个 HTTP 接口,用 RestTemplate、OkHttp、Java 自带的 HttpClient 都能调。我第一次接入的时候就是原生 HttpClient 手搓的,单看"能对话"这个目标,半天就做完了。问题的关键出现在需求往后走的时候:要加流式输出,要支持多轮,要让模型能调用我的订单查询方法,还要统计每个用户消耗了多少 token。这些需求每加一个,手写 HTTP 的方案就要多几十行代码,错误处理和参数适配还会成倍增长。

Spring AI 解决的就是这个问题。它把和大模型对话这件事抽象成了 ChatModel 和 ChatClient 两层,底层走 HTTP 还是 WebClient,请求体长什么样,响应怎么解析,都不用业务代码关心。更关键的是,Spring AI 是 Spring 官方维护的项目,不是某个开发者一时兴起的小封装,版本在迭代、文档在更新,踩坑也有社区帮忙兜底。对于 Java 后端团队来说,选它比选各种个人开源库稳得多。

还有一个让我最终确定这个方案的理由:DeepSeek 的 API 是 OpenAI 兼容协议。这意味着我可以直接用 Spring AI 的 OpenAI starter,把 base-url 指到 DeepSeek 就行——网上一大批人搜的"springai 中 openai 换 url"就是这件事。这个特性表面看只是省了一次适配,实际带来的好处是切换成本极低:今天用 DeepSeek,明天想换通义、豆包,或者切到本地 Ollama 部署的模型,改配置就行,业务代码一行不动。

那什么时候不需要 Spring AI?如果你是写一次性脚本、内部命令行工具,或者只做 Demo 演示,直接用 HTTP 客户端更轻量。但只要是会长期维护的 Spring Boot 工程,我还是建议多花这一层抽象的时间。这篇教程的读者,我默认是三种人:刚学完 Spring Boot 想接大模型的同学、做毕业设计需要给系统加 AI 功能的、公司内部在做大模型接入调研的 Java 后端。

2. 初始化与依赖:版本搭错,后面全是白费

这个项目的第一步就很容易踩坑,因为 Spring AI 的版本迭代实在太快了。2024 年大部分教程还在用 0.8.x 的 M 系列版本,2025 年 5 月才发布 1.0.0 GA。更麻烦的是 0.8.x 和 1.0.x 的 API 风格完全不同:0.8 时代用 ChatClient.create(model) 创建客户端,1.0 改成注入 ChatClient.Builder;0.8 的依赖叫 spring-ai-openai-spring-boot-starter,1.0 叫 spring-ai-starter-model-openai。如果你照着 0.8 的教程在 1.0 项目里写代码,编译期就开始报错,根本不是配置能救回来的。

2.1 JDK、Spring Boot、Spring AI 的版本搭配表

我这里给一份我实测可行的版本组合,也标注了旧版本的情况:

组合JDKSpring BootSpring AI说明
推荐组合17+3.4.x / 3.5.x1.0.x(GA 版本)依赖在 Maven Central,API 以 Builder 为主
旧项目组合17+3.2.x / 3.3.x0.8.x(M 系列)API 风格不同,需要额外配 spring-milestones 仓库
不建议8 / 112.7.x不支持Spring AI 只支持 Spring Boot 3.x 和 JDK 17+

我建议新项目直接上推荐组合。如果你的项目因为历史原因锁死在 Spring Boot 3.2,那也不是不能跑,但你搜教程的时候千万要记得加"0.8"这个版本关键词,否则会被 1.0 的代码带偏。

2.2 用 BOM 管理依赖,别一个一个手动引

这是我的第一个建议:别在 dependencies 里手动写 spring-ai 相关依赖的版本号,用 BOM 统一管理。Spring AI 的模块很多,只靠人工对齐版本非常容易出错,BOM 导入后你可以直接不写版本号。

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.4.5</version> <relativePath/> </parent> <properties> <java.version>17</java.version> <spring-ai.version>1.0.0</spring-ai.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>${spring-ai.version}</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-starter-model-openai</artifactId> </dependency> </dependencies>

如果你是纯手撸 Maven 工程而不是 IDE 生成,build 节点里别忘了加 spring-boot-maven-plugin,这样mvn spring-boot:run才能直接用。IDE 生成通常会自带,手写的话特别容易漏。

另外一个隐藏坑:1.0.0 GA 之后的 Spring AI 已经发布到 Maven Central,不需要配额外仓库。如果你在教程里看到<repository>spring-milestones</repository>这样的配置,那是 0.8.x 时代的东西,1.0 里可以删掉,留着反而可能拉到不期望的中间版本。

2.3 两种 Starter 选择:官方 DeepSeek 和 OpenAI 兼容

接入 DeepSeek 有两条路:

接入方式依赖配置前缀适用场景
官方 DeepSeek Starterspring-ai-starter-model-deepseekspring.ai.deepseek.*只接 DeepSeek,版本够新(1.0.0 GA 后)
OpenAI 兼容方式spring-ai-starter-model-openaispring.ai.openai.*要灵活切换多个兼容服务,或还没升到最新版

我的实际项目用的是 OpenAI 兼容方式,因为除了 DeepSeek,我还用同一个配置模式接了一个 OpenAI 协议的网关服务,一个前缀到处切。如果你非常确定只接 DeepSeek,而且所用 Spring AI 版本里能拉到 spring-ai-starter-model-deepseek 这个依赖,直接用官方 starter 语义更清晰。两种方式我用下来,默认的调用逻辑基本一致,差异主要体现在配置前缀和个别模型参数命名上。

再说一下 API Key。去 DeepSeek 开放平台注册后建一个 Key,形如sk-开头。成本上 DeepSeek 一直走的是"白菜价"路线,同等任务量通常比 OpenAI 便宜一个数量级,日常开发和普通业务问答完全不用心疼。精确价格随时会调,以平台页面的实时报价为准,这篇文章就不写死具体数字了。

3. 配置文件:就这几个键,错一个调一天

Spring AI 的自动配置让大部分事情在 application.yml 里就能搞定。以 OpenAI 兼容方式为例,最小配置就几个键:

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

这几行看起来简单,但每一项都有人填错过。

3.1 base-url 填错,是所有 401/404 的根源

base-url 一定要填https://api.deepseek.com,不要画蛇添足。Spring AI 会在请求时自己拼上/chat/completions/v1/chat/completions,而 DeepSeek 这两个路径都兼容,所以域名根地址是最安全的选择。

我见过两种典型错误。第一种是抄别的教程时把地址一起抄过来了,比如写成https://api.openai.com,key 却是 DeepSeek 的,结果 401 让人怀疑人生。第二种是自己"好心"把 base-url 写成https://api.deepseek.com/v1或者https://api.deepseek.com/chat/completions,一旦 Spring AI 内部会拼路径,就会变成双重路径,直接 404。

提示:Spring AI 需要的是"服务地址",不是"接口地址"。接口路径它知道怎么拼,你只需要告诉它 DeepSeek 的服务在哪。

遇到 404 时,第一反应应该是去翻实际发出的请求路径,而不是继续猜配置。

3.2 deepseek-chat 和 deepseek-reasoner,别在业务里混着用

DeepSeek 开放平台上有两个主要模型名,很多人一开始分不清:

模型名类型特点适用场景
deepseek-chat通用对话模型响应快、价格低、支持函数调用客服问答、内容生成、日常业务
deepseek-reasoner推理模型会返回思维链、复杂问题强,但慢且贵数学推导、代码分析、需要展示思考过程

在 Spring AI 里,模型名直接写在配置里。如果你希望某些请求走推理模型、另一些走通用模型,可以在单次请求里覆盖:

chatClient.prompt() .options(OpenAiChatOptions.builder() .model("deepseek-reasoner") .build()) .user("证明一下为什么根号2是无理数") .call() .content();

这里有个容易忽略的点:模板是通用模型的"最佳实践",比如设置 temperature 为 0.7 对 deepseek-chat 有效,但 deepseek-reasoner 会忽略大部分采样参数。我建议把 deepseek-reasoner 理解成"专用计算器",而不是"更聪明的对话模型",对话场景用 deepseek-chat,复杂推理才切 reasoner,成本和响应时间都是这么省下来的。

3.3 密钥别写死在 yml 里

这个提醒看起来老生常谈,但我见过太多次为了省事把 API Key 直接写在 application.yml 里,然后整个仓库推到 GitHub。DeepSeek 的 Key 虽然便宜,但被人刷起来一样能把账号打爆。正确做法是:

spring: ai: openai: api-key: ${DEEPSEEK_API_KEY}

本地开发可以建一个.gitignore排除掉的 application-local.yml,再在启动参数里指定--spring.profiles.active=local。生产用环境变量注入。样例配置里只放${DEEPSEEK_API_KEY}占位符,这样就算配置文件被拉走也没有实际风险。

4. 写第一个接口:ChatClient 用对了,代码很干净

配置好之后,写代码反而是最快的一步。先不要想什么复杂架构,把"用户发一句话,模型回一句话"这个最简链路跑通,再逐步加能力。

4.1 注入 ChatClient.Builder,不要再用 ChatClient.create()

Spring AI 0.8 时代的经典写法是这样:

ChatClient chatClient = ChatClient.create(chatModel);

在 1.0 里,更推荐的写法是注入自动配置好的ChatClient.Builder,在 Builder 上统一挂公共配置,再 build 出来。我一开始也不太理解为什么非要 Builder,用了一段时间发现这才是串联整个框架的关键:默认 system prompt、默认 memory、默认 tools 都可以挂在 Builder 上,业务代码里只需要关心"用户这一句说了什么"。

@Configuration public class AiConfig { @Bean ChatClient chatClient(ChatClient.Builder builder) { return builder .defaultSystem("你是一个严谨的技术助手,回答保持简洁、准确。") .build(); } }

4.2 Service + Controller + record DTO 的最小实现

Service 层把 ChatClient 包一层,避免 Controller 直接跟 AI SDK 打交道:

@Service public class DeepSeekChatService { private final ChatClient chatClient; public DeepSeekChatService(ChatClient chatClient) { this.chatClient = chatClient; } public String chat(String message) { return chatClient.prompt() .user(message) .call() .content(); } }

Controller 暴露一个 POST 接口:

@RestController @RequestMapping("/api/deepseek") public class DeepSeekChatController { private final DeepSeekChatService chatService; public DeepSeekChatController(DeepSeekChatService chatService) { this.chatService = chatService; } @PostMapping("/chat") public ChatResponse chat(@RequestBody ChatRequest request) { String answer = chatService.chat(request.message()); return new ChatResponse(answer); } }

DTO 用 record 就够了,Spring Boot 默认用 Jackson 序列化,返回给前端的 JSON 不用写一行转换代码:

public record ChatRequest(String message) {} public record ChatResponse(String answer) {}

这一层结构非常重要:Controller 只负责 HTTP 协议,Service 只负责拼 prompt 和拿回复,将来换模型、加流式、加记忆都不需要改 Controller。很多人把 Spring Boot 的 AI 项目写成一个巨大的 Controller 类,维护起来会非常痛苦。

4.3 用 curl 做冒烟测试

mvn spring-boot:run启动后,直接用 curl 打一下:

curl -X POST http://localhost:8080/api/deepseek/chat \ -H "Content-Type: application/json" \ -d '{"message":"用一句话介绍杭州"}'

正常的响应类似:

{"answer":"杭州是浙江省省会,以西湖、龙井茶和电商经济闻名。"}

第一次跑通后,建议把三个指标记一下:响应时间、返回内容是否正确、日志里有没有意外打印敏感内容。如果返回 JSON 解析失败,检查一下是不是 Controller 返回类型写成了 String 而忘了我上面说的 record 结构。

注意:如果你的请求等了很久然后报超时,直接跳到 6.2 节,那是所有接大模型的人都会遇到的一道坎。

5. 从"能对话"到"能上线":流式、记忆、结构化输出、工具调用

跑通一个同步接口只是开始。实际项目里,几乎不会有人愿意看着 loading 转 5 秒等一个完整回答,也不会满足于让模型"随便说说",而是希望它返回能直接入库的结构化数据。

5.1 流式输出:SSE 让首字等待从几秒降到亚秒

大模型的响应是"首 token 延迟 + 逐 token 输出"的模式。你问一个复杂问题,可能 3 秒后模型才开始生成,但如果同步接口要等全部生成完,用户感知的等待时间可能是 10 秒以上。改用流式后,模型吐出第一个 token 用户就能看到,体感完全不一样。

Spring AI 的流式写法非常直接:

public Flux<String> chatStream(String message) { return chatClient.prompt() .user(message) .stream() .content(); }

Controller 返回类型改成Flux<String>,并指定内容类型为 text/event-stream:

@PostMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux<String> chatStream(@RequestBody ChatRequest request) { return chatService.chatStream(request.message()); }

这里有一个前端配合的经典坑:SSE 的EventSource对象只支持 GET 请求,而你的接口大概率是 POST。所以用 Postman 测试没问题,跟前端联调时却被 EventSource 限制卡住。解决办法是让前端用fetch+ReadableStream解析,或者单独提供一个 GET 的 SSE 地址接受 query 参数。我在项目里是两者都留了,内部工具用同步,对外服务用流式。

5.2 多轮对话:ChatMemory 别无限存,窗口大小要设

默认情况下,ChatClient 每次调用都是无状态的,模型记不住上一轮说了什么。要让模型"记得"上下文,要么前端每次把完整历史发过来,要么在服务端维护会话记忆。Spring AI 提供了MessageWindowChatMemory,按窗口保留最近 N 条消息:

@Bean ChatMemory chatMemory() { return MessageWindowChatMemory.builder() .maxMessages(20) .build(); }

然后在构建 ChatClient 时挂上去:

@Bean ChatClient chatClient(ChatClient.Builder builder, ChatMemory chatMemory) { return builder .defaultMemory(chatMemory) .build(); }

这样同一个 ChatClient 实例内部会自动维护最后一次的对话上下文。但注意:这个 memory 默认是按"整个服务"共享的,多用户访问会串上下文。做真实项目至少要按会话维度隔离,简单做法是自己维护一个sessionId -> ChatClient的 Map,或者把历史消息交给前端维护,服务端只做无状态推理。窗口大小也直接决定 token 成本,20 条消息往上走,每轮请求都会把 20 条历史发给模型,成本会明显上涨。

5.3 结构化输出:让模型直接返回 Java 对象

跟模型聊天时它爱写散文,但很多业务场景要的是字段。比如给一段聊天记录,希望模型提取出订单号、客户名、金额。Spring AI 的结构化输出封装了这件事:

public record OrderInfo(String orderId, String customerName, String amount) {} public OrderInfo extractOrder(String rawText) { return chatClient.prompt() .user("从下面这段聊天记录中提取订单信息:" + rawText) .call() .entity(OrderInfo.class); }

调用.entity()时,Spring AI 会在请求里注入 JSON Schema 指令,让模型按结构返回 JSON,再反序列化成你的 Java 对象。这比自己在 prompt 里写"请返回 JSON"再手动解析靠谱得多。经验上要注意两点:第一,DeepSeek 的 JSON 输出模式要求 prompt 里出现"json"字样,Spring AI 生成的指令默认带了,所以一般能过,但如果你复用了自定义 system prompt,最好加一句"只输出 JSON";第二,模型偶尔会不听话返回解析不了的内容,调用 entity 时记得 catch 异常做兜底,不要让一个解析错误把整个接口打崩。

5.4 工具调用:@Tool 让 DeepSeek 能查你的系统

如果你的业务需要模型"动手"——查订单状态、查天气、查库存——那就到了工具调用(Function Calling / Tool Calling)的范畴。Spring AI 1.0 用@Tool注解标记方法,模型会在回答前决定要不要调用:

@Component public class OrderTools { @Tool("根据订单号查询订单状态") public String queryOrderStatus(String orderId) { return "订单 " + orderId + " 已发货,预计明天到达"; } }

调用时把工具实例传进去:

chatClient.prompt() .user("帮我查一下订单 DS20241127 现在什么状态") .tools(orderTools) .call() .content();

模型会先返回"要调用 queryOrderStatus",Spring AI 自动执行完再把结果喂回给模型,由模型组织最终回答。整个流程帮你屏蔽了,看起来就像模型自己会查数据。这里有个必须留意的点:工具方法一般都有副作用或至少消耗真实资源,上线前要确认调用权限、频率限制和超时处理。另外 deepseek-chat 的函数调用(工具调用)相对稳定,reasoner 上的支持情况要以官方文档为准,我在项目里没有让推理模型挂工具,避免不可控的额外 token 消耗。

6. 实测踩坑:reasoning_content 400、超时、Actuator 泄露、版本炸弹

下面这些坑不是我提前预判的,是真实项目里一个一个撞出来的。我按遇到频率排个序,每个都给出了当时的排查链路,希望能帮你看完少走同样的弯路。

6.1 400 错误:deepseek-reasoner 多轮对话时 reasoning_content 必须原样回传

这是被问得最多的一个问题,也是社区里非常经典的报错。现象是:用 deepseek-reasoner 加上聊天记忆,第一轮正常,第二轮请求直接返回 400,响应体大致写着 "thereasoning_contentin the thinking mode must be passed back to the api"。

排查链路是这样的:先用 curl 直接调了一次 DeepSeek 官方接口,确认单轮没问题;然后怀疑是记忆窗口把历史消息按 OpenAI 格式序列化,assistant 消息里只带了 content,丢了 reasoning_content。后来查 DeepSeek 官方文档确认,reasoner 模式在多轮对话时,上一轮 assistant 返回的 reasoning_content 需要在下一轮原样带回服务端。而 Spring AI 的 OpenAI 兼容实现默认不保留这个字段,于是第二轮就 400 了。

我最后的处理方案是:多轮对话一律用 deepseek-chat;deepseek-reasoner 只用在单轮、无记忆的深度推理场景。如果业务上非要 reasoner 走多轮,就得自己维护消息历史,手工构造 DeepSeek 要求的 assistant 消息格式,把 reasoning_content 拼回去,这属于偏底层的适配,不建议在业务代码里铺开。另外,如果你是刚更新版本后开始报这个错,先去查 Spring AI 的 release notes,看看有没有相关的兼容修复,再决定要不要升级。

排查 400 类错误的通用经验:错误要看完整响应体,别只看状态码。LLM 服务的错误信息往往已经把原因写得很直白了,很多时候答案就在那行英文里。

6.2 漫长等待:读超时是接大模型最常见的生产事故

第二个高频坑就是超时。现象是接口偶尔正常、偶尔报 Read timed out,或者干脆 502,尤其在 deepseek-reasoner 上,一个复杂问题想 60 秒很正常,普通 HTTP 客户端默认读超时根本撑不住。

Spring AI 底层用的是 RestClient/WebClient,它的超时配置在不同小版本里字段名称有差异,有的版本是统一 timeout,有的拆成 connect/read 两个。为了避免版本差异影响,我最喜欢用代码定制 RestClient:

@Bean RestClientCustomizer restClientCustomizer() { return builder -> builder.requestFactory( ClientHttpRequestFactories.get( ClientHttpRequestFactorySettings.defaults() .withConnectTimeout(Duration.ofSeconds(10)) .withReadTimeout(Duration.ofSeconds(120)) ) ); }

connect-timeout 设 10 秒,read-timeout 设 120 秒,基本能覆盖绝大多数情况。除了应用层,网关层也要一起检查:Nginx 的 proxy_read_timeout、Spring Cloud Gateway 的 response-timeout 如果还是默认值,应用层再宽松也会在网关那一层被掐断。我那次线上 502 就是 Nginx 默认 60 秒卡的,应用日志里根本没有对应耗时,排查了一圈才想起来看网关。

超时的另一面是资源占用。同步接口等 120 秒,等于每个请求要占着一个连接挂 2 分钟,并发一高吞吐就崩。这也是我坚持在对外接口上做异步化的原因,具体做法放到第 7 节讲。

6.3 Actuator 端点:别为了看指标把 AI 服务裸奔出去

Spring AI 和 Micrometer 集成得非常好,会自动上报模型调用的耗时、调用次数、token 估算等指标。这本是件好事,但前提是你的 actuator 端点没有全暴露出去。有些教程为了展示指标,会让你配:

management: endpoints: web: exposure: include: "*"

这个配置等于把服务的管理后门彻底打开。放在 AI 项目里风险更具体:别人能从指标接口看出你每天调用了多少次模型、大概花了多少钱,甚至通过某些端点读取到环境信息。Spring Boot 3 默认只暴露 health,这是相对安全的,我建议生产环境保持默认,或者显式只开 health 和 info:

management: endpoints: web: exposure: include: health,info

如果你确实需要把 micrometer 指标接进监控大盘,把 actuator 端口内网隔离或者加一层认证,别直接挂在公网上。另外,排查问题的时候很多人喜欢写一个打印请求体的 filter,一打日志把用户输入和 API Key 全打出来了,这也是接大模型项目里特别常见的信息泄露来源。生产环境日志里只记录消息长度的摘要,不要记录完整 prompt。

6.4 版本炸弹:0.8.x 和 1.0.x 的代码完全不兼容

最后说一个"看不见的坑"——版本。0.8.x 和 1.0.x 看着都是 Spring AI,代码差异却很大:

  • 依赖名:0.8 是 spring-ai-openai-spring-boot-starter,1.0 是 spring-ai-starter-model-openai。
  • 客户端创建:0.8 常用 ChatClient.create(ChatModel),1.0 推荐注入 ChatClient.Builder。
  • Boot 版本:0.8 适配 Spring Boot 3.2/3.3,1.0 适配 3.4+。

我见过不少同学代码没问题、配置没问题,就是编译不过,最后发现是照着 0.8 的帖子在 1.0 的项目里写。搜索解决方案的时候,尽量带上自己的 Spring AI 版本号,别把所有帖子混着看。还有一个隐藏场景:你为了对比 DeepSeek、Ollama、OpenAI,一口气引入了多个 AI starter,容器里就会有多个 ChatModel Bean。这时候注入 ChatClient.Builder 它不知道该用哪个,启动或调用时报错,解决方式是给 Bean 加 @Qualifier,或者只在配置里保留一个启用的模型。

7. 接口调通之后,工程上还差这几步

接口能跑通只是开始,真正让项目变得可维护、能上线的,是后面这些工程化细节。我每次接一个新项目,都会至少过一遍下面这几件事。

7.1 目录规范:按 controller / service / config / dto 分包就够用了

Spring Boot 项目目录规范没有强制标准,但 AI 项目我建议至少拆出这几层:

src/main/java/com/example/deepseek/ ├── config/ # ChatClient、ChatMemory 等 Bean 定义 ├── controller/ # REST 接口 ├── service/ # 业务逻辑和 prompt 组装 ├── dto/ # 请求/响应 record └── tool/ # @Tool 注解的模型工具方法

把 AI 相关的 Bean 收敛到 config 包里,收益很快就能看到:将来换模型或者切本地部署,几乎只需要动 config 和 yml,Controller 和 Service 都不用翻。工具方法单独放 tool 包,是为了让模型可调用的能力边界一目了然,做权限评估的时候也好查。这个目录规范在 Spring Boot 面试里也常被问到,虽然不同团队约定有差异,但"按职责分层、把易变部分隔离"这个思路是通用的。

7.2 长耗时 POST 怎么做:提交任务 ID,再轮询状态

同步 POST 等几十秒,前端体验和网关超时都很尴尬。一个不用引入消息队列就能实现的方案是"异步任务 + 状态轮询":请求进来先返回任务 ID,前端拿 ID 轮询状态接口。核心代码其实很简单:

public record ChatTask(String status, String answer, String error) {} @Service public class ChatTaskService { private final Map<String, ChatTask> tasks = new ConcurrentHashMap<>(); public String submit(String prompt) { String taskId = UUID.randomUUID().toString(); tasks.put(taskId, new ChatTask("RUNNING", null, null)); CompletableFuture.runAsync(() -> { try { String answer = chatService.chat(prompt); tasks.put(taskId, new ChatTask("SUCCESS", answer, null)); } catch (Exception e) { tasks.put(taskId, new ChatTask("FAILED", null, e.getMessage())); } }); return taskId; } public ChatTask query(String taskId) { return tasks.getOrDefault(taskId, new ChatTask("NOT_FOUND", null, null)); } }

前端先POST /api/chat/tasks拿 taskId,然后GET /api/chat/tasks/{taskId}查到 SUCCESS 再取结果。这里 ConcurrentHashMap 只是演示级别,单机还好,多实例部署时任务状态会漂移,生产环境建议换成 Redis 或者数据库任务表。但思路是通用的。

7.3 成本、限流与模型选型:这几个问题想过一遍再上线

大模型成本不会吃掉你的预算,但如果不管,很容易浪费在三个地方:重复计算、超长输入、无限调用。第一,加缓存,相同或相似的问题直接命中缓存返回,能省一大半 token。第二,限制输入长度和 max-tokens,请求体太长的先截断或拒绝,别让用户把整本书塞进 prompt。第三,做最简单的单用户限流,每秒放行几次,防止有人拿到接口地址后无限刷。令牌桶实现几十行代码就能搞定,但能挡住绝大多数滥用。

模型选型方面,我当前项目留在 DeepSeek 的原因很直接:OpenAI 兼容协议让 Spring AI 直接焊上、中文效果可靠、价格足够低、函数调用稳定。豆包、通义千问、元宝也都各有优势,但如果你已经有 DeepSeek 在跑,迁移前一定要确认目标模型的协议兼容性、上下文长度和函数调用能力这三项,否则会有额外适配成本。如果公司内部打算本地部署开源模型拿数据隐私,Ollama 那套也提供一个 OpenAI 兼容端点,Spring AI 只需要把 base-url 指过去、模型名改成本地模型名,业务代码基本不用动——这就是当初多花那一小时包一层 Spring AI 带给你的灵活度。

再补一个排查技巧:当 DeepSeek 某个模型在 Spring AI 里返回异常,别急着怀疑代码。先用 curl 直连一次官方接口,把响应体完整打出来对比。很多"兼容"问题的答案,都藏在这个响应体里,它比 Spring 报错信息直白得多。这个习惯救过我多次,分享给正在踩坑的你。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 5:32:17

STM32 OLED(IIC)波形显示实战:模拟IIC时序与SSD1306驱动详解

简介&#xff1a;面向野火STM32F1开发板的0.96英寸OLED&#xff08;IIC接口&#xff09;波形显示工程&#xff0c;适合正在学习STM32裸机外设驱动与显示应用的单片机开发者。工程基于标准外设库&#xff0c;覆盖RCC、TIM、ADC、I2C、USART等常用模块&#xff0c;核心演示如何通…

作者头像 李华
网站建设 2026/9/9 5:31:41

HarmonyOS 6.0分布式开发实战:跨端协同与软总线落地指南

不用多解释&#xff0c;HarmonyOS 6.0 最值得动手折腾的&#xff0c;就是分布式能力。这个版本把“手机PC”的跨端协作从 PPT 概念变成了真正可落地的工程方案&#xff0c;尤其是分布式软总线、跨端流转和原子化服务的成熟度&#xff0c;已经到了一种“只要你想做&#xff0c;官…

作者头像 李华
网站建设 2026/9/9 5:27:42

opencode不是工具,而是开发者常见误操作的集合体

1. “opencode”到底是什么&#xff1f;别被名字骗了&#xff0c;它不是开源代码平台&#xff0c;也不是某个大厂新发布的AI编码工具最近在技术社区和开发者群里&#xff0c;“opencode”这个词出现频率陡增&#xff0c;但很多人一搜就懵——没有官网、没有GitHub主仓库、没有明…

作者头像 李华
网站建设 2026/9/9 5:26:00

路径总和 III 前缀和优化:从暴力深搜到 O(n) 解法

1. 从“路径总和”到“路径总和3”&#xff1a;这题到底在考什么力扣热题100里的第48题“路径总和3”是很多人的分水岭。前面两题只要会简单的递归就能过&#xff0c;这道题却突然跳出了“根到叶子”的框框&#xff0c;要求统计的是任意节点向下到任意节点的路径和。第一次看到…

作者头像 李华
网站建设 2026/9/9 5:24:20

动态包含性能瓶颈:PHP include/require优化实战与改造方案

接手这个老项目优化任务的时候&#xff0c;我第一反应是去看数据库慢查询和缓存命中率&#xff0c;结果折腾半天都没找到大头。后来把PHP的请求链路拆开&#xff0c;才发现一个被很多人忽略的细节&#xff1a;模板块里大量使用了动态包含——也就是把include/require的参数写成…

作者头像 李华
网站建设 2026/9/9 5:20:50

嵌入式GPU编程实战:计算着色器、性能优化与Jetson开发

提起嵌入式GPU编程&#xff0c;很多人第一反应是&#xff1a;这不就是把显卡编程搬到嵌入式板子上吗&#xff1f;这句话对了一半。GPU确实是那颗GPU&#xff0c;但嵌入式环境里的存储模型、功耗墙、驱动差异和工具链&#xff0c;跟你在PC上写CUDA或者OpenGL完全是两种玩法。我这…

作者头像 李华