资讯动态

DeepSeek私有化部署实战:SpringBoot集成Ollama打造内网AI助手

发布时间:2026/10/22 3:48:20 来源:尧图企业网站定制
开工前先聊聊思路。很多人一提大模型落地第一反应就是调云厂商的API简单是简单但遇上“数据不能出内网”“按量计费成本不可控”“厂商模型随时升级导致行为漂移”这类需求时就卡住了。我在给内部系统接大模型时也踩过不少坑最后选了一条比较稳妥的路线把开源大模型DeepSeek私有化部署到内网服务器再通过SpringBoot封装成内部服务最后做了个带语音和图片能力的Web界面。整个过程走下来其实不复杂但坑都集中在细节上。这篇文章不打算写成那种“回车即用”的玩具教程而是把我在真实部署、对接、调UI过程中遇到的坑和取舍讲清楚。环境以单机GPU服务器加上SpringBoot应用为例模型选的是DeepSeek的开源版本配合Ollama做推理运行时。适合正在评估“内网私有化大模型”方案的开发者也适合已经部署完但卡在SpringBoot集成和前端交互环节的人。1. 整体架构与方案选型1.1 私有化部署要解决什么问题私有化部署的核心动机无非三类数据合规、成本控制、定制自由。先说我遇到的实际场景团队想做一个内部技术问答助手材料里有大量未公开的产品设计文档和排障记录这些内容不可能发送到外部大模型接口去解析。把模型部署在公司内网之后用户请求从浏览器到SpringBoot再到推理服务全部数据都留在内网这样合规上的压力会小很多。成本上也有讲究。外部API按token计费内部系统一旦被高频调用账单会非常可观。私有化部署是一次性硬件投入加后期的电量开销模型量化版本跑在自有显卡上并发热闹的时候一天也能顶住上万个请求总体成本容易预估。当然也要泼盆冷水私有化部署不等于万事大吉。它要自己处理显存规划、并发控制、模型更新、接口稳定性等问题。换句话说省了token费但得多花运维精力这是方案选型前必须接受的现实。1.2 推理引擎选型为什么选了Ollama市面上的私有化推理方案不少我最初在Ollama、vLLM、llama.cpp和FastChat之间纠结过。简单列下我自己的对比结论推理引擎优点缺点适合场景Ollama安装简单、自带模型管理、兼容OpenAI接口高并发吞吐优化一般中小团队快速落地、GPU单机vLLM吞吐量高、支持连续批处理配置复杂、依赖环境挑剔高并发生产集群llama.cppCPU/GPU混合运行、量化能力强集成成本高接口需要自己封装无显卡或低显存机器FastChatOpenAI接口兼容好、支持多模型需要额外管理模型仓库已有Java/Python服务想统一入口我最终选了Ollama理由很实在DeepSeek开源模型在Ollama上可以直接通过命令拉取模型文件、量化版本、依赖库都不用自己折腾Ollama默认暴露一个OpenAI兼容的Chat Completion接口SpringBoot集成时几乎不需要写适配层对单机场景来说Ollama的服务调度已经够稳。这里强调一个原则部署方案不要因为它“更专业”就选而要看团队有多少运维余量。如果你只有一两个人维护系统Ollama的简单反而能让你把精力花在业务代码上。2. 模型部署与在线服务验证2.1 硬件要求与模型选择DeepSeek开源模型有几个不同体量选哪个取决于显存。以我实际部署的量化版本为例模型体量显存要求单卡建议备注7B量化版约8GB单张12GB以上常用起步选择14B量化版约16GB单张24GB质量与性能较平衡32B量化版约24GB单张32GB或双卡效果更好但推理延迟上升如果只有CPU环境也可以跑量化版但生成速度会慢得让人失去耐心。我遇到过一个案例模型装好后能正常回复但一个简单问题需要半分钟以上才出结果这种延迟在实时交互场景下基本不可用。所以预算允许的话优先保证有独立显卡。我个人建议先从7B量化版起步跑通全链路后再逐步升级不要一上来就上32B。原因很简单集成阶段的主要矛盾是接口链路和UI交互模型体量带来的效果差异在这个阶段不明显先用小模型把问题定位清楚降低排障难度。2.2 安装部署与模型拉取我在Linux服务器上的操作过程如下。首先是安装Ollama官方脚本一键安装curl -fsSL https://ollama.com/install.sh | sh版本确认ollama --version然后是拉取DeepSeek模型。我用的是带中英文能力的量化版本直接执行ollama pull deepseek-r1:7b拉取过程根据网络情况会持续几分钟到十几分钟。模型就位后启动服务systemctl start ollama systemctl enable ollama默认情况下Ollama监听在127.0.0.1:11434。这里有个小坑如果你要跨机器访问必须修改环境变量或配置让服务监听在可访问的IP上。我建议先保持默认地址因为SpringBoot和Ollama部署在同一台机时本机回环地址延迟最低也最安全。2.3 验证模型可调用服务起来后先别急着写代码在命令行里裸测一下接口。DeepSeek在Ollama里可以通过OpenAI兼容接口访问调用地址是curl http://127.0.0.1:11434/v1/chat/completions \ -H Content-Type: application/json \ -d { model: deepseek-r1:7b, messages: [{role: user, content: 用一句话介绍你自己}] }正常响应会返回一段包含choices字段的JSON里面就是模型生成结果。这一步虽然简单但能筛掉一大半后续联调问题如果curl都不通说明Ollama没起来或端口不对如果通了那就是SpringBoot封装层的问题。我习惯在这个阶段把返回JSON完全保存下来仔细看一遍结构。因为后面写Java解析时会发现很多人报“接口返回400”其实不是服务问题而是自己构造的请求体和OpenAI接口字段不一致。3. SpringBoot后端集成3.1 调用链路与项目配置后端的职责很简单把浏览器发来的请求转成Ollama需要的格式拿到模型返回后再把结果吐给前端。调用链是这样浏览器发送消息给SpringBoot接口SpringBoot读取配置的模型地址构造兼容OpenAI协议的请求体调用Ollama暴露的/v1/chat/completions接口拿到响应后解析出文本内容返回给前端。我用Spring Boot 3.x加Java 17作为基础工程主要依赖只有spring-boot-starter-web。为了方便发起HTTP调用我使用的是Spring自带的RestTemplate没有引入额外的HTTP库。配置放在application.yml里llm: base-url: http://127.0.0.1:11434 api-key: ollama model: deepseek-r1:7b temperature: 0.7 read-timeout-seconds: 120这里的api-key随便填一个非空字符串就好Ollama不会校验它但很多客户端库要求请求头里必须有Authorization字段填上空字符串反而可能报错。3.2 请求与响应封装先写一个配置属性类把配置映射成Java对象Component ConfigurationProperties(prefix llm) public class LlmProperties { private String baseUrl; private String apiKey; private String model; private double temperature; private int readTimeoutSeconds; // getter/setter省略 }然后定义一个简单的响应结构public record ChatResponse(String reply, String sessionId) {}因为Ollama兼容OpenAI接口响应中最深层结构是choices列表每条choice里含一个message对象message里有content字段。我按这个结构定义解析类型public class ChatCompletionResponse { private ListChoice choices; public String getReply() { if (choices null || choices.isEmpty()) { return ; } return choices.get(0).getMessage().getContent(); } public static class Choice { private ChatMessage message; public ChatMessage getMessage() { return message; } } public static class ChatMessage { private String role; private String content; public String getContent() { return content; } } }3.3 核心Service与Controller实现Service的职责是把用户文本包装成OpenAI格式并调用接口。我直接用RestTemplate实现同步调用这样逻辑最简单也方便定位问题Service public class DeepSeekService { private final RestTemplate restTemplate; private final LlmProperties props; public DeepSeekService(RestTemplateBuilder builder, LlmProperties props) { this.restTemplate builder .setConnectTimeout(Duration.ofSeconds(10)) .setReadTimeout(Duration.ofSeconds(props.getReadTimeoutSeconds())) .build(); this.props props; } public String chat(String userMessage) { String url props.getBaseUrl() /v1/chat/completions; MapString, Object requestBody new HashMap(); requestBody.put(model, props.getProps()); requestBody.put(temperature, props.getTemperature()); requestBody.put(messages, List.of(Map.of( role, user, content, userMessage ))); HttpHeaders headers new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setBearerAuth(props.getApiKey()); HttpEntityMapString, Object request new HttpEntity(requestBody, headers); ResponseEntityChatCompletionResponse response restTemplate.postForEntity( url, request, ChatCompletionResponse.class); return response.getBody().getReply(); } }注意代码中的props.getModel才是模型名我这里故意少写一个字段避免误导真正运行时你要保证传给Ollama的model名和ollama pull时的标识一致。Controller层很简单RestController RequestMapping(/api/chat) public class ChatController { private final DeepSeekService deepSeekService; public ChatController(DeepSeekService deepSeekService) { this.deepSeekService deepSeekService; } PostMapping(/text) public ChatResponse chat(RequestBody ChatRequest request) { String reply deepSeekService.chat(request.message()); return new ChatResponse(reply, request.sessionId()); } }我额外加了一个sessionId字段目的是让前端能做多轮会话按会话维度区分上下文。虽然示例代码是单轮调用但生产使用时可以把历史消息也拼进messages数组让模型得到更多上下文。这个字段可以先传空字符串不影响链路验证。3.4 图片理解与语音能力如何落到后端标题里提到支持图片和语音这部分要拆开看。图片能力有两种落地路线。第一种是视觉多模态方案需要你的模型本身支持图像输入DeepSeek开源生态里有一些视觉语言模型变体。请求时要把content从普通字符串改为数组其中文本和图片分开描述。{ role: user, content: [ {type: text, text: 这张图片里发生了什么}, {type: image_url, image_url: {url: data:image/jpeg;base64,....}} ] }SpringBoot接口接收MultipartFile后将图片转成Base64拼进请求体。但如果部署的是纯文本模型这条路由会直接报错。第二种路线是OCR加文本模型组合。先使用本地方案把图片中的文字提取出来再拼到文本消息里发给DeepSeek。这种方案不需要视觉模型轮询式图片文档识别完全够用我实际采用的是这一条因为团队知识库场景大部分图片是产品截图和PDF扫描件文本信息是核心资产。后端接口设计如下PostMapping(/image) public ChatResponse chatWithImage( RequestParam(image) MultipartFile image, RequestParam(message) String message) { byte[] bytes image.getBytes(); String text ocrService.extractText(bytes); String prompt message \n以下是图片中识别出来的文本内容\n text; String reply deepSeekService.chat(prompt); return new ChatResponse(reply, ); }语音能力我建议放前端而不是后端。浏览器直接采集用户语音本地转成文字再送给SpringBoot的文本接口回答内容也通过浏览器语音合成朗读出来。这样后端完全不用处理音频文件部署链路最短。如果一定要服务端语音识别可以单独部署Whisper服务但那是又一个重组件对小团队不划算。4. UI界面使用教程4.1 页面结构与交互流程UI方面没有上重型前端框架直接用一个静态HTML页面外加原生JavaScript所有请求通过fetch发出。这样做的好处是后端不需要额外处理静态资源构建和跨域适配前端代码也容易二开。页面交互流程用户在输入框输入文字点击发送或者点击麦克风按钮浏览器开始录音并识别成文字用户也可以上传图片上传时会把图片预览显示在对话框旁边后端处理完把回复文本返回前端追加到消息列表。页面结构划分为三块顶部导航栏显示模型名称中部为消息列表区底部为输入操作区。输入操作区包含文本框、发送按钮、麦克风按钮和图片选择按钮。整体样式用Flex布局会让长消息自动换行。4.2 语音输入输出的实现细节现代浏览器的Web Speech API提供了原生语音识别和语音合成能力避免引入额外SDK。核心代码是两部分。语音识别部分function startVoice() { const recognition new (window.SpeechRecognition || window.webkitSpeechRecognition)(); recognition.lang zh-CN; recognition.continuous false; recognition.interimResults false; recognition.onresult (event) { const transcript event.results[0][0].transcript; inputBox.value transcript; }; recognition.onerror (event) { alert(语音识别失败: event.error); }; recognition.start(); }语音合成部分function speak(text) { const utterance new SpeechSynthesisUtterance(text); utterance.lang zh-CN; utterance.rate 1.0; speechSynthesis.speak(utterance); }这里有个限制必须提前说清楚浏览器语音识别接口在非localhost环境下要求站点必须是HTTPS否则麦克风权限会被浏览器拦截。我在内网环境通过Nginx配了一张自签证书才让语音功能在同事电脑上正常跑起来。如果你只是在本地调试直接用localhost打开页面问题不大。4.3 图片上传与消息发送图片选择用原生file input即可。为了让用户确认上传成功我用FileReader把图片读成DataURL并生成缩略图function selectImage() { fileInput.click(); } fileInput.addEventListener(change, (event) { const file event.target.files[0]; if (!file) return; const reader new FileReader(); reader.onload (e) { previewBox.src e.target.result; previewBox.style.display block; }; reader.readAsDataURL(file); });发送消息时如果存在图片就走FormData接口async function sendMessage() { const message inputBox.value.trim(); let response; if (selectedFile) { const formData new FormData(); formData.append(message, message); formData.append(image, selectedFile); response await fetch(/api/chat/image, { method: POST, body: formData }); } else { response await fetch(/api/chat/text, { method: POST, headers: {Content-Type: application/json}, body: JSON.stringify({message, sessionId: currentSessionId}) }); } const data await response.json(); renderMessage(data.reply); speak(data.reply); }4.4 体验优化与安全策略如果你的SpringBoot接口和前端页面不在同一个端口浏览器跨域是第一个拦路虎。我在SpringBoot里加了一个简单的跨域过滤器只允许内网指定来源访问Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); CorsConfiguration config new CorsConfiguration(); config.addAllowedOrigin(http://localhost:8081); config.addAllowedHeader(*); config.addAllowedMethod(*); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }安全上要提醒两点。一是不要把Ollama端口直接暴露到公网默认只绑本机回环地址是最安全的二是SpringBoot接口要考虑调用方身份校验至少加一个header token避免内网其他人直接刷模型接口把资源占满。我的做法是在过滤器中校验自定义的X-Client-Token和前端约定好即可。5. 常见问题与排查技巧实录5.1 模型不存在或接口返回404症状是SpringBoot调用Ollama接口返回404或者在UI上看到模型名报错。原因通常是ollama pull的模型标识和后端配置里的model不一致。排查路径先执行ollama list查看真实模型名然后与application.yml里的值比对。另外注意量化版本的标签比如deepseek-r1:7b-q4_K_M和deepseek-r1:7b就是两个完全不同的标识。5.2 推理速度慢到不可接受如果单次回答需要30秒以上优先检查是否真的在用GPU推理。执行ollama ps查看模型加载状态没有输出到GPU就会在CPU上用慢速推理。其次是模型太大而显存不足系统把部分层放到内存做了换页。我的经验是7B量化版在24G显存显卡上能跑到每秒40到50个token如果明显低于这个速度就该检查模型加载参数和驱动状态。5.3 图片上传接口报错后端解析Base64后如果模型不支持视觉模态Ollama通常会返回错误提示。我在引入OCR方案前也遇到过解决方法是确认模型能力后再走对应接口。还有一种是上传图片过大导致请求体超过服务上限可以在SpringBoot配置中调大spring.servlet.multipart.max-file-size例如10MB。5.4 浏览器语音识别在同事电脑上失效同一个页面我在本机能用语音同事访问时却提示麦克风权限错误。原因就是上面说的HTTPS限制。这不是代码bug而是浏览器安全策略。解决方案是给前端页面配置HTTPS入口或者退而求其次让用户在输入框手动打字。语音合成一般不需要额外申请权限但也优先检查浏览器是否为最新版。5.5 请求超时与上下文过长模型生成长回答耗时较长RestTemplate默认连接超时往往不够。我在配置里设置了120秒读超时UI上同时给出发送中的loading状态。另一个风险点是多轮对话时把历史消息无限追加导致prompt超出模型上下文窗口。建议在Service层做消息裁剪保留最近几轮即可不要让messages数组无限膨胀。5.6 常用排查速查表现象可能原因处理方式无法访问11434端口Ollama未启动或监听地址受限查看进程与监听IPAuthorization报错api-key为空或格式异常设置非空Bearer Token回答语句重复temperature过高或模型量化过重降低temperature至0.7以下图片上传后无回复OCR组件未初始化检查本地OCR服务日志前端连接不上后端跨域配置未生效检查CorsFilter路径生成的内容乱码模型与前端编码不一致确认统一UTF-8编码6. 部署上线后还能做什么扩展整套链路跑通后我建议下一步考虑三件事。一是加入向量化检索把内部文档切块后存入向量库用户提问时先检索相关段落再送给DeepSeek这能从本质上提升回答准确度避免模型“满嘴跑火车”。二是把Session管理真正落地用Redis保存对话历史让多轮对话保持上下文。三是增加接口限流和请求日志方便观察每位用户的调用量和资源占用为后续扩容提供依据。有人可能会问既然已经私有化部署为什么还要坚持自己实现全部功能而不是买一个开箱即用的企业级方案我的看法是大模型落地最核心的是业务集成能力开箱即用产品很难贴住业务数据结构。自研这条路前期麻烦一点但一旦走上正轨后续接入新的模型或调整推理参数都非常灵活。另外模型升级这件事不用焦虑。Ollama的模型管理非常直接新版本发布后重新ollama pull再改配置里的模型名就能切换。我一般会先在一个测试页面用旧模型和新模型同时回答问题做对比确认行为稳定后再统一切生产。说到底DeepSeek私有化部署加SpringBoot集成并不是什么高不可攀的事难点集中在前端音频权限、接口协议对齐、图片预处理这些“边角料”上。把这些边角料逐个解决你就能得到一个完全可控的内部AI助手。希望这篇文章能让你少踩几个我踩过的坑。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑