本地部署DeepSeek-R1:SpringBoot+Ollama+Spring AI实战指南
简介随着大模型应用普及本地化部署成为企业保护数据隐私、降低调用成本的重要选择。Ollama作为轻量级模型管理工具屏蔽了模型加载与推理细节提供OpenAI兼容接口Spring AI则统一了LLM访问抽象可让开发者像使用普通Bean一样调用ChatClient。二者结合能将DeepSeek-R1低成本接入SpringBoot服务实现同步/流式对话、上下文记忆与并发控制。本文从本地环境搭建出发详解模型变体选择、Spring AI配置、REST接口封装及真实避坑经验适合需要在内网构建AI服务能力的Java工程师参考。1. 本地跑deepseek-r1到底图什么把大模型变成SpringBoot里的一个Bean很多人看到“本地免费使用deepseek-r1”第一反应是下载一个几百GB的模型文件然后焦虑显卡。实际上本地部署真正值钱的地方在于数据不出内网、请求零成本、可以按自己业务调参。而SpringBoot Spring生态这套组合解决的是“模型有了之后怎么接进业务代码”的问题——你不需要自己写HTTP客户端去拼JSON不需要手工管理对话上下文更不需要把模型推理进程和Web服务割成两套难维护的系统。我用这套方案给内部工具做过几轮改造从“写Python脚本调Ollama接口”进化到“SpringBoot启动后直接注入一个ChatClient当普通Bean用”整个链路是顺的。这篇文章按落地顺序来先把deepseek-r1在本地拉起来再用Spring AI接入SpringBoot最后给出一份避坑清单。适合两类读者一是想在公司内网搭一个免费AI接口的Java工程师二是折腾过Ollama但不知道怎么优雅接入Web服务的Spring玩家。2. 本地部署deepseek-r1Ollama安装与模型变体选择2.1 为什么选Ollama而不是自己起一个模型服务本地跑大模型绕不开“谁来加载模型、谁来管显存、谁来暴露接口”这三件事。常见的方案有四种直接用transformers库写Python脚本、用llama.cpp自己编译、用Docker跑官方镜像、用Ollama。我试过前三种最后还是回归Ollama原因很直接。transformers方案需要自己处理Python环境、CUDA版本、模型分片加载跑通第一句对话就要折腾半天而且和SpringBoot属于两个世界——要么用命令行调Python脚本要么再包一层HTTP服务中间全是胶水代码。llama.cpp性能好但编译参数多模型量化文件也要自己找对Java团队不友好。Docker方案能跑但显存分配、模型热切换、日志查看都得自己写脚本维护。Ollama把“加载模型”和“暴露接口”打包成了两个命令ollama serve启动常驻服务默认监听11434端口提供OpenAI兼容的/v1/chat/completions接口ollama pull负责下载和管理模型。这意味着SpringBoot这边不用关心模型推理细节只当它是一个本地的远程API。更关键的是Ollama内置了量化运行能力显存不够时可以把一部分层卸载到CPU这在模型参数量超过单卡显存时是救命功能。2.2 一条命令拉取deepseek-r1安装、下载与验证安装Ollama在Windows、macOS、Linux上都有对应安装包。Linux服务器上常见做法是执行官方安装脚本这里我用Linux命令说明# 安装OllamaLinux curl -fsSL https://ollama.com/install.sh | sh # 启动服务 ollama serve提示ollama serve会以前台方式运行生产环境建议用systemd管理安装脚本通常会自动注册服务直接systemctl start ollama即可。服务起来后拉取模型# 拉取deepseek-r1的7b蒸馏版体积小适合验证链路 ollama pull deepseek-r1:7b # 查看本地已有哪些模型 ollama list拉取完成后先用一行命令验证模型能正常出话curl http://localhost:11434/api/chat \ -d {model:deepseek-r1:7b,messages:[{role:user,content:你好用一句话介绍你自己}],stream:false}stream:false表示等完整回复一次返回方便排查问题。返回里会带done字段值为true就说明推理链路没毛病。到这一步deepseek-r1已经在本地跑起来了剩下的工作就是让SpringBoot去调它。2.3 选哪个deepseek-r1变体7b、32b还是官方671b先澄清一个认知deepseek-r1官方发布的是671B MoE架构完整权重本地跑需要多张高端显卡普通开发机根本带不动。Ollama仓库里以deepseek-r1命名的标签实际上是官方蒸馏版基于Qwen/Llama蒸馏和量化版的组合。所以“本地免费使用deepseek-r1”的真实含义是跑它的蒸馏小模型而不是把671B原版塞进你的电脑。我按实际体验把几个常见变体列成表方便你按机器配置对号入座模型标签参数量量化后体积最低配置建议适合场景deepseek-r1:7b7B约4.7GB16GB内存无显卡也能跑链路验证、简单问答、低延迟要求deepseek-r1:14b14B约9GB16GB内存 8GB显存中文质量明显提升通用场景折中deepseek-r1:32b32B约20GB32GB内存 24GB显存复杂推理、代码生成、内网知识库deepseek-r1:70b70B约40GB64GB内存 2×24GB显存接近满血效果但成本高deepseek-r1:671b671B数百GB多卡服务器不推荐个人本地尝试我的经验是如果你只是想验证SpringBoot能不能调通直接上7b十分钟跑通如果要做内部工具真正给人用14b是性价比最高的起点。32b回答质量确实上了一个台阶尤其在推理步骤和代码生成上但你需要先确认机器扛得住。选型时还有个重要的点显存不够会退化成CPU推理32b在CPU上跑速度很感人一条回复等两三分钟很正常这是本地免费要付出的代价。3. 用Spring AI把deepseek-r1接进SpringBoot依赖、配置与第一个对话3.1 Spring AI为什么适合做这件事统一抽象与自动装配Spring AI是Spring生态为LLM应用提供的集成框架。它能被SpringBoot直接管理这意味着模型客户端、对话记忆、提示词模板都能通过自动配置组装好业务代码里只管调用。标题里的“SpringBootSpring”落到实处就是SpringBoot负责启动和装配Spring AI负责把Ollama的HTTP接口抽象成Java对象。有人会问我直接用RestTemplate调Ollama不行吗当然行但后患很多。第一Ollama的响应体比较复杂嵌套的message、usage、done_reason字段要写一堆DTO去接每加一个字段就要改类。第二流式输出要处理application/x-ndjson格式的分行JSON手写解析容易翻车。第三将来想换模型服务商从Ollama换到云端API手写代码要从头改一遍。Spring AI把这些都封装了换服务商时只改配置不看代码。我一般建议团队用Spring AI的另一个理由是它内置了ChatMemory抽象。大模型应用最难缠的“多轮对话记忆”在Spring AI里通过一个Advisor就挂上不需要自己把历史消息拼进数组。这部分在最后一章展开讲先把单轮调用跑起来。3.2 引入依赖与application.yml最简配置Spring Boot项目引入Spring AI的Ollama Starter即可。注意Spring AI版本要和Spring Boot版本对齐我用的是Spring Boot 3.x Spring AI 1.x组合依赖如下dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-ollama-spring-boot-starter/artifactId version${spring-ai.version}/version /dependency提示Spring AI的1.0版本前后API有较大调整如果你用的是0.8.x旧版后文的ChatClient写法会略有不同。建议新项目直接上1.x。配置文件只改两处Ollama服务地址和模型名。最小配置如下spring: ai: ollama: base-url: http://localhost:11434 chat: options: model: deepseek-r1:7b temperature: 0.6 num-predict: 2048base-url指向Ollama服务的根地址Spring AI会自己拼/api/chat等路径。model必须是ollama list里能看到的模型名。temperature控制随机性deepseek-r1这类推理模型建议0.6以下太高容易胡说。num-predict限制最大生成token数本地模型没这个限制容易生成到天荒地老。3.3 第一段Java代码把“你好”发给本地模型Spring AI 1.x里最核心的入口是ChatClient它长得像RestTemplate但语义上更接近“和模型对话”。先注入并调用一次最简单的对话import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.ChatClient.Builder; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.stereotype.Service; Service public class DeepSeekService { private final ChatClient chatClient; public DeepSeekService(ChatClient.Builder builder) { this.chatClient builder.build(); } public String chat(String message) { ChatResponse response chatClient.call( new Prompt(message) ); return response.getResult().getOutput().getText(); } }代码逻辑不复杂构造器通过ChatClient.Builder构建客户端因为Spring Boot的自动配置已经把我们配置的Ollama地址和模型参数绑定进去了builder.build()拿到的就是一个“指向deepseek-r1:7b的对话客户端”。call方法返回完整的ChatResponse里面嵌套了三层getResult()拿到Generation再getOutput()拿到AssistantMessage最后getText()才是字符串内容。这里有个容易踩的坑Prompt(message)只传了用户消息没有任何系统提示词。deepseek-r1的蒸馏版本在没有系统提示的情况下回答语言风格可能随机漂移——有时中文有时英文。所以生产代码里建议用SystemPromptTemplate或直接构造ChatMessage列表把角色和内容写清楚ListChatMessage messages List.of( new SystemMessage(你是deepseek-r1模型请用简体中文回答回答应包含推理步骤。), new UserMessage(message) ); ChatResponse response chatClient.call(new Prompt(messages));到这一步已经能通过一个Service类的chat方法拿到本地模型的回复。后面要做的就是把DeepSeekService暴露成REST接口让浏览器或其他系统能调用。4. 把本地模型做成REST接口同步调用、流式输出与参数调优4.1 同步接口适合内部工具与离线任务如果调用方是后端服务比如定时任务、内部管理后台、批处理脚本同步接口足够用。它的优点是实现简单客户端等一次HTTP请求就能拿到完整结果不需要处理事件流。代码结构就是一层薄薄的Controller包住Serviceimport org.springframework.web.bind.annotation.*; RestController RequestMapping(/api/ai) public class DeepSeekController { private final DeepSeekService deepSeekService; public DeepSeekController(DeepSeekService deepSeekService) { this.deepSeekService deepSeekService; } PostMapping(/chat) public MapString, String chat(RequestBody MapString, String request) { String answer deepSeekService.chat(request.get(message)); return Map.of(answer, answer); } }同步接口的调用方视角很直观POST一个JSON里面带message字段响应里拿answer字段。这个接口可以直接用curl验证curl -X POST http://localhost:8080/api/ai/chat \ -H Content-Type: application/json \ -d {message:11等于几}同步接口有个细节要注意本地模型推理是耗时的CPU跑7b模型生成100个token可能要几十秒所以Web层的请求超时时间要放开。Spring内置的server.tomcat.connection-timeout只控制连接建立不控制请求处理时间真正可能拦路的是网关或Nginx的proxy_read_timeout默认60秒很容易断。如果部署在Nginx后面记得把这个参数调到300秒或更高。4.2 流式输出用SSE让用户看到逐字回复面向用户的页面如果走同步接口体验是“转圈30秒然后一次性弹出全文”。换成流式输出模型每生成一个token就推给浏览器用户看到的是打字机效果体感快很多。Spring AI的chatClient.stream方法返回FluxChatResponse配合Spring MVC的SSE能力可以直接推流import org.springframework.http.MediaType; import reactor.core.publisher.Flux; PostMapping(value /chat/stream, produces MediaType.TEXT_EVENT_STREAM_VALUE) public FluxString chatStream(RequestBody MapString, String request) { return chatClient.stream(new Prompt(request.get(message))) .map(response - response.getResult().getOutput().getText()); }逻辑说明stream方法和call一样接收Prompt区别是返回Flux响应式流。每次模型生成一个增量Spring AI会把它包装成ChatResponse推过来map操作提取其中的文本片段。produces TEXT_EVENT_STREAM_VALUE告诉浏览器响应是SSE格式前端用EventSource或fetch读流即可。前端用fetch读SSE有个坑浏览器对EventSource只支持GET请求而我们的接口是POST。实际做法是用fetch配合ReadableStream手动解析或者把接口改成GET并把消息放在查询参数里。为了不把代码搞复杂我通常这样配合前端fetch发起POST拿到response.body后通过getReader()逐块读取文本按data:前缀切割。如果你的前端团队不熟悉流式处理最简单的降级方案是后端流式接口照写前端暂时用同步接口顶上后面再优化体验。4.3 三个必调的生成参数temperature、num_predict与系统提示词流式和同步接口跑通后就该处理回答质量了。本地模型的可用性很大程度上取决于参数我整理三个必调项参数建议区间调低的效果调高的效果我的默认值temperature0.3 ~ 0.8回答保守、稳定、少幻觉回答多样、有创造力但容易跑偏0.6num_predict512 ~ 4096响应快但长文被截断能生成完整长文等待更久2048repeat_penalty1.0 ~ 1.3可能循环重复减少重复但可能打断推理节奏1.1temperature对deepseek-r1的影响尤其明显。这个模型在0.6以上时推理步骤容易发散可能从“计算11”跳到“顺便解释一下数学史”。内部工具场景建议0.4到0.6需要创意文案再调高。系统提示词是另一个隐藏关键。deepseek-r1的蒸馏模型对中文指令跟随能力不如原版直接问“用中文解释”它可能还是蹦英文。我在Service层固定了一段系统提示“你是deepseek-r1模型使用简体中文回答所有问题先给出推理过程再给出最终结论。”加了这个提示后中文回答的稳定性明显提升这比调任何采样参数都管用。还有一个容易忽略的点num_predict如果设太小长代码补全会在中间截断看起来像模型能力不行。判断模型是不是被截断看响应里ChatResponse的finishReason字段——如果是length就说明到长度上限了不是推理出错。把这个字段透出到接口日志里能省很多排查时间。5. 本地调用deepseek-r1的避坑清单5个真实翻车场景5.1 坑一ollama pull卡住或下载到一半失败现象执行ollama pull deepseek-r1:32b后进度条长时间不动或者下载到某个百分比直接报错退出重新执行又从头开始下载。原因Ollama从公共仓库拉取大文件网络抖动或存储空间不足都会导致中断。7b模型4个多G勉强能扛32b近20G一次拉完的概率直线下降。很多人误以为是模型仓库被墙其实大部分情况是网络不稳或磁盘满了。解决先确认磁盘空间ollama pull前用df -h看/usr/share/ollama所在分区剩余容量模型体积要按量化后体积的两倍预留。如果网络确实不稳先拉体积小的模型验证网络再拉大的或者找一台网络条件好的机器拉完把整个/usr/share/ollama/models目录打包拷到目标机器。Ollama的模型目录是自包含的拷过去重启ollama服务就能识别。5.2 坑二显存不够模型加载到一半进程被杀现象Ollama服务还在但请求模型时响应极慢日志里出现killed字样或者ollama run deepseek-r1:32b后命令行直接卡死接着进程退出。原因启动32b模型需要约20GB显存8GB显卡根本装不下。Ollama的调度器会把一部分层放到CPU但如果总内存也不够操作系统会直接触发OOM杀掉进程。症状就是“服务活着但模型没了”。解决先查显存再选模型。nvidia-smi看显存总量和空闲量显存不够就选14b甚至7b。如果你只有显卡显存小而内存大可以在环境变量里限制Ollama使用GPU的层数export OLLAMA_NUM_GPU20 ollama serveOLLAMA_NUM_GPU代表把模型的前多少层放在GPU剩下的跑CPU。这个值没有绝对标准我的经验是从20开始试观察ollama ps显示的GPU/CPU占用比例再微调。内存也紧张的话可以加OLLAMA_MAX_LOADED_MODELS1强制同时只加载一个模型防止多个模型抢资源。5.3 坑三回答全是英文中文质量明显变差现象模型能正常对话但不管问什么都会先蹦一段英文 reasoning最后才给简短中文结论或者中文回答夹着英文术语读起来很生硬。原因deepseek-r1的蒸馏版尤其是7b指令跟随能力有限它默认用训练数据里占比最高的英文模式来组织回答。这不是模型坏了是没有明确提示它用中文。另一个隐藏因素是采样参数设置不对temperature太高会让模型在语言选择上摇摆。解决在每次对话的消息列表里把系统提示词放第一位明确写“用简体中文回答”并且最好在用户消息里也带一句“请用中文”。双保险之后中文回答的稳定性会好很多。如果仍然频繁切英文把temperature从0.8降到0.5减少随机性对语言选择的干扰。对于内部工具这是性价比最高的解法如果还不行只能换14b或更大模型。5.4 坑四Spring AI升级后ChatClient API编译不过现象项目从Spring AI 0.8.x升到1.x原本注入OllamaChatModel的代码一片飘红chatModel.call()方法直接编译失败。原因Spring AI 0.8.x时代主推OllamaChatModel1.x重构后统一收敛到ChatClient很多方法签名都变了。这是开源框架早期版本迭代的正常现象但确实会坑到升级用户。解决我的建议是不要混用新旧API。新项目直接按1.x的ChatClient.Builder写法老项目升级时先对照Spring AI官方samples里的chat-client示例把OllamaChatModel替换成ChatClient同时留意Prompt构造方式变化——1.x里Prompt(String)构造器仍然保留但推荐用Prompt(ListChatMessage)显式传消息。如果团队里同时在维护多个项目统一锁定同一个Spring AI版本不要有的用0.8有的用1.x否则两套API并存维护成本翻倍。5.5 坑五流式接口前端看不到输出或乱码现象接口返回了text/event-stream类型但前端一直不打印内容或者打印出来了全是乱码尤其是中文。原因一个是响应类型不匹配整个应用的全局Filter或拦截器拦截了响应流并做了缓冲把SSE流“攒”在一起一次性返回前端自然看不到逐字效果。另一个是字符编码问题流式响应的Content-Type里没带charsetUTF-8默认编码和解码不一致导致中文乱码。解决先确认接口的produces是否设置了MediaType.TEXT_EVENT_STREAM_VALUE这是基础。再看项目里有没有自定义OncePerRequestFilter写的响应包装类——有的话要对/api/ai/chat/stream路径放行不做缓冲包装。编码问题在后端指定produces text/event-stream;charsetUTF-8前端读取response.body时按TextDecoder(utf-8)解码。排查时最直接的办法是用curl -N看原始响应头确认Content-Type和分块情况避免扯皮。6. 进阶给deepseek-r1加上上下文记忆并做一次并发验证单轮对话接口能用之后下一步就是处理“多轮对话”和“并发”。这两件事不做应用只能算demo算不上工具。Spring AI的ChatMemory模块可以解决上下文问题。核心思路是把历史消息存在服务端每次请求自动带上最近的N轮。我用的是InMemoryChatMemory加MessageChatMemoryAdvisor代码很轻ChatClient chatClient ChatClient.builder(ollamaChatModel) .defaultAdvisors(new MessageChatMemoryAdvisor( new InMemoryChatMemory(), default, 10)) .build();MessageChatMemoryAdvisor的构造参数三个对话存储的实例、会话ID按用户区分、保留最近10轮消息。超过10轮自动丢弃最早的防止上下文越来越长把num_predict预算吃光。会话ID从请求参数里取比如用户ID这样每个用户各聊各的互不干扰。InMemoryChatMemory适合单机部署重启丢记忆如果是集群换RedisChatMemory用法一样。并发验证我用的办法很简单写一个shell脚本用ab模拟20个并发请求观察两个指标——接口成功率、平均响应时间。目的是确认本地模型在多人同时访问时的真实表现。ab -n 20 -c 5 -p body.json -T application/json \ http://localhost:8080/api/ai/chat-c 5表示5个并发body.json里放{message:介绍一下你自己}。如果没有ab用curl加后台跑20个也行。跑完看两个数Failed requests必须是0Time per request如果从几十毫秒涨到几秒说明推理排队了。这是因为本地模型推理是串行的显存不够时并发请求会在Ollama内部排队。如果并发结果不理想最有效的兜底是给Service层加一个信号量限流private final Semaphore semaphore new Semaphore(2);Semaphore(2)限制同时最多两个推理请求其余请求在Spring层排队而不是挤爆Ollama这样系统的行为更可预测。我习惯把限流数设成显存能支撑的并行模型数7b模型在12GB显存下跑2个实例问题不大超过这个数性能会断崖下跌。这套方案我从Spring AI 0.8.x一路用过来最大的体会是“本地免费”的真正成本不在钱而在折腾。把Ollama跑稳、把Spring AI版本对齐、把并发限流设计好后面就剩日常维护了。希望这些实践经验能帮你少走弯路把deepseek-r1真正用起来。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →