拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Ubuntu 22.04 自建企业级知识库:Halogen + Milvus 处理 6.4 万切片实战

1. 项目缘起与整体架构思路1.1 为什么要在 Ubuntu 上自建知识库企业知识库这件事我前前后后折腾了快两年。最开始用 SaaS 方案数据放在别人服务器上老板不放心后来试过几个开源方案要么检索效果拉胯要么部署复杂度劝退。直到把 Halogen 跑在 Ubuntu 22.04 LTS 上6.4 万块文档切片真正跑通的那天我才觉得这套东西可以拿出来讲讲了。先说清楚这个项目是什么它是一套跑在 Ubuntu 服务器上的企业级知识库系统核心能力是把公司散落在各处的文档Word、PDF、Markdown、Confluence 导出件统一 ingest 进来做 embedding 向量化存进向量库然后通过语义检索把最相关的片段喂给大模型做问答。解决的核心问题是企业内部的非结构化知识找不到、搜不准、用不上。适合谁来参考如果你手上有几千到几十万份文档团队有基本的 Linux 运维能力想搞一套私有化、可控、检索质量过得去的知识库那这篇东西对你有用。纯小白也能看但至少要会敲apt install和改配置文件。1.2 整体架构拆解从文档到答案的完整链路整套系统我拆成四层每层职责清晰方便单独调优和排障层级职责我选的方案选型理由接入层文档采集与解析Halogen 自研 parserHalogen 对多格式支持好解析质量稳定向量层切片、embedding、存储BGE-M3 Milvus中文效果好Milvus 支持混合检索检索层语义召回 重排向量召回 BGE-reranker两段式检索精度提升明显应用层问答与 APIFastAPI LLM 接口轻量方便对接内部系统这个分层不是拍脑袋定的。最早我把解析和 embedding 揉在一起结果文档格式一多parser 报错直接把整个流水线搞挂。后来拆开之后解析失败只影响单个文档不会拖垮全局。解耦是知识库工程化的第一原则这话我踩过坑才真正理解。1.3 6.4 万块切片意味着什么6.4 万块切片按平均每块 500 字算大概是 3200 万字的原始文本。这个量级不算大但也不小——它刚好卡在一个尴尬的位置单机内存扛得住向量但暴力检索已经明显慢了用轻量方案精度不够用重型方案又有点杀鸡用牛刀。我实测下来6.4 万块切片用 Milvus 单机部署索引构建时间约 12 分钟检索 P99 延迟在 80ms 左右。这个数据后面会详细展开。关键是这个量级是很多中型企业的真实水位所以这套方案的可复现性很强。2. 环境准备与 Halogen 部署实操2.1 Ubuntu 22.04 LTS 基础环境配置系统我选的是 Ubuntu 22.04 LTS不是 24.04。原因很简单22.04 的生态兼容性经过两年验证Docker、CUDA、Python 3.10 这些依赖都稳。24.04 虽然新但有些库的版本冲突还没完全理顺生产环境没必要冒这个险。基础环境配置按这个顺序来# 1. 更新系统并安装基础工具 sudo apt update sudo apt upgrade -y sudo apt install -y build-essential git curl wget vim htop # 2. 安装 Python 3.1022.04 自带就是 3.10确认一下 python3 --version # 3. 安装 pip 和虚拟环境 sudo apt install -y python3-pip python3-venv # 4. 配置国内 pip 源速度提升明显 pip3 config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple这里有个坑要提醒不要用sudo pip install会把系统 Python 环境搞乱。我见过太多人因为这个问题重装系统。正确做法是每个项目建独立 venv。注意如果你在虚拟机里跑内存至少给 16GB磁盘至少 100GB。embedding 模型加载和向量索引构建都是吃内存的大户。2.2 Halogen 安装与依赖处理Halogen 的安装本身不复杂但依赖链有点长。我建议按官方文档走但有几个地方需要手动干预# 创建项目目录 mkdir -p /opt/knowledge-base cd /opt/knowledge-base # 创建虚拟环境 python3 -m venv venv source venv/bin/activate # 安装 Halogen假设通过 pip 或源码 pip install halogen-core # 如果源码安装 git clone https://github.com/xxx/halogen.git cd halogen pip install -e .安装过程中最容易出问题的是gcc 编译失败。Ubuntu 22.04 默认的 gcc 版本是 11有些 C 扩展需要更高版本或者特定头文件。解决办法sudo apt install -y gcc-12 g-12 sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-12 100 sudo update-alternatives --install /usr/bin/g g /usr/bin/g-12 100还有一个常见问题是Python 头文件缺失报错信息通常是Python.h: No such file or directory。装一下python3-dev就好sudo apt install -y python3.10-dev2.3 向量库选型为什么是 Milvus向量库这块我对比过几个主流方案方案优势劣势适用场景Milvus功能全支持混合检索部署重资源占用高中大型知识库Qdrant轻量Rust 性能好生态相对小中小型项目Chroma极简上手快生产级能力弱原型验证FAISS纯库性能极致无服务化需自己封装嵌入式场景6.4 万块切片这个量级Chroma 其实也能扛但它的持久化和并发能力让我不放心。FAISS 性能最好但要自己写服务层维护成本高。最后选 Milvus核心原因是它原生支持标量过滤 向量检索的混合查询这对企业知识库太重要了——你经常需要在某个部门范围内检索或者只搜某个时间段更新的文档。Milvus 用 Docker Compose 部署最省事# 下载 docker-compose 配置 wget https://github.com/milvus-io/milvus/releases/download/v2.3.0/milvus-standalone-docker-compose.yml -O docker-compose.yml # 启动 sudo docker compose up -d # 检查状态 sudo docker compose ps启动后默认端口是 19530。验证连接from pymilvus import connections connections.connect(hostlocalhost, port19530) print(Milvus connected)提示Milvus 默认会占用不少内存如果服务器内存紧张可以在docker-compose.yml里调低MINIO和ETCD的资源限制。3. 核心细节解析embedding 与检索调优3.1 embedding 模型选型与实测对比embedding 模型是知识库的命根子选错了后面怎么调都白搭。我实测了四个模型在同一批 500 条中文问答对上做召回率对比模型维度中文召回率10推理速度条/秒显存占用text-embedding-ada-002153682%API 依赖无BGE-large-zh-v1.5102488%452.1GBBGE-M3102491%382.8GBm3e-base76879%601.2GB最后选BGE-M3理由是它在中文语义匹配上确实强而且支持多语言和长文本最长 8192 token企业文档里经常有长段落这个特性很关键。速度慢一点可以接受知识库不是实时对话离线 ingest 慢几分钟无所谓。模型加载代码from sentence_transformers import SentenceTransformer model SentenceTransformer(BAAI/bge-m3) model.max_seq_length 1024 # 根据实际文档长度调整 def get_embedding(texts): return model.encode(texts, normalize_embeddingsTrue, batch_size32)normalize_embeddingsTrue这个参数别漏它把向量归一化到单位长度后续用内积计算相似度时等价于余弦相似度省一步计算。3.2 文档切片策略500 字不是随便定的切片大小直接影响检索质量。切太大一块里混了多个主题检索出来噪声多切太小语义不完整模型理解不了。我试过 256、512、1024 三种粒度最后定在500 字左右重叠 50 字。这个数字是这么算出来的BGE-M3 的最佳语义理解长度在 512 token 以内中文 1 token 约等于 1.5 字所以 500 字差不多是 330 token留了余量。重叠 50 字是为了防止关键信息刚好被切断。切片代码逻辑def split_text(text, chunk_size500, overlap50): chunks [] start 0 while start len(text): end start chunk_size chunk text[start:end] chunks.append(chunk) start end - overlap return chunks但纯按字数切有个问题会把一句话从中间劈开。更好的做法是按段落切段落超长再按句号切。我实际用的是递归切分先按\n\n切段落段落太长按。切句子句子还长才按字数硬切。3.3 向量检索的精度提升重排是关键光靠向量召回Top-10 里经常混进不相关的内容。加一层reranker之后精度提升非常明显。我用的是 BGE-reranker-largefrom FlagEmbedding import FlagReranker reranker FlagReranker(BAAI/bge-reranker-large, use_fp16True) def rerank(query, candidates, top_k5): pairs [[query, c] for c in candidates] scores reranker.compute_score(pairs) ranked sorted(zip(candidates, scores), keylambda x: x[1], reverseTrue) return [c for c, s in ranked[:top_k]]流程是向量召回 Top-50reranker 精排取 Top-5再喂给 LLM。这样做的代价是每次查询多 100-200ms但答案准确率的提升完全值回票价。我实测下来加了 reranker 之后人工评估的答案相关率从 71% 提到了 89%。注意reranker 模型也不小和 embedding 模型同时加载的话显存至少准备 6GB。如果显存不够可以用use_fp16True或者换 small 版本。4. 完整实操流程与关键环节实现4.1 文档 ingest 流水线搭建整个 ingest 流程我拆成五步采集、解析、切片、向量化、入库。每一步都有独立的日志和错误处理方便定位问题。import os import logging from pathlib import Path logging.basicConfig(levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s) class IngestPipeline: def __init__(self, milvus_client, embed_model): self.client milvus_client self.model embed_model self.stats {success: 0, failed: 0, skipped: 0} def run(self, doc_dir): files list(Path(doc_dir).rglob(*)) for f in files: if not f.is_file(): continue try: text self.parse(f) if not text or len(text) 50: self.stats[skipped] 1 continue chunks self.split(text) vectors self.model.encode(chunks, normalize_embeddingsTrue) self.insert(chunks, vectors, str(f)) self.stats[success] 1 logging.info(fProcessed: {f}, chunks: {len(chunks)}) except Exception as e: self.stats[failed] 1 logging.error(fFailed: {f}, error: {e}) return self.stats这个结构的好处是单个文档失败不影响整体。我跑 6.4 万块切片的时候有 200 多个文件解析失败主要是扫描版 PDF 和加密文档但流水线没停最后统一处理失败列表就行。4.2 Milvus 集合设计与索引参数Milvus 的集合设计要考虑字段和索引。我的 schema 是这样的from pymilvus import CollectionSchema, FieldSchema, DataType fields [ FieldSchema(nameid, dtypeDataType.INT64, is_primaryTrue, auto_idTrue), FieldSchema(namedoc_id, dtypeDataType.VARCHAR, max_length128), FieldSchema(namechunk_text, dtypeDataType.VARCHAR, max_length2000), FieldSchema(nameembedding, dtypeDataType.FLOAT_VECTOR, dim1024), FieldSchema(namesource, dtypeDataType.VARCHAR, max_length512), FieldSchema(namedept, dtypeDataType.VARCHAR, max_length64), FieldSchema(nameupdate_time, dtypeDataType.INT64), ] schema CollectionSchema(fields, descriptionenterprise knowledge base)索引参数是调优的重点index_params { metric_type: IP, # 内积配合归一化向量等价余弦 index_type: IVF_FLAT, params: {nlist: 1024} }nlist的选择有个经验公式nlist ≈ 4 * sqrt(N)N 是向量总数。6.4 万块的话sqrt(64000) ≈ 2534 倍就是 1012取整 1024。这个参数影响索引构建时间和检索精度的平衡nlist 越大精度越高但构建越慢。检索时还有个nprobe参数控制搜索多少个聚类search_params {metric_type: IP, params: {nprobe: 16}}nprobe一般取nlist的 1%-10%。我实测 16 是个不错的平衡点再往上精度提升不明显延迟却线性增长。4.3 检索接口与 LLM 对接检索接口用 FastAPI 封装核心逻辑是召回 重排 组装 promptfrom fastapi import FastAPI from pydantic import BaseModel app FastAPI() class Query(BaseModel): question: str top_k: int 5 dept_filter: str None app.post(/search) def search(q: Query): # 1. 向量化 query q_vec model.encode([q.question], normalize_embeddingsTrue)[0] # 2. 构建过滤表达式 expr fdept {q.dept_filter} if q.dept_filter else None # 3. 向量召回 Top-50 results collection.search( data[q_vec], anns_fieldembedding, paramsearch_params, limit50, exprexpr, output_fields[chunk_text, source] ) # 4. 重排取 Top-K candidates [r.entity.get(chunk_text) for r in results[0]] top_chunks rerank(q.question, candidates, top_kq.top_k) # 5. 组装上下文 context \n\n.join(top_chunks) return {context: context, sources: [r.entity.get(source) for r in results[0][:q.top_k]]}这个接口返回的是检索到的上下文不直接调 LLM。这样做的好处是检索和生成解耦你可以对接任何 LLM也可以先看检索质量再决定要不要生成。4.4 性能实测数据与资源占用跑完 6.4 万块切片后我记录了一组实测数据指标数值说明索引构建时间12 分 30 秒IVF_FLAT, nlist1024单次检索延迟 P5035msnprobe16单次检索延迟 P9982ms含网络往返重排延迟120msTop-50 精排端到端问答延迟1.8s含 LLM 生成内存占用8.2GBMilvus 模型显存占用5.6GBBGE-M3 reranker这个性能对于企业内部使用完全够用。如果并发上来了Milvus 可以水平扩展加节点就行。5. 常见问题与排查技巧实录5.1 部署阶段高频问题速查问题现象根本原因解决方法gcc 编译失败gcc 版本过低装 gcc-12 并切换Python.h 找不到缺 python3-devapt install python3.10-devMilvus 启动后连不上端口未映射或防火墙检查 19530 端口和 ufw 规则模型加载 OOM显存不足用 fp16 或换 small 模型pip 安装超时源太慢换清华源或阿里源Docker 权限拒绝用户不在 docker 组usermod -aG docker $USER5.2 检索质量差的排查思路检索质量差是最常见也最头疼的问题。我的排查顺序是第一步看切片质量。把检索出来的 chunk 打印出来如果发现 chunk 本身语义不完整或者混了多个主题那就是切片策略的问题。调整 chunk_size 和 overlap 重新 ingest。第二步看 embedding 是否正常。用几个已知相似的句子测一下余弦相似度正常应该在 0.7 以上。如果相似句子得分很低可能是模型加载有问题或者 normalize 没开。第三步看召回数量。如果 Top-50 里都没有正确答案说明向量召回阶段就漏了。这时候要么换更强的 embedding 模型要么增大召回数量要么检查是不是过滤条件把正确结果排除了。第四步看重排效果。如果召回里有正确答案但重排后掉了说明 reranker 和你的场景不匹配可以试试换模型或者调整重排的 top_k。实操心得我建议每次调整参数后用同一批测试问题跑一遍记录召回率和准确率。没有量化指标调优就是瞎猜。5.3 几个我踩过的坑坑一中文输入法导致命令行乱码。在 Ubuntu 上装搜狗输入法之后终端里偶尔会输入乱码字符导致命令执行失败。解决办法是终端里尽量用英文输入或者装 fcitx5 配拼音。坑二环境变量配置错误导致模型找不到。我把模型路径写在.bashrc里但用 systemd 启动服务时不加载.bashrc结果服务启动就报模型找不到。正确做法是把环境变量写在 systemd 的 service 文件里或者用.env文件显式加载。坑三Milvus 数据持久化没配好重启后数据丢了。默认的 docker-compose 配置里 volume 映射要检查清楚确保volumes目录挂载到宿主机。我第一次跑的时候没注意重启容器后 6 万块向量全没了重新 ingest 花了半小时。坑四批量 ingest 时内存暴涨。一次性把所有文档加载进内存再处理6.4 万块直接吃满 32GB。后来改成流式处理每批 100 个文档内存稳定在 4GB 左右。5.4 增量更新与版本管理知识库不是一次性的文档会更新。我的做法是给每个文档算一个内容 hashingest 前先查 hash 是否已存在。如果存在就跳过如果 hash 变了就删掉旧向量重新插入。import hashlib def get_doc_hash(file_path): with open(file_path, rb) as f: return hashlib.md5(f.read()).hexdigest() def upsert_document(doc_id, chunks, vectors): # 先删旧的 collection.delete(fdoc_id {doc_id}) # 再插新的 collection.insert([...])这样每次增量更新只需要处理变化的文档全量 6.4 万块重新跑一遍要 12 分钟增量更新通常几十秒就搞定。6. 后续扩展方向与个人体会这套系统跑稳之后我陆续加了几个扩展。一个是多路召回除了向量检索还加了 BM25 关键词检索两路结果合并去重再重排对专有名词和代码片段的召回提升明显。另一个是查询改写用户的问题往往口语化先用 LLM 改写成更适合检索的形式召回率能再提 5-8 个百分点。还有一个方向是权限隔离。企业知识库不同部门的数据要隔离我在 schema 里加了dept字段检索时通过expr过滤。但要注意Milvus 的标量过滤是在向量检索之后做的如果某个部门的数据很少可能会被其他部门的数据挤掉。解决办法是给每个部门建独立的 partition检索时指定 partition。我个人在实际操作中的体会是知识库这件事工程细节比算法选型更重要。embedding 模型换来换去效果差异可能就几个百分点但切片策略、过滤条件、重排逻辑这些工程细节处理不好直接让系统不可用。我见过太多团队花大力气调模型结果败在文档解析和切片上。最后分享一个小技巧ingest 的时候把原始文档的元数据标题、作者、更新时间、部门一起存进去检索结果里带上这些信息用户看到答案来源会更信任。这个改动很小但用户体验提升很大。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门