☰
内网AI知识库与DeepSeek离线部署:从选型到安全加固全指南
2026/10/6 1:36:53 网站建设 项目流程

简介:这是一份面向政企内网场景的DeepSeek离线部署实战指南,定位清晰:帮助IT运维人员和技术爱好者在无互联网环境下搭建安全、高效的AI知识库。资源为单个PDF文件,大小5.28MB,轻量易存,但内容覆盖从离线资源准备、环境部署、数据导入到权限管理的完整链路。离线部分给出模型、Ollama安装包、客户端及相关工具的获取途径和导入方法;环境部署支持Windows、Linux、macOS,并兼容国产系统与多种CPU架构;RAG实现提供两种方案,其中通过Cherry Studio数据馈送的方式对新手尤其友好。针对内网安全,还重点说明了Ollama API访问控制、Lobe Chat访问控制以及宝塔面板安全加固等设置。内容同时包含模型完整性校验、大文件分割合并、Modelfile导入、安全网闸传输等实操细节,是一份能从准备到上线全程照做的参考手册。目前已有1369人学习,适合内网部署新手按步骤操作,也适合有经验者查阅排错。

1. 内网AI知识库与DeepSeek离线部署:为什么值得投入

一台完全不联网的服务器,几张国产加速卡,几十GB的模型权重文件,就能让公司内部的制度文档、产品手册、客服话术全部变成可检索、可追问的知识库——这是内网AI知识库最典型的落地画面。DeepSeek离线部署的核心难点往往不在模型本身,而在离线依赖、国产化适配和安全加固这三件事:依赖从哪里来、算子在不在、接口给谁用。适合谁?注重数据私有化的政企项目、研发网与生产网隔离的制造企业,以及不想把内部资料交给外部API的团队。这篇笔记按选型、部署、知识库链路、排错到加固的顺序,把整条路线讲透。

2. 硬件选型与国产化适配:先算显存,再选加速卡与操作系统

2.1 先估显存:模型规模、上下文长度与量化档位

每次有人问内网AI知识库用什么模型,我第一句话都是:先看显存。DeepSeek开源仓库里有一批可用于私有化部署的模型,知识库问答场景一般用DeepSeek-R1-Distill-Qwen系列,1.5B到32B都有;如果团队想要更强的通用能力,V3/R1主模型是MoE架构,显存和工程复杂度是另一个数量级,绝大多数内网项目扛不住。模型权重占显存很好算:FP16下大概1B参数占用2GB,7B约14GB,14B约28GB,32B约64GB。但模型服务不是只有权重,KV cache才是变量最大的那块。给模型开4096的上下文和开32768的上下文,显存占用差好几倍,这也是很多翻车现场的共同原因。我的建议是预算内先上14B,拿FP16或BF16跑通,上下文长度从8K开始压测,不够再换32B。

量化放到后面考虑。国产加速卡对AWQ、GPTQ这些量化格式的支持差异很大,模型文件能加载但算子不支持的情况很常见,所以第一版不要一上来就量化。判断一张卡够不够,简单办法是把--max-model-len和--gpu-memory-utilization固定,启动后看日志里KV Cache pool size。gpu-memory-utilization我一般设0.85,留出动态余量给并发请求;网上那些一上来填0.95的教程,多半没有经历过满负载时隐存溢出。

2.2 国产化适配的三个层次:加速卡、推理后端与操作系统

国产化适配是我见过最像黑匣子的部分,原因在于加速卡的软件栈和CUDA不是一回事。昇腾对应的是CANN和torch_npu,寒武纪是torch_mlu,海光DCU兼容ROCm;同一个模型在CUDA上跑几条命令就起来,到了这些卡上,先要确认算子覆盖率。下表是我评估加速卡时常用的对照:

加速卡常见适配路径部署注意
昇腾310P/910B系列vllm-ascend插件或MindIECANN版本与驱动要匹配,量化算子逐项测试
寒武纪MLU370系列厂商提供的PyTorch+MLU框架软件栈更新快,镜像要锁版本
海光DCUROCm技术栈与主流x86服务器兼容性好,依赖包要自己攒

适配路线上我一般首选昇腾,因为昇腾的vLLM插件和MindIE相对成熟,公开踩坑记录也多;寒武纪和海光的项目多依赖厂商驻场支持,可以先让厂商出一个能跑BF16或量化模型的样例,再决定是否复制到业务环境。这里有个经验:官方提供的适配容器往往很大,但内网部署最省事的就是直接拿厂商整机配套的操作系统镜像和容器,驱动、CANN版本、PyTorch版本已经对齐过,自己拼容易死在版本矩阵上。

操作系统层面,麒麟V10、统信UOS是国产化环境最常见的两个选项。两者都能跑Python3.8/3.10,但系统自带的安全策略有差异,部署时建议先在测试环境关掉SELinux/AppArmor,跑通后再按安全策略补回来。Python环境我坚持用conda或miniconda离线装,不要直接用系统Python,系统Python一旦被升级或安装其他软件改了依赖,整个服务可能直接挂掉。这个决定能帮你省下大量半夜排查依赖冲突的时间。

2.3 服务架构:模型服务与知识库服务分开部署

内网AI知识库系统不要做成一个大单体。我常见的做法是拆成三个角色:模型推理服务(vLLM暴露OpenAI兼容接口)、文档加工链路(解析、切分、向量化)、向量检索服务。模型服务单独占一台带GPU的机器,文档加工可以跑在CPU机器上,向量库可以放在业务数据库所在机器。拆开的好处是:模型服务重载或者OOM,不会把已经入库的向量一起带走;知识库要更新文档,只需重跑文档加工链路,不碰模型。

如果你的内网已有PostgreSQL,向量库直接用pgvector是最轻量的做法;文档量到了百万级、并发查询高,再上Milvus。文档量小的时候,Chroma也能满足验证需求。我见过太多项目一开始就上三节点Milvus,结果知识库只有几千篇文档,运维成本直接劝退。技术选型要按数据量走,不要按ppt上的架构图走。下一章开始讲模型服务本身怎么部署。

3. 用vLLM部署DeepSeek:离线安装、启动参数与接口验证

3.1 离线安装三步:下载依赖包、拷贝模型、建立目录

内网环境最大的敌人是离线。很多人第一次部署时在外网机器上装好Python环境,然后整个打包传进内网,这在同构Linux环境下可行,但一旦目标机的glibc版本不一致,就是各种玄学报错。更可控的做法是:在外网准备机上执行pip download收集whl包,内网用--no-index离线安装。

# 在外网准备机上,按目标Python版本收集依赖 pip install vllm==0.6.* openai langchain-text-splitters pip freeze > requirements-offline.txt pip download -r requirements-offline.txt -d /tmp/offline_wheels # 把整个目录拷进内网后执行 pip install --no-index --find-links=./offline_wheels -r requirements-offline.txt

这里的关键是requirements-offline.txt要在pip install成功后立刻导出,保证把依赖版本全部锁死。内网机器的Python版本要和准备机一致,否则torch、tokenizers这类编译型包根本不认。vLLM的依赖树非常大,第一次下载会看到大批轮的包,这很正常,漏一个就会在启动时弹ModuleNotFoundError;所以我在准备机上装完vLLM后,会把整个离线包目录压缩留档,下次换机器直接复用。

模型文件的搬运同样要按目录整体处理:

# 确认模型目录结构,config.json、tokenizer.json、*.safetensors 都要在 mkdir -p /data/models rsync -av ./deepseek-ai/DeepSeek-R1-Distill-Qwen-14B /data/models/ # 目录名保持和config.json里的模型名一致,避免后续加载混乱 ls -lh /data/models/DeepSeek-R1-Distill-Qwen-14B

模型存放路径我建议固定为/data/models,不要在/root或/home下建,因为后续LaTeX服务和向量库都要读同一个目录,权限设置一乱,排查起来全是权限问题。如果是从网盘或共享存储拿到的模型,拷贝完先看一遍文件大小和checksum再启动,文件损坏时vLLM的报错往往非常隐晦。

3.2 启动vLLM服务:参数怎么设才不翻车

模型服务启动命令是整个离线部署里最值得反复确认的地方。

python -m vllm.entrypoints.openai.api_server \ --model /data/models/DeepSeek-R1-Distill-Qwen-14B \ --served-model-name deepseek-local \ --host 0.0.0.0 --port 8000 \ --gpu-memory-utilization 0.85 \ --max-model-len 8192 \ --enforce-eager

先解释--served-model-name:知识库和客户端只用这个逻辑名调用模型,以后想换模型文件不用改前端代码,改启动参数就行。--max-model-len 8192对知识库问答够用,如果文档片段很长或要做多轮对话,可以提高到16384,但显存占用会明显上升。--gpu-memory-utilization 0.85是经验值,不要让vLLM把所有显存都分配给模型和KV cache,否则并发请求一到直接OOM。

--enforce-eager是我在国产卡上必开的参数。vLLM默认会构建CUDA graph来加速,但国产加速卡和某些老驱动对这个优化支持不完整,执行到一半报错的情况很多;关掉它虽然性能有一定损失,但换来的是在昇腾、寒武纪、海光这些设备上先跑通。跑通之后再做性能优化,不要一开始就追求完美。

如果你更熟悉新版本vLLM的命令行,很多版本推荐vllm serve写法,参数含义一致,选自己熟悉的入口就行。网上也有DeepSeek Harness这类整合部署脚本,它们把模型下载、依赖安装、服务拉起捆在一起,在外网机器上确实省事,但离线内网环境不要指望一把梭,它默认还是连外网拉依赖,一旦卡住,反而要按本节拆开来排查。

3.3 OpenAI兼容接口:从命令行验证到知识库调用

vLLM启动完成后,先做接口冒烟验证,用curl确认服务活着:

curl http://127.0.0.1:8000/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "deepseek-local", "messages": [{"role": "user", "content": "什么是内网AI知识库?"}], "max_tokens": 256, "temperature": 0.3 }'

响应里会返回choices数组和usage字段。如果这一步报连接失败,先看服务是否还在前台运行,再检查端口监听状态;最愚蠢的错误是vLLM启动时主机名解析慢,服务起来了但端口没绑定,多等几秒再试。下一步用Python的openai库调用,这才是知识库真正会用到的接口方式:

from openai import OpenAI client = OpenAI( base_url="http://127.0.0.1:8000/v1", api_key="local-offline-key", # 离线环境任何值都可以,安全加固在后置网关做 ) resp = client.chat.completions.create( model="deepseek-local", messages=[ {"role": "system", "content": "你是企业知识库助手,回答要简洁准确。"}, {"role": "user", "content": "差旅报销的申请流程是什么?"} ], temperature=0.2, max_tokens=512, ) print(resp.choices[0].message.content)

注意base_url的路径要精确到/v1,漏掉会导致404。api_key在vLLM本地接口可以不校验,但后面接入知识库和前端时,必须在入口层做鉴权,不能把这个裸接口直接暴露给全公司,这是安全加固的基本功,第六章会细讲。接口验证通过后,DeepSeek的本地服务已经可用,接下来要解决的是怎么把企业内部文档变成它能读的知识库。

4. 把本地文档变成知识库:切分、向量化、检索与Prompt拼接

4.1 文档解析与切分:中文切分不要照抄英文参数

知识库的原料通常是Word、PDF、Excel、TXT,还有大量扫描件。先做一个解析层,把PDF转成文本格式;扫描件必须过OCR,这一步我常用RapidOCR这类离线OCR组件,避免把文档内容送到外部识别服务。解析之后有一个反复强调的坑:PDF排版有分栏时,纯文本提取会把两栏内容混在一起,导致同一段文字语义断裂,所以解析后要做一次人工抽检,抽检的比例不用高,但能提前暴露大量问题。文档质量差,后面所有环节都会被拖累。

切分是RAG里最影响效果的一步。很多教程直接写chunk_size=500, chunk_overlap=50,那是对英文语料拍的,中文按字符切会有两个问题:一是切在句子中间,二是切点把专有名词砍断。我一般用递归字符分割器,并把分隔符换成中文句读:

from langchain_text_splitters import RecursiveCharacterTextSplitter splitter = RecursiveCharacterTextSplitter( chunk_size=500, chunk_overlap=80, separators=["\n\n", "\n", "。", "!", "?", ";", ",", " ", ""], ) chunks = splitter.split_text(raw_text) print(f"切分完成,共{len(chunks)}个片段")

chunk_size落在500到800之间比较合适,制度文档句子较长,可以取800;FAQ类短问答,500更贴近问题粒度。chunk_overlap给50到100,用来补偿切点造成的上下文断裂。这里强调一下:separators里中英文标点都要有,因为很多企业文档里混用半角符号。改完切分参数后一定要清空向量库重新入库,否则库里还是旧分块的复盘,等于没改。

4.2 Embedding与向量库:全离线怎么选型

向量化模型是知识库召回质量的关键,我优先选中文友好的开源模型,比如BGE系列的bge-m3,支持多语言,输出1024维,离线直接加载本地目录,不依赖外部服务。在知识库场景,我会配套加一个reranker模型,对检索回来的候选片段重新打分;第一版可以不上reranker,但检索纯度不够时它是很好的补救手段。

向量库选型不要贪大,按数据规模选:

方案部署成本适合规模说明
Chroma很低几千个片段验证阶段,落盘在本地目录
pgvector低几十万片段复用PostgreSQL,运维成本最低
Milvus高百万片段以上分布式,高并发查询

企业知识库起步阶段,pgvector是最省心的选择。初始化语义如下:

CREATE EXTENSION IF NOT EXISTS vector; CREATE TABLE knowledge ( id bigserial PRIMARY KEY, chunk_text text, embedding vector(1024), source_file text, updated_at timestamptz DEFAULT now() ); CREATE INDEX ON knowledge USING hnsw (embedding vector_cosine_ops);

HNSW索引查询快,但首次写入时内存占用明显,离线环境可以先建索引验证,再正式灌数据。如果你用的是Chroma,初始化更简单,一个目录就够了;它适合第一次验证全链路时用,正式投产前再迁到pgvector也不会伤筋动骨。

4.3 检索并拼接Prompt:让DeepSeek只回答知识库内的事

知识库的核心逻辑是:把用户问题向量化,在库里找到最相似的片段,拼进Prompt交给DeepSeek。这一步拼装的质量,直接决定回答是引经据典还是胡编乱造。

def rag_query(question, top_k=5): # 1. 问题向量化 qvec = embedder.encode(question, normalize_embeddings=True) # 2. 向量检索,pgvector使用余弦距离 <=> rows = conn.execute(""" SELECT id, chunk_text FROM knowledge ORDER BY embedding <=> %s LIMIT %s """, (qvec, top_k)).fetchall() context = "\n\n".join(r[1] for r in rows) system_prompt = ( "你是企业内网AI知识库助手。" "只能依据提供的知识片段回答,如果片段里没有答案,明确说不知道。" ) resp = client.chat.completions.create( model="deepseek-local", messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": f"知识片段:\n{context}\n\n问题:{question}"} ], temperature=0.1, max_tokens=512, ) return resp.choices[0].message.content

top_k=5是一个平衡点,太少召回不全,太多上下文被无关片段污染。temperature=0.1压到最低,知识库问答需要确定性,不需要创造性。system prompt里明确要求“没有答案就说不知道”,远比在代码里硬堵幻觉有效。

如果你发现检索结果不对,优先加两样东西:查询改写和重排。查询改写是把用户的口语化问题改写成更适合检索的表达,重排则用reranker对候选片段重新打分。这些功能会各多一个服务,离线环境维护成本上升;我一般第一版不上,等检索命中率确实不够再加。知识库链路跑通后,还可以把服务接到企业微信或飞书机器人,用同样的OpenAI兼容接口就能对接,只需要处理好消息历史列表的长度,否则多轮问答越长越容易触到模型上下文上限。

5. 离线部署与国产化适配的常见问题:五个翻车点排查

5.1 现象1:模型一启动就被OOM杀掉

vLLM日志刚打印出来,进程就消失,这种情况在国产卡上更容易出现。原因是显存估算过于乐观:max-model-len开得太大,KV cache预分配把显存吃满;同时gpu-memory-utilization设成0.95,留给并发请求的余量太小。另一个隐蔽原因是底层PyTorch或CANN版本不对,导致显存分配异常。

解决顺序是:先把max-model-len降到4096,gpu-memory-utilization降到0.8,再加一行--max-num-seqs 4限制最大并发;启动后看日志里KV Cache pool size,确认实际占用。如果仍然启动失败,检查驱动和CANN版本是否匹配,不要在关键设备上冒险用测试版驱动。

5.2 现象2:国产加速卡报NotImplementedError

这类报错通常出现在加载模型时,提示某个算子没有注册到后端。不用急着读算子源码,先确认这个vLLM是为哪类卡编译的。通用vLLM在昇腾上跑,遇到不支持的算子概率极高。

解决方法是换厂商官方推荐的后端:昇腾用MindIE或vllm-ascend插件,寒武纪用厂商提供的适配包。这类生态问题直接找厂商要离线容器镜像,比自己改源码省太多时间。最后同步验证--enforce-eager,它会关掉CUDA graph的预构建,避免因图构建失败导致整个服务起不来。

5.3 现象3:离线pip安装依赖装一半卡死

内网无外网时,pip默认会连PyPI,连不上就一直卡着直到超时;就算指定了本地目录,漏掉的依赖仍会触发网络访问。解决办法是回到第三节的做法:外网准备机用pip download拉全,内网执行--no-index安装。但这里还有个隐蔽问题,torch和tokenizers这类编译型包必须与目标机Python版本一致,否则装上也是坏包。

如果还报缺底层库,比如gcc、openmpi,那不是pip能解决的,需要补操作系统软件源离线包。另有一个Windows环境的特殊案例:有同学把DeepSeek Harness这类工具放在中文路径下,skill文件读取时莫名报权限错,本质是Windows权限模型在搞鬼,把整个目录挪到纯英文路径,问题立刻消失。

5.4 现象4:检索命中但回答跑题

最伤的是知识库明明有答案,模型却答非所问。先别急着怪模型,把检索到的片段打印出来看一眼。大概率是切分把答案切断了,或者top_k太小,正确答案排在5名以外;也有可能是embedding模型对专有名词不敏感,导致召回的片段里混入大量同主题但无关的内容。

解决思路:把chunk_overlap调大到100,top_k调到8,然后人工核对召回片段。如果召回片段本身没问题但回答仍偏,那就是Prompt拼接的锅——没有告诉模型“优先引用片段原话”,需要强化system prompt。还有一种常见情况是知识库里有同一份制度的多个历史版本,检索优先命中旧版本,数据源清洗比调参更有价值。

5.5 现象5:接口通了但前端或办公IM接不进来

vLLM服务正常响应,curl在服务器本机也正常,但Web前端和办公IM机器人就是连不上。这类问题多数出在网络边界:要么前端机器不在允许列表里,要么前端请求带了OPTIONS预检,vLLM没做跨域处理。解决时在服务入口层加一个nginx或自研轻量网关,统一做跨域头、请求方法限制和IP白名单。类似CTF里常见的nginx安全加固题,第一步是关闭服务器版本号、限制请求体大小、加访问控制。端口不要直接暴露,所有外部请求从统一入口转发到8000端口,既解决跨域,也能收敛访问面。

6. 安全加固与压测验证:让知识库长期省心跑

6.1 安全加固四件事:鉴权、白名单、入口与审计

离线部署不连公网,但内网不等于不设防。我习惯按四件事收口:第一,模型服务不裸奔,vLLM的OpenAI兼容接口再加上一层api-key校验,内部系统统一使用一个key,前端和知识库调用时都要带;第二,系统防火墙只放行业务网段,把8000端口范围限定在固定IP,宁可配置时麻烦,不给内网横向扩散留口子;第三,服务自身做限流和超时,vLLM用--max-num-seqs限制最大并发,入口层限制请求体大小和响应超时时间;第四,开审计日志,每轮问答把问题、检索片段、模型响应完整落盘,事后排查是模型瞎编还是知识库没覆盖,全靠这份日志。先能跑再补锁,四件事半小时内能全部收口。

6.2 用最简单的方式验证:并发、时延与上下文管理

压测结果看似玄学,但方法论不玄学。我先跑一个单并发冒烟脚本,确认接口稳定后再加并发:

import time import requests url = "http://127.0.0.1:8000/v1/chat/completions" payload = { "model": "deepseek-local", "messages": [{"role": "user", "content": "用一句话介绍内网AI知识库"}], "max_tokens": 128, "temperature": 0.1, } for i in range(10): t0 = time.time() resp = requests.post(url, json=payload, timeout=120) if resp.status_code == 200: data = resp.json() elapsed = round(time.time() - t0, 2) tokens = data.get("usage", {}).get("completion_tokens", 0) print(f"第{i+1}次 时延{elapsed}s 生成{tokens}tokens") else: print(f"第{i+1}次 失败,状态码{resp.status_code}")

这个脚本只看两点:错误率和基本时延。跑通后把requests换成异步客户端,5个并发持续打3分钟,重点观察是否出现超时和毛刺。出现毛刺时,先查知识库检索的慢查询,而不是急着加推理服务节点;很多所谓性能问题,其实是向量库索引没建好造成的。最后提醒一个藏得深的细节:多轮问答时,消息列表不控制长度,迟早会撞上max-model-len导致报错。新对话要承接上一个对话的上下文,不应该无限拼接历史消息,而是只保留最近3到5轮,把再前面的内容压缩成摘要。这个坑我掉进去过一次,从此多轮会话都先做消息裁剪。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询