前阵子我接到一个挺实际的需求:公司内部的一堆制度文档、项目总结和操作手册散落在共享盘里,大家想做一个能"问人话"的检索入口,而不是继续在几十个目录里翻文件。一开始的方案是接在线大模型的服务做 RAG,结果数据安全负责人一口否决:核心资料不允许离开内网。于是我把方案改成了纯本地链路——用 vLLM 部署 DeepSeek 开源模型,配合 Milvus 构建本地向量库,文档切片后向量化入库,检索时先语义召回再让模型生成答案。从搭环境到跑通完整问答,加起来大概一周,现在这套东西已经稳定跑在我们自己的服务器上。如果你也打算做私有化知识库、给内部系统加语义检索,或者单纯想研究 vLLM、DeepSeek、Milvus 这套组合怎么落地,这篇文章应该能帮你把大部分坑提前趟平。我会按"为什么这么选型"、"环境与部署"、"入库与检索"、"调优与排错"四个方向来讲,代码全部可复现。
1. 为什么是"本地向量库":需求边界与组件分工
1.1 本地化解决的三个现实问题
做本地向量库最直接的理由不是"技术更炫",而是"数据不能出去"。企业内部的知识库往往包含未公开的项目细节、财务制度、人事规范,接第三方 API 意味着把内容交给别人保管,这在大多数公司都过不了安全评审。数据留在内网之后,权限、审计、备份都是自己说了算,这是一个硬约束,不是偏好问题。
第二个是可用性。内网环境不一定有稳定的出网带宽,在线接口的延迟和限流也会影响体验。本地部署的 vLLM 服务跑在同一台服务器或同一个机房,调用开销是毫秒级的,不依赖外网状态。
第三个是成本模型。在线大模型按 token 计费,知识库一旦被高频查询,单月账单很容易失控;本地部署是一次性硬件投入加电费,长期跑下来反而可预测。对于数据量中等、查询频率稳定的内部场景,这笔账算得过来。
1.2 vLLM、DeepSeek、Milvus 各干哪一摊活
这套链路的角色划分非常清晰:
- vLLM负责把模型"服务化"。它把模型权重加载进显存,对外提供 OpenAI 兼容的 HTTP 接口,自带高吞吐推理和显存管理,不用自己写推理代码。
- DeepSeek是模型本身。在这个项目里它同时承担两个任务:一是把文本变成向量(向量化),二是根据检索结果生成回答。开源权重直接下载到本地,不依赖任何在线服务。
- Milvus负责向量数据的存储和检索。它是真正的数据库,有集合、字段、索引、标量过滤的概念,能支撑后续按部门、按时间过滤查询,而不是一个简单的向量数组。
整个流程可以这样理解:文档先被切成小片段,每段文本交给 vLLM 里的 DeepSeek 模型转成向量,向量连同原文和来源信息一起写进 Milvus;查询的时候,用户问题被同一个模型转成向量,Milvus 在索引里快速找出最相似的几个片段,最后把片段拼进 prompt,交给生成模型产出回答。中间没有一个环节需要把数据送出内网。
1.3 候选方案对比:为什么最后留下 Milvus
选型时我也对比过几个主流方案,整理成表格就一目了然了:
| 方案 | 优势 | 短板 | 适合场景 |
|---|---|---|---|
| Elasticsearch | 生态成熟,全文检索强 | 向量能力是后加的,调优复杂,资源占用大 | 以关键词检索为主的老系统 |
| Faiss | 轻量、算法全、速度快 | 只是计算库,没有持久化、过滤、权限、API | 离线实验、单机小批量 |
| Chroma | 上手快,几行代码就能跑 | 数据量一上去性能就掉,不适合多并发 | 原型验证、千条级数据 |
| Milvus | 完整的向量数据库,索引类型多、过滤能力强 | 要单独部署,多一个运维组件 | 面向生产的本地向量库 |
选 Milvus 还有一个原因:它是"数据库"而不是"索引库"。企业内部的知识库一定会遇到按文档类型、所属部门过滤的场景,也会遇到数据增删改的问题,这些能力 Faiss 里都得自己重新造轮子,而 Milvus 原生就支持。对于"先跑通再扩到全公司"的路径来说,一步到位用 Milvus 反而省事。
2. 先把地基打好:硬件估算、软件安装与模型权重
2.1 先算显存账:你的卡能跑多大的模型
这一步是后面所有环节的前提。模型权重在 FP16 精度下大约等于参数量的两倍字节数:1.5B 参数的模型约 3GB,7B 约 14GB,32B 约 64GB。但显存不光是装权重,KV Cache 和计算过程中的激活值同样要吃显存,所以实际需求要预留 20% 到 30% 的余量。
| 模型规模 | 权重大小(FP16) | 建议显存 | 推荐用途 |
|---|---|---|---|
| DeepSeek-R1-Distill-Qwen-1.5B | 约 3GB | 8GB 可跑 | 向量化、低资源设备 |
| DeepSeek-R1-Distill-Qwen-7B | 约 14GB | 24GB | 生成、日常内部问答 |
| DeepSeek-R1-Distill-Qwen-32B | 约 64GB | 80GB 或双卡 | 复杂推理、高质量生成 |
我自己的环境是一张 24GB 显存的显卡,最终采用了"7B 做生成 + 1.5B 做向量化"的组合,两个模型加起来大约 17GB 权重,给 KV Cache 留了充足空间。磁盘方面也不能忽略,一百万条 1536 维向量的裸数据就要 6GB 左右,加上索引和 Milvus 自身的开销,建议预留至少 100GB 磁盘。
2.2 vLLM 安装:版本、Python 环境和第一个探针
vLLM 对 Python 版本有要求,我建议直接用 conda 建一个干净环境,避免和系统 Python 打架:
conda create -n kb python=3.11 -y conda activate kb pip install vllm装完之后先别急着拉模型,跑两条命令确认环境正常:
python -c "import torch; print(torch.__version__, torch.cuda.is_available())" nvidia-smitorch.cuda.is_available()必须是 True。如果显示 False,基本可以断定是 CUDA 版本和 PyTorch 不匹配,重装对应版本的 PyTorch 再回来。这里有个经验:vLLM 迭代很快,不同版本的参数名可能微调,遇到陌生参数别硬猜,直接vllm serve --help看看当前版本的说明。
2.3 Milvus 单机部署:一条命令和两个检查
Milvus 的部署方式很多,单机场景用官方脚本最省心。它会自动拉起 etcd、MinIO 和 standalone 三个容器,把依赖关系处理好:
curl -sfL https://raw.githubusercontent.com/milvus-io/milvus/v2.4.0/scripts/standalone_embed.sh -o standalone_embed.sh bash standalone_embed.sh等十几秒后检查两个地方,一个是 Milvus 健康状态,一个是 Python 客户端能不能连上:
curl -s http://localhost:19530/health pip install pymilvus openai python -c "from pymilvus import MilvusClient; MilvusClient(uri='http://localhost:19530'); print('ok')"健康检查返回{"status":"OK"}、Python 客户端不报错,就说明数据库已经就绪。
提示:standalone_embed.sh 启动的数据默认落在 Docker volume 里。容器重建之前一定要确认 volume 还在,否则向量库说没就没。我从一开始就把数据目录映射到宿主机独立目录,后面会专门讲持久化的坑。
2.4 DeepSeek 权重落地:两条下载路径和 config.json 检查
DeepSeek 的开源权重在 HuggingFace 和 ModelScope 上都有分发。如果下载速度不理想,可以换 ModelScope 的仓库:
# HuggingFace 方式 huggingface-cli download deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B --local-dir /data/models/deepseek-r1-1.5b # ModelScope 方式 modelscope download --model deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B --local_dir /data/models/deepseek-r1-1.5b权重下载完,记得打开模型目录里的config.json看一眼两个字段:hidden_size和max_position_embeddings。前者决定向量维度,后者决定模型能处理的最大序列长度。DeepSeek-R1-Distill-Qwen-1.5B 的 hidden_size 是 1536,这个数字后面要原封不动写进 Milvus 的字段定义里,写错就插不进去数据。
3. 用 vLLM 把 DeepSeek 拉起来:启动参数背后的权衡
3.1 生成和向量化各占一个端口
vLLM 一个进程只能绑定一种任务类型:generate模式对外提供/v1/chat/completions,embedding模式对外提供/v1/embeddings。所以我把两个实例分开跑,互不干扰:
# 生成实例:端口 8000 vllm serve /data/models/deepseek-r1-7b \ --host 0.0.0.0 \ --port 8000 \ --task generate \ --gpu-memory-utilization 0.6 \ --max-model-len 8192 \ --served-model-name deepseek-chat# 向量实例:端口 8001 vllm serve /data/models/deepseek-r1-1.5b \ --host 0.0.0.0 \ --port 8001 \ --task embedding \ --gpu-memory-utilization 0.3 \ --max-model-len 4096 \ --served-model-name deepseek-embed几个参数说清楚:
--task指定模型用途,generate 和 embedding 不能写混,否则接口直接 404。--served-model-name是对外暴露的名字,可以随便起。它只影响 API 调用时传的 model 字段,不影响模型权重本身。--gpu-memory-utilization写成小数,表示该实例最多能吃掉多少比例的显存。两张卡共 24GB 时,0.6 + 0.3 合适,剩下 0.1 留给 CUDA context 和系统开销。千万不要两个实例都写 0.9,第二次启动直接 OOM。--max-model-len不是越大越好。它决定 KV Cache 的预留规模,模型加载时 vLLM 会按这个长度估算显存,设得过大模型都加载不出来。1.5B 向量实例用 4096 足够,因为切片本身不会太长。
3.2 让生成模型"兼职"向量化:补一个池化配置
这里是我踩过的第一个硬坑。DeepSeek 的官方权重是为生成任务训练的,模型配置里没有池化层,直接带--task embedding启动会报"模型不支持嵌入任务"之类的错误。vLLM 要求嵌入模型在 config.json 里显式声明pooling_config,解决办法是在模型目录的 config.json 顶层补一个配置块:
{ "pooling_config": { "pool_type": "last", "normalize": true } }pool_type: last表示取最后一个 token 的隐藏状态作为整段文本的向量,normalize: true表示对向量做归一化,这样后面用 COSINE 相似度时打分才准确。补完配置后重新启动向量实例,确认/v1/models里能看到这个模型,就算成功。
提示:如果你不想动模型文件,或者试了几天还启动不起来,直接换一个原生支持嵌入的模型(比如 BGE-M3 系列)是最省事的。代码只需要改模型名和维度,其他逻辑完全不变。拿 DeepSeek 硬凑 embedding 是为了少维护一个模型,如果折腾成本超过收益,果断换。
3.3 接口自检三步:models、chat、embeddings
服务起来之后,别急着写业务代码,先用 curl 把三个接口都验一遍:
# 1. 看看模型加载好没有 curl -s http://localhost:8000/v1/models | head # 2. 生成接口 curl -s http://localhost:8000/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{"model": "deepseek-chat", "messages": [{"role": "user", "content": "你好"}], "max_tokens": 64}' # 3. 向量接口 curl -s http://localhost:8001/v1/embeddings \ -H "Content-Type: application/json" \ -d '{"model": "deepseek-embed", "input": "你好"}'向量接口返回的embedding数组长度必须是 1536,和前面 config.json 里的 hidden_size 对应。有两个时间点很容易误判:模型刚启动时 curl 返回 503 是正常的,vLLM 要先把权重全部加载进显存,这个过程可能要好几分钟,用/v1/models轮询,看到served-model-name才说明就绪;另一个是端口和任务类型对应关系,生成实例没有/v1/embeddings,向量实例也没有/v1/chat/completions,这两个实例不能混着用。
4. 让 Milvus 把数据接住:Schema、切片和索引的真实选择
4.1 先设计 Schema:维度写错满盘皆输
Milvus 是强 Schema 的数据库,建集合之前必须把字段定义清楚。我这个项目用到四个字段:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | INT64 | 主键,自己管理自增 |
| content | VARCHAR(4096) | 切片原文 |
| source | VARCHAR(256) | 来源文件路径或章节 |
| embedding | FLOAT_VECTOR | 向量数据,维度 1536 |
建集合的代码用 pymilvus 的 MilvusClient 写起来很直观:
from pymilvus import MilvusClient, DataType mc = MilvusClient(uri="http://localhost:19530") # 重新建库,避免多次运行脚本时 schema 冲突 mc.drop_collection("doc_kb") schema = mc.create_schema(auto_id=False) schema.add_field("id", DataType.INT64, is_primary=True) schema.add_field("content", DataType.VARCHAR, max_length=4096) schema.add_field("source", DataType.VARCHAR, max_length=256) schema.add_field("embedding", DataType.FLOAT_VECTOR, dim=1536) index_params = mc.prepare_index_params() index_params.add_index( field_name="embedding", index_type="HNSW", metric_type="COSINE", params={"M": 16, "efConstruction": 200}, ) mc.create_collection("doc_kb", schema=schema, index_params=index_params)dim=1536就是前面反复强调的维度,必须和向量模型输出严格一致。我一开始用 BGE-M3 测过一版,后来切回 DeepSeek 时忘了改维度,insert 阶段报错才发现。这个错误信息很直白,但定位起来要绕一圈,建议把维度写进一个常量,换模型时全局搜索就行。
4.2 切片方式直接影响召回上限
向量库的检索质量,一半取决于切片。切片太长,一句话的关键信息会被淹没在无关内容里;切片太短,语义又不够完整。我的做法是先按 Markdown 的段落切,段落超长再按字符数补一刀,同时保留重叠区:
import re def chunk_markdown(text, max_chars=800, overlap=100): blocks = [] for para in re.split(r"\n\s*\n", text): if len(para) <= max_chars: blocks.append(para) else: start = 0 while start < len(para): blocks.append(para[start:start + max_chars]) start += max_chars - overlap return blocks800 字、重叠 100 字是中文文档一个比较稳的起步配置。重叠的意义在于:如果关键信息恰好落在切片边界上,两个切片都能覆盖到它,检索时不容易漏。说实话,切片属于"看起来简单、实际要反复试"的参数,我建议先用默认值跑通链路,再针对你自己的语料风格去调。
4.3 批量向量化写入:限流与进度打印
入库脚本的逻辑不复杂:读文件、切片、调向量接口、写入 Milvus。但有两个细节值得注意:一是向量化请求要控制并发,vLLM 默认支持一定并发,但你要是一口气丢几百个请求进去,某些版本会开始排队甚至报错;二是写入 Milvus 时用批量 insert,减少网络往返次数。我的完整脚本结构如下:
import glob from openai import OpenAI from pymilvus import MilvusClient EMBED_BASE = "http://localhost:8001/v1" EMBED_MODEL = "deepseek-embed" client = OpenAI(api_key="EMPTY", base_url=EMBED_BASE) mc = MilvusClient(uri="http://localhost:19530") rows = [] idx = 0 for path in glob.glob("corpus/**/*.md", recursive=True): with open(path, encoding="utf-8") as f: text = f.read() for chunk in chunk_markdown(text): vec = client.embeddings.create( model=EMBED_MODEL, input=chunk ).data[0].embedding rows.append({"id": idx, "content": chunk, "source": path, "embedding": vec}) idx += 1 # 每 50 条批量写一次,方便观察进度 if len(rows) % 50 == 0: mc.insert("doc_kb", rows) rows = [] if rows: mc.insert("doc_kb", rows) print("total:", mc.query("doc_kb", output_fields=["count(*)"]))OpenAI 客户端的api_key随便填一个非空字符串就行,vLLM 不校验它。并发方面我实测 4 到 8 个并发请求是安全的,具体数值取决于你的显卡和处理能力,稳妥起见可以先串行跑一遍,确认吞吐量后再说。
4.4 索引选型:HNSW 为什么是中等规模的首选
Milvus 支持很多索引类型,选型逻辑其实不复杂。数据量决定索引类型,查询规模决定索引参数:
| 索引类型 | 内存占用 | 检索精度 | 构建速度 | 建议场景 |
|---|---|---|---|---|
| FLAT | 高 | 100% | 不需要构建 | 一万条以下暴力搜索 |
| IVF_FLAT | 中 | 中 | 快 | 百万级,对查询速度要求不高 |
| HNSW | 高 | 高 | 慢一点 | 十万到千万级,在线低延迟查询 |
| DISKANN | 低 | 中 | 慢 | 亿级,数据放磁盘 |
HNSW 是目前中等规模向量库的主力选择,它的思路是构建一张多层图,检索时从顶层快速向下逼近目标节点。M=16表示每个节点最多连 16 个邻居,图越密召回越高,内存也越大;efConstruction=200是构图时的搜索宽度,会影响建索引的耗时和索引质量。这两个参数对十万到百万条数据来说是很常见的配置,不需要一上来就用极端值。
5. 检索链路与"搜得准"的调优方法
5.1 一次完整的检索:向量化、召回、生成
链路跑通的标志是能完成一次完整的"提问到回答"。查询阶段和入库阶段对称:先把问题向量化,再用同样的向量去 Milvus 里搜索,最后把命中的片段交给生成模型:
from openai import OpenAI query = "发票报销需要走什么流程" # 1. 问题向量化(走向量实例) qvec = client.embeddings.create( model=EMBED_MODEL, input=query ).data[0].embedding # 2. Milvus 召回 top5 res = mc.search( collection_name="doc_kb", data=[qvec], limit=5, output_fields=["content", "source"], search_params={"metric_type": "COSINE", "params": {"ef": 64}}, ) # 3. 组装上下文,交给生成实例 gen = OpenAI(api_key="EMPTY", base_url="http://localhost:8000/v1") context = "\n".join( f"[{item['entity']['source']}]\n{item['entity']['content']}" for item in res[0] ) resp = gen.chat.completions.create( model="deepseek-chat", messages=[ {"role": "system", "content": "你是内部知识库助手,只能依据给定资料回答,资料不足时直接说明不知道。"}, {"role": "user", "content": f"资料:\n{context}\n\n问题:{query}"}, ], max_tokens=512, ) print(resp.choices[0].message.content)这个 prompt 里的 system 消息值得多说一句:明确告诉模型"只能依据资料回答、资料不足就直说",能显著减少幻觉。RAG 最常见的翻车现场就是模型在资料没有答案时自己编一个,这句话能把这个风险压下去大半。
5.2 相似度算法与打分阈值
Milvus 支持的相似度度量主要有三种:内积(IP)、余弦(COSINE)、欧氏距离(L2)。文本语义检索场景推荐 COSINE,它衡量的是方向而不是长度,对文本向量的量级差异不敏感。前提是向量做过归一化,恰好我前面在 pooling_config 里开了normalize: true,两者是配套的。
检索结果的distance字段,对 COSINE 来说越大表示越相似,理论范围是 -1 到 1。我跑了一段时间的经验区间是:0.75 以上基本可靠,0.6 到 0.75 需要人工看一眼,0.6 以下大概率是误召回。但注意这是经验值,不同模型的分值分布差异很大,建议你入库后抽几条查询自己观察一遍再定阈值。
5.3 用 ef 参数换速度与召回:一张实测表
HNSW 检索时最关键的是ef参数,它表示搜索时遍历的候选范围。ef 越大召回越准,延迟也越高。在我 60 万条、1536 维向量、单张 24G 显卡的压力测试下,大致是这样的关系:
| ef 值 | 单条查询延迟 | 召回相对水平 | 适用场景 |
|---|---|---|---|
| 16 | 约 10ms | 85% | 快速预览,检索频率极高 |
| 64 | 约 35ms | 95% | 日常内部问答,默认推荐 |
| 256 | 约 120ms | 99%以上 | 离线评测、高精度场景 |
调优路径我建议是先固定 ef=128,确认召回质量满足要求,再把 ef 往下调,找到准确率不掉、延迟明显下降的拐点。不要一上来追求 99% 的召回,企业内部问答对两三个百分点的差距没那么敏感,但延迟体感差异很大。
5.4 让检索更准:标量过滤、混合检索与重排
当知识库数据量大起来之后,单靠向量相似度不够,还要引入三个进阶手段。
第一个是标量过滤。Milvus 允许在 search 时加 filter 条件,比如只查某个部门的文档,或者排除某些文件类型:
res = mc.search( collection_name="doc_kb", data=[qvec], limit=5, filter='source like "%finance%"', search_params={"metric_type": "COSINE", "params": {"ef": 64}}, )这样能避免不同部门的资料互相污染答案,在内部知识库场景几乎是刚需。
第二个是混合检索。向量检索擅长语义相似,但遇到"用户问法跟文档用词完全不一样"时依然可能漏。Milvus 支持稀疏向量,把传统的 BM25 关键词能力也引进来,用SPARSE_INVERTED_INDEX建索引,查询时做稠密和稀疏向量的加权融合,能同时照顾语义和关键词两个维度。这个属于进阶玩法,第一版先不做。
第三个是重排。如果 top5 里混入了不相关的片段,可以让生成模型对候选片段做一个二次判定:把 top10 的内容和问题一起发给 DeepSeek,让它挑出最相关的两三个片段。效果不错但成本高,适合对回答质量要求特别严的场景。我自己平时先用打分过滤,命中率低到不能忍才上重排。
6. 踩坑实录:五类问题从现象到根因的完整排查
6.1 curl 一直 503:是没加载完,不是服务挂了
问题现象:vLLM 启动命令执行后,curl 接口一直返回 503 Service Temporarily Unavailable,等了半分钟还是不行,感觉像服务坏了。排查链路是这样的:先看 vLLM 进程是不是活着,再用/v1/models轮询,同时观察nvidia-smi里的显存占用。vLLM 加载权重、做模型 profiling 需要几分钟,这段时间内接口就是 503。如果显存占用在持续上升,说明还在加载,继续等;如果显存已经占了 95% 以上但日志反复报错退出,那就是真正的 OOM,需要调小gpu-memory-utilization或者max-model-len。
6.2 404:task 不对,接口不存在
很多人(包括我)第一次都会犯这个错:对着 8000 端口的生成实例发/v1/embeddings,或者对着 8001 端口的向量实例发/v1/chat/completions,结果全是 404。vLLM 的 HTTP 路由是按任务类型注册的,generate 实例压根没有 embeddings 路由,embedding 实例也没有 chat 路由。解决办法不是找什么"兼容模式",而是确认你调用的端口对应的是哪个 task,换到正确的端口就行。
6.3 建集合报错:维度不一致和重复创建
入库脚本跑第二次的时候,大概率在create_collection阶段报两个错。一个是 "collection already exists",因为我脚本开头有drop_collection,第一次把集合删掉又重建就好了;另一个是维度不一致,报错类似 "dimension is not aligned with field schema",这时去看 config.json 的 hidden_size 和代码里的 dim 是不是同一个数字。换模型是最容易触发这个问题的操作,建议把维度常量放在脚本最顶部,并在日志里打印出来。
还有一个隐蔽问题:数据入库后第一次 search,报 "collection not loaded"。Milvus 的集合默认需要加载到内存才能检索,MilvusClient 部分操作会自动加载,但某些版本不会。遇到就手动执行:
mc.load_collection("doc_kb")这个坑的特点是"数据明明在,查不到",极具迷惑性。
6.4 检索结果"看着像又不像":切片、阈值和过滤的联合调试
更头疼的是"没报错但结果不对":distance 0.83 很高,但返回的文本就是没回答到点子上。我遇到过三次这种情况,根因各不相同。第一次是切片把关键信息切断了,前半段是背景,后半段才是结论,问题命中在结论上但上下文不完整;第二次是 query 写得太口语化,文档里用词很正式,语义检索没能匹配上;第三次是没有加部门过滤,另外一个团队的同名制度文档干扰了排序。调试方法很朴素:把 top10 的 distance 和内容摘要全部打印出来,一行一行肉眼比对,然后只改一个变量重新查。千万不要同时改切片、阈值和过滤条件,那样永远定位不了问题。
6.5 显存不够与数据持久化:上生产前最后处理
如果你的显卡只有 16G,不要试图硬跑 7B 生成加 1.5B 向量两个实例,权重都装不下。这时候要么只跑 1.5B 生成实例,向量模型换成更小的专用嵌入模型;要么把 7B 换成 4 位量化版本,但量化会损失生成质量,内部问答能接受才用。
数据持久化这块,我吃过一次亏。当时以为数据进了 Milvus 就万事大吉,结果容器重建后集合还在、数据全空。原因是 Milvus 的数据实际写在 etcd 和 MinIO 里,而这两个组件的数据落在 Docker volume,容器删除时 volume 如果没保留,数据就没了。建议启动 Milvus 前先把 compose 文件里的 volume 显式映射到宿主机目录,并定期执行 compact 清理删除标记:
mc.compact("doc_kb")安全方面也提一句:vLLM 的两个实例如果绑定了0.0.0.0,请确保它们只暴露在内网,别把端口映射到公网。Milvus 默认没有鉴权,多人内网使用时最好设置 root 密码或者用独立网段隔离。
最后说一点我自己的体会。整套链路里真正决定效果上限的,不是 vLLM 也不是 Milvus,而是你喂给它的语料和切片质量——数据脏、切片碎,再好的索引也救不回来。所以如果你时间有限,先把精力放在清理语料和设计切片规则上,把 demo 跑通后再逐步加 filter、混合检索和重排。我自己是从"1.5B 向量 + 7B 生成"这套组合上车的,等数据增长到百万级之后,再考虑把生成实例换成本地集群并引入更完整的权限体系。希望这篇总结能让你少走一点弯路,也欢迎你在评论区分享自己的踩坑经历。