简介面向需要低成本接入大模型能力的Java开发者这是一套演示如何基于Spring Boot与Spring AI调用deepseek-r1模型、实现本地免费使用的Demo工程。资源包为rar压缩格式共7个文件其中2个Java源文件为核心业务代码负责构造请求、处理响应另含pom.xml、properties等Maven工程与运行配置文件以及README文档、LICENSE许可证和.gitignore版本控制文件结构紧凑便于导入开发工具直接阅读。该示例针对云端模型服务成本偏高、数据隐私和网络稳定性难以保障等痛点给出了基于开源框架的本地部署思路代码中完整覆盖了调用前的参数配置、调用中的请求发送与结果解析可帮助开发者快速迁移到自身业务中。借助这一示例开发者无需支付额外模型调用费用即可在本地体验DeepSeek-R1的语言理解与生成能力也为后续扩展其他AI能力提供参考范式。压缩包仅10KB轻量实用已有1462人学习下载适合具备一定Spring基础、希望快速上手DeepSeek本地部署的Java工程师。 最近大模型API的按token计费让我这个习惯本地测试的人越来越不自在尤其是一些不在业务主链路里的文本处理任务——摘要、命名实体抽取、文档初筛——跑一晚上就是几百次调用账单数字不小。正好DeepSeek-R1开源了网上也有“7B量化版”可以在普通桌面机上跑起来我就起了个念头把模型拉到本地然后用SpringBoot写一个简单的REST服务把它暴露出来内部调用和联调都不花钱数据也不出内网。折腾了两个晚上完整链路已经能稳定跑通这篇文章把整个过程、踩过的坑、以及SpringBoot调用DeepSeek-R1时的几个关键选择都写出来给同样想“免费蹭自家显卡”的人一个可复现的参考。1. 本地模型部署选型为什么我不用vLLM而用Ollama1.1 一条命令装好Ollama先把最核心的工具敲定。本地跑DeepSeek-R1的路径很多但我最终选了Ollama。原因很简单它把“模型下载、运行、HTTP接口暴露”这三个环节全部压缩成了一条命令对后端开发来说几乎零学习成本。macOS或Linux上的安装命令是curl -fsSL https://ollama.com/install.sh | shWindows则直接下载安装包装完以后在终端里拉取模型ollama pull deepseek-r1:7b如果你机器性能一般可以拉量化程度更高的版本比如deepseek-r1:7b-q4_K_M文件大小只有4.7GB左右内存占用比原版小一半还多。拉完以后直接跑ollama serve默认监听0.0.0.0:11434日志里会打出Listening on 11434。到这一步本地“模型服务器”就算起来了。1.2 不同推理框架的真实区别在网上翻资料的时候你一定会看到vLLM、llama.cpp、LM Studio这些名字。它们不是同类工具适配的场景差别很大我整理成了下面这张表框架安装难度高并发API兼容性资源占用适合谁Ollama最低一般原生HTTP较低开发人员本地调试vLLM较高很强OpenAI风格较高服务化部署、大规模推理llama.cpp偏高较弱需自己封装最低嵌入式、旧机器、研究底层LM Studio最低弱仅本机中等非开发者、想用图形界面我的判断很直接SpringBoot项目的诉求是“通过HTTP接口调用一个模型”不是一个模型训练平台也不是一个高性能推理集群。vLLM再强部署一个生产节点要装CUDA、配模型并行对个人玩票来说太重llama.cpp更适合C/C生态放到JVM项目里还得折腾JNI得不偿失。Ollama暴露的/api/chat接口完全够用而且它支持一次调用常驻内存后续请求不需要重新加载模型这个特性对SpringBoot调用来说非常关键。1.3 先手动验证模型可用性再做集成Code写多了容易出幻觉所以在接SpringBoot之前先用curl验证一下模型是否正常响应curl http://localhost:11434/api/chat -d { model: deepseek-r1:7b, messages: [{role: user, content: 用一句话介绍你自己}], stream: false }返回的JSON大概是这样{ model: deepseek-r1:7b, message: { role: assistant, content: 我是DeepSeek-R1一个开源的大语言模型... }, done: true }注意这里content字段就是最终回答。但如果你用的是真正的R1原版响应里还会带一个reasoning_content字段里面是模型“内心思考”的过程。手动调用时看到它很正常但业务接口如果把这个字段直接透传给用户体验会很怪后面我会专门讲怎么关掉它。模型验证通过后Ollama这个黑盒就算合格了接下来进入SpringBoot集成。2. SpringBoot接入本地模型的两条技术路线Spring AI还是裸HTTP2.1 Spring AI其实很香但这里我选择绕过它Spring Boot 3.x时代官方推出了Spring AI模块里面提供了OllamaChatModel配置好基础URL之后可以直接注入使用代码风格类似老版的RestTemplateOllamaChatModel chatModel new OllamaChatModel(builder, new OllamaApi(baseUrl));好处是Spring AI帮你封装了请求体、响应体、消息历史转换还天然支持ChatClient那套流式API。坏处是它迭代太快1.0版本前后的配置项和类名变动很大而且Ollama API里一些自定义的推理参数比如think、num_ctx在Spring AI的OllamaOptions里支持得并不完整传参时经常要对源码反而拉长了调试时间。所以我最终选择了第二条路直接用Spring MVC自带的RestTemplate或WebClient去调Ollama的HTTP接口。原因很实际依赖最少不引入Spring AI也能跑请求体是自己构造的JSON任何Ollama参数都能直接透传不受封装限制方便查看完整请求和响应出了问题用日志就能排查。如果你在正式项目里已经依赖了Spring AI那就继续用它但如果你是像我一样想快速跑通一个可用的本地模型服务裸HTTP反而是最稳的。2.2 项目初始化与必要依赖新建一个SpringBoot项目只需要引入spring-boot-starter-web连JSON解析都自带Jackson。我用的版本是SpringBoot 2.7或3.x都可以只要JDK8这里对版本要求很宽松。pom.xml里核心依赖就这一段dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency在application.yml里配置模型地址和模型名deepseek: base-url: http://localhost:11434 model: deepseek-r1:7b2.3 手写一个调用DeepSeek-R1的Service直接上代码这是一个最简可用的ServiceService public class DeepSeekR1Service { private final RestTemplate restTemplate; private final String baseUrl; private final String model; public DeepSeekR1Service(Value(${deepseek.base-url}) String baseUrl, Value(${deepseek.model}) String model, RestTemplateBuilder restTemplateBuilder) { this.baseUrl baseUrl; this.model model; this.restTemplate restTemplateBuilder .connectTimeout(Duration.ofSeconds(10)) .readTimeout(Duration.ofSeconds(120)) .build(); } public String chat(String userMessage) { MapString, Object request new HashMap(); request.put(model, model); request.put(messages, List.of(Map.of(role, user, content, userMessage))); request.put(stream, false); MapString, Object options new HashMap(); options.put(temperature, 0.6); options.put(num_predict, 1024); request.put(options, options); String url baseUrl /api/chat; ResponseEntityChatResponse response restTemplate.postForEntity( url, request, ChatResponse.class); if (response.getBody() null || response.getBody().getMessage() null) { throw new RuntimeException(模型返回为空); } return response.getBody().getMessage().getContent(); } public static class ChatResponse { private Message message; private boolean done; public Message getMessage() { return message; } public void setMessage(Message message) { this.message message; } public boolean isDone() { return done; } public void setDone(boolean done) { this.done done; } } public static class Message { private String role; private String content; public String getRole() { return role; } public void setRole(String role) { this.role role; } public String getContent() { return content; } public void setContent(String content) { this.content content; } } }为什么要单独把超时时间拉长到120秒因为本地模型在CPU上跑7B模型时生成1000个token可能需要几分钟如果沿用SpringBoot默认的5秒读取超时客户端一准报Read timed out。这里的经验是connectTimeout可以短但readTimeout必须按最坏情况给足。Controller层更简单RestController RequestMapping(/api/ai) public class AiController { private final DeepSeekR1Service deepSeekR1Service; public AiController(DeepSeekR1Service deepSeekR1Service) { this.deepSeekR1Service deepSeekR1Service; } PostMapping(/chat) public MapString, String chat(RequestBody ChatRequest request) { String reply deepSeekR1Service.chat(request.getMessage()); return Map.of(reply, reply); } }到这一步你已经可以通过POST http://localhost:8080/api/ai/chat调用本地模型了。但别高兴太早第一次真正跑通对话后你会发现还需要处理三个更实际的问题思考模式、超时以及上下文。2.4 关闭思考模式让回答少一段内心戏DeepSeek-R1与传统LLM最大的不同是它有“链式思考”能力在生成正式答复之前会先输出一段推理过程。本地Ollama部署的R1同样保留了这个特性如果你用2.3节的代码请求返回的content里可能会混入“嗯用户问我某件事首先我需要分析...”这类内容。要关闭它需要在请求体里显式传入think参数request.put(think, false);为什么这样有效因为Ollama的DeepSeek-R1模型模板里支持了这个参数设置后会在解码阶段跳过思考token的生成。这个参数不是所有Ollama模型都支持但对R1系列是有效的我在7B和14B模型上都验证过。如果传了think: false依然会输出思考内容还有一种兜底方案手动匹配响应中的reasoning_content字段然后在返回给前端前把它剔除。我在Service里加了这样一段过滤逻辑String rawContent response.getBody().getMessage().getContent(); // 去除思考内容只保留正式回答 int idx rawContent.indexOf(回答); if (idx 0) { rawContent rawContent.substring(idx 回答.length()); }不过这只是“治标”最推荐的做法还是直接塞think参数。3. 从能调到好调超时、流式输出与上下文管理3.1 给RestTemplate配超时时间不然模型思考时连接会断前面已经给了超时配置的示例这里再展开说一下。本地模型和云API有一个巨大差异云API服务端有充足GPU资源首token延迟通常几百毫秒本地模型如果用的是CPU推理一个2000字的回答可能要憋两三分钟。SpringBoot默认的RestTemplate读取超时是5秒HTTP连接通常更久你调两次就知道什么叫做“连接被服务器关闭”。我用RestTemplateBuilder设置RestTemplate restTemplate restTemplateBuilder .connectTimeout(Duration.ofSeconds(10)) .readTimeout(Duration.ofMinutes(5)) .build();建议readTimeout不要小于5分钟。这不算夸张CPU推理7B模型在长文本场景下真的需要这个时间。如果你用的是GPU可以酌情缩短但保险起见还是在配置里留一个可调的deepseek.read-timeout-seconds。3.2 流式输出打字机效果这样实现非流式请求适合后台任务或接口调试但如果你要把模型能力接入聊天框用户等待几十秒看到一句完整的话体验会很煎熬。Ollama支持SSE流式响应只改两个地方第一个是把请求里的stream设为truerequest.put(stream, true);第二个是发送方不能用RestTemplate的postForEntity等同步方法得用能够处理连续数据流的WebClient或者直接读InputStream。我这边用WebClient实现了一个最简版本WebClient webClient WebClient.builder() .baseUrl(baseUrl) .build(); FluxString stream webClient.post() .uri(/api/chat) .bodyValue(request) .retrieve() .bodyToFlux(String.class);响应体每行是一个SSE JSON形如{model:deepseek-r1:7b,message:{role:assistant,content:大家好},done:false} {model:deepseek-r1:7b,message:{role:assistant,content:我是},done:false} {model:deepseek-r1:7b,message:{role:assistant,content:一个AI助手},done:true}解析时按\n切分每行判断是否以data:开头然后交给Jackson反序列化聚合成content作为返回片段直到done为true。如果你用Spring WebFlux可以直接把FluxString返给前端实现SSE如果项目是Servlet栈可以用SseEmitter包装。3.3 上下文对话别把历史全塞进去注意长度写一个简单测试的时候你会发现单独发一个问题模型答得很好一旦连续追问“那再展开讲讲第一步”模型已经忘光你之前问的是什么。这不是模型笨是因为你每次请求都没有携带历史消息。Ollama的/api/chat接口要求把完整对话历史放在messages数组里。我的做法是做一个内存级会话缓存public class ChatSession { private final MapString, ListMapString, String sessions new ConcurrentHashMap(); public void addMessage(String sessionId, String role, String content) { sessions.computeIfAbsent(sessionId, k - new ArrayList()) .add(Map.of(role, role, content, content)); } public ListMapString, String getHistory(String sessionId) { return sessions.getOrDefault(sessionId, new ArrayList()); } public void clear(String sessionId) { sessions.remove(sessionId); } }但这里有个很大的坑7B模型的上下文窗口通常是4096个token左右如果你把十轮对话历史全塞进去可能会触发模型截断或报错。所以我在加历史之前先简单预估token数中文大约 0.6 个token/字如果历史总字符数超过 6000就丢弃最早一半的消息只保留最近几轮。这个策略简单粗鲁但很有效。真要精确控制可以接入jieba或HanLP做tokenizer估算但对大部分测试场景字符串截断已经够用。4. 本地跑DeepSeek-R1的硬件门槛与实测性能4.1 7B模型到底要多少内存量化选哪个文件很多人在Ollama页面看到deepseek-r1:7b就直接拉原版结果发现自己16GB内存的笔记本被吃满系统卡到鼠标都飘。模型对不同量化精度的资源占用差异非常大我用不同版本实测如下模型标识文件大小加载后内存/显存占用CPU推理速度deepseek-r1:7b (FP16)约14GB约14GB约5 tokens/sdeepseek-r1:7b-q8_0约8GB约8GB约8 tokens/sdeepseek-r1:7b-q4_K_M约4.7GB约4.7GB约11 tokens/sdeepseek-r1:1.5b约1.1GB约1.1GB约30 tokens/s如果你有8GB以上显存的N卡3060及以上可以优先选q4_K_M跑起来流畅度和效果平衡得最好。如果只有CPU内存8GB-16GB并且没有独立显卡建议直接用1.5b版本虽然能力弱一些但至少能实时交互。硬上7B CPU推理基本是“等一顿饭的回答”。4.2 并发请求下的真实表现与限流策略本地模型的并发能力远不如云API这是物理条件决定的。我用同一个7B模型发起两个并发请求Ollama默认会排队处理第二个请求等到第一个跑完才返回。听起来能接受但问题出在如果排队超过5个内存消耗会猛增甚至直接把机器打挂。在SpringBoot侧建议加一个简单的令牌桶限流保证同时只有1-2个请求进入推理层private final RateLimiter rateLimiter RateLimiter.create(1.0); // 每秒1个 public String chat(String userMessage) { rateLimiter.acquire(); return doChat(userMessage); }如果你用的是Google Guava的RateLimiter需要提前在pom.xml引入guava依赖。这种粗暴限流在这种场景下是合理的因为本地模型本身吞吐有限限制入口反而能保护整台机器。4.3 我踩过的三个坑端口占用、OOM、中文乱码第一个坑端口占用。Ollama默认监听11434如果你本机开过Docker容器占用了这个端口服务会启动失败。解决方式是启动时指定OLLAMA_HOST0.0.0.0:11435但注意SpringBoot里的base-url也要同步改。第二个坑OOM内存耗尽。Ollama在模型加载时会一次性分配全部所需内存你的电脑如果有16GB物理内存但浏览器、IDE、Docker同时吃掉了10GB再试图加载7B q4模型时会直接Killed进程。建议加载前用free -h检查可用内存至少预留6GB以上。第三个坑中文乱码。因为Ollama接口返回的是UTF-8而老版本SpringBoot的RestTemplate默认ISO-8859-1编码容易导致中文变乱码。解决办法是自定义RestTemplate时设置UTF-8编码或者直接使用StringHttpMessageConverter注册。更现代的做法是全部换成WebClient默认UTF-8少踩很多坑。5. 让本地模型服务从玩具变成生产力5.1 用Spring Cloud把模型服务独立成微服务如果你只是自己本地调试一个SpringBoot启动类就够了。但在团队里更好的做法是把模型推理服务做成一个独立的微服务统一提供/api/ai/chat这样的接口其他业务系统通过Feign或OpenFeign调用。这样做的好处是模型地址、参数、Ollama版本变更都不需要影响到业务方就像调用一个普通的HTTP服务一样。Feign接口定义可以写成FeignClient(name deepseek-local, url ${deepseek.service-url}) public interface DeepSeekClient { PostMapping(/api/ai/chat) R chat(RequestBody ChatRequest request); }这样业务侧完全感知不到“本地模型”的存在哪天模型升级或替换成云API只需要改服务提供方风险可控。5.2 接上向量数据库做一个本地知识库问答本地模型最大的优势是数据不出内网这让它特别适合企业内部知识库场景。我的思路是这样的用Embedding模型如bge-m3把企业文档向量化存入pgvector或Milvus用户提问时先在向量库里检索Top-K相关片段把片段拼接成Prompt交给DeepSeek-R1生成答案。整体的SpringBoot流程也不复杂Controller接收问题调用向量检索Service拿到相关内容然后拼到messages里传给DeepSeek-R1。由于本地模型不花钱你可以放心把上下文塞得比较长Retrieval的效果比闭源API内网调用还稳定。5.3 个人心得什么时候适合本地模型折腾完这一整套我最大的感受是本地模型的成本优势是“边际成本为零”但前提是你得有一台能跑得动的机器。如果你要处理的是敏感数据或者每天有大量轻量级请求本地部署DeepSeek-R1是很划算的。如果只是偶尔跑一两个需求云API按量付费反而更省心毕竟电费和硬件折旧也是成本。我在实际运行中已经把这套服务和内部的一个文档抽取需求接上了每天跑几百次调用Ollama在后台稳定运行了超过一周除了偶尔重启一下SpringBoot应用没有出过什么问题。整个过程下来最值得记住的经验有三条模型版本选q4_K_M请求thinkfalse关思考流readTimeout给够五分钟。剩下的就是在Ollama和SpringBoot之间找到最顺手的那条HTTP通道哪怕是裸调用也完全够用。后来我又试着在同一个服务里挂了deepseek-r1:1.5b做快速回复用7B做深度分析两个模型切换只需要改一个model字段相当方便。这种“本地模型池”的思路让SpringBoot应用对模型资源的调度变得非常灵活也算是一个值得继续往纵深做的方向。本文还有配套的精品资源点击获取