☰
Ubuntu 自建企业知识库:Halogen 混合检索与向量化实战
2026/10/3 11:32:32 网站建设 项目流程

1. 项目缘起与整体架构思路

1.1 为什么要在 Ubuntu 上自建企业知识库

先说背景。我所在团队维护着一个大约 6.4 万份文档规模的企业知识库,涵盖产品手册、售后工单、内部规范、技术方案、合同模板等。之前一直用某 SaaS 知识库,按席位收费,一年下来成本不低,而且数据不在自己手里,检索效果也没法深度调优。后来决定迁到自建方案,核心诉求就三条:数据不出内网、检索质量可控、成本能压到一台机器扛住。

选 Ubuntu 作为底座,理由很直接。一是生态成熟,Docker、Python、CUDA 这些依赖装起来最省心;二是长期支持版本稳定,22.04 LTS 和 24.04 LTS 都能拿到五年安全更新;三是团队里运维同学对它的熟悉度最高,出问题排查快。至于 Halogen,它是我在对比了几套开源知识库方案后选定的检索层框架,核心能力是把 embedding 向量检索和关键词检索做混合召回,再叠一层重排,对中文长文档的匹配度提升很明显。

这套组合最终跑下来的效果:6.4 万份文档全量入库,单次查询 P95 延迟控制在 800ms 以内,召回准确率相比原来的纯关键词方案提升了大约 40%。硬件就是一台 32 核 128G 内存、带一张 24G 显存显卡的机器,没有上集群。

1.2 整体架构分层拆解

整个系统我分成四层来理解,这样排查问题时能快速定位是哪一层出的毛病。

数据接入层负责把各种格式的原始文档解析成纯文本。PDF、Word、Excel、PPT、Markdown、HTML 都要处理,还要做清洗,去掉页眉页脚、乱码、重复段落。这一层我用的是 Python 脚本加一批解析库,跑成批处理任务。

向量化层是核心。文档切块之后,每一块都要过 embedding 模型转成向量。这里模型选型直接决定检索质量,我试过好几个,后面会详细说。向量维度、切块大小、重叠长度这些参数都需要反复调。

存储与检索层就是 Halogen 发挥作用的地方。它同时维护一个向量索引和一个倒排索引,查询时两路召回再融合。向量索引我用的 HNSW,倒排索引基于 Lucene 那套思路。

应用层是对外的 API 和前端。内部系统通过 REST 接口调用,返回带原文片段和来源链接的结果。

提示:分层不是为了好看,是为了出问题时能快速二分定位。比如检索结果差,先看是向量化层切块不合理,还是检索层融合权重没调好,别一上来就怀疑模型。

1.3 方案选型背后的取舍逻辑

为什么不用现成的 SaaS?前面说了,成本和数据主权。为什么不用更重的方案比如带完整 RAG 流水线的平台?因为我们的场景相对单纯,就是检索问答,不需要复杂的 agent 编排,引入太重的东西反而增加维护负担。

Halogen 相比其他开源检索框架的优势在于它对混合检索的支持是原生的,不用自己拼两套系统。而且它的索引构建支持增量更新,6.4 万份文档不需要每次全量重建,新增文档几分钟就能进索引。这一点在文档持续更新的企业场景里非常关键。

embedding 模型这块我踩过坑。一开始图省事用了某个通用小模型,结果中文语义匹配一塌糊涂,问“怎么退货”匹配不到“退款流程”。后来换成中文优化过的大模型,效果立竿见影,但显存占用和推理速度要重新平衡。这个取舍过程后面细讲。

2. 环境准备与依赖安装的实操细节

2.1 Ubuntu 系统层面的准备工作

我用的镜像是 Ubuntu 22.04.4 LTS 桌面版,其实服务器版更合适,但当时手头只有桌面版镜像就先用了。装系统时有个坑要提醒:如果你在虚拟机里装,显卡直通和显存分配要提前规划好,不然后面跑 embedding 模型会非常慢。

系统装完第一件事是换源。默认源在国内访问速度不稳定,换成国内镜像源之后 apt 安装快很多。具体操作是编辑/etc/apt/sources.list,把里面的地址替换掉,然后sudo apt update。这一步看着简单,但源没换对会导致后面装 Docker、装驱动各种超时。

接着装基础工具链。build-essential、git、curl、wget、vim这些是必须的。Python 环境我建议用系统自带的 3.10,然后配一个 venv 虚拟环境,不要直接往系统 Python 里装包,不然后面依赖冲突很难受。

sudo apt update sudo apt install -y build-essential git curl wget vim python3-pip python3-venv python3 -m venv /opt/kb-env source /opt/kb-env/bin/activate

显卡驱动这块单独说。如果你要用 GPU 加速 embedding 推理,NVIDIA 驱动和 CUDA 工具包必须装对版本。我遇到过驱动装完nvidia-smi能显示但 Python 里torch.cuda.is_available()返回 False 的情况,原因是驱动版本和 CUDA 版本不匹配。稳妥做法是先确定你要用的深度学习框架需要哪个 CUDA 版本,再倒推装对应的驱动。

注意:卸载显卡驱动时不要用apt autoremove一把梭,容易把桌面环境依赖一起删掉导致进不去系统。用sudo apt-get purge nvidia-*精确卸载更安全。

2.2 Docker 与容器化部署环境

虽然可以直接裸机跑,但我强烈建议用 Docker。原因有三:依赖隔离干净、迁移方便、出问题重建快。装 Docker 的步骤官方文档写得很清楚,这里只说几个容易出错的点。

安装完成后要把当前用户加入 docker 组,否则每次都要 sudo。命令是sudo usermod -aG docker $USER,然后重新登录生效。另外 Docker 默认的镜像存储位置在/var/lib/docker,如果系统盘小,要提前改到数据盘,不然镜像和容器数据很快把根分区撑满。

sudo mkdir -p /data/docker sudo vim /etc/docker/daemon.json

在 daemon.json 里写入:

{ "data-root": "/data/docker", "registry-mirrors": ["你的镜像加速地址"] }

然后sudo systemctl restart docker。这一步做完可以用docker info确认存储路径已经改过去。

2.3 Halogen 及其依赖的安装

Halogen 本身是 Python 包,通过 pip 安装。但它依赖一些底层库,比如用于向量检索的 faiss、用于文本处理的 jieba 分词、用于文档解析的 pypdf 和 python-docx。这些依赖有的需要编译,所以前面装 build-essential 是必要的。

pip install halogen-retrieval faiss-cpu jieba pypdf python-docx

如果你要用 GPU 版 faiss,把faiss-cpu换成faiss-gpu,但要注意 CUDA 版本对应关系。我一开始装了 GPU 版结果和系统 CUDA 不匹配,跑起来直接段错误,换回 CPU 版反而稳定。后来查清楚是 faiss-gpu 对 CUDA 小版本很敏感,索性向量检索用 CPU,embedding 推理用 GPU,各司其职。

embedding 模型我最终选的是一个中文优化过的开源模型,参数量在 1B 左右,24G 显存能轻松放下,单条推理延迟在 30ms 上下。模型文件提前下载好放到本地目录,不要每次从网络拉,内网环境根本拉不动。

3. 知识库数据流水线的搭建过程

3.1 文档解析与清洗的关键步骤

6.4 万份文档,格式五花八门。PDF 占大头,大概四万份,剩下的是 Word、Excel、Markdown 和 HTML。解析这步看着枯燥,但直接决定后面检索质量的上限。垃圾进垃圾出,解析不干净,后面模型再好也白搭。

PDF 解析我用 pypdf 打底,遇到扫描件就上 OCR。这里有个经验:不要指望一个库解决所有 PDF。有的 PDF 是文字层完整的,pypdf 直接抽就行;有的是图片扫描的,必须走 OCR;还有的是排版复杂的表格,抽出来全是乱序。我的做法是先跑一遍 pypdf,统计每份文档抽出的字符数,低于阈值的标记为疑似扫描件,再走 OCR 流程。

清洗规则我列了一个清单,按优先级处理:

  • 去掉连续重复行,很多 PDF 每页页眉页脚都一样,不去掉会污染向量
  • 合并被硬换行切断的句子,中文文档里这个特别常见
  • 过滤纯符号行和空白行
  • 统一全角半角标点
  • 去掉文档里的水印文字,比如“内部资料请勿外传”这种
import re def clean_text(text): lines = text.split('\n') seen = set() result = [] for line in lines: line = line.strip() if not line or line in seen: continue if re.fullmatch(r'[\W_]+', line): continue seen.add(line) result.append(line) merged = ''.join(result) merged = re.sub(r'\s+', ' ', merged) return merged

这段代码逻辑简单但实用。seen集合去重能干掉大量重复页眉,正则过滤纯符号行,最后把换行合并成空格。实测下来,清洗前后同一份文档的检索命中率能差出两成。

3.2 文本切块策略与参数计算

切块是知识库检索里最容易被忽视但影响最大的环节。切太大,一个块里混了好几个主题,检索时向量被平均掉,匹配不准;切太小,上下文丢失,模型理解不了完整语义。

我的策略是按语义边界切,优先在段落、标题、列表项处断开,而不是机械地按字数切。具体参数上,我设定的目标块大小是 500 个中文字符,重叠 80 个字符。为什么是这两个数?500 字大约对应一段完整的论述,能覆盖一个独立知识点;80 字重叠是为了防止关键信息正好卡在切分点上被切断。

对于结构化文档,比如带标题层级的 Markdown 或 Word,我会把标题路径拼到每个块的头部。比如一个块属于“第三章 售后政策 > 3.2 退货流程”,那这个块的文本开头就加上这个路径。这样检索时标题信息也参与匹配,效果提升明显。

def split_by_semantic(text, max_len=500, overlap=80): paragraphs = text.split('\n') chunks = [] current = '' for para in paragraphs: if len(current) + len(para) <= max_len: current += para + '\n' else: if current: chunks.append(current.strip()) current = current[-overlap:] + para + '\n' if current else para + '\n' if current.strip(): chunks.append(current.strip()) return chunks

这个切块函数是简化版,实际生产里我还加了标题识别和表格特殊处理。表格我单独抽出来转成 Markdown 格式再切,不跟正文混在一起。

3.3 向量化批处理与入库流程

切完块就是批量过 embedding 模型。6.4 万份文档切下来大概 38 万个块,这个量级必须批处理。我设的 batch size 是 64,太大显存扛不住,太小吞吐上不去。38 万个块按 64 一批,大概 6000 批,单批推理 200ms 左右,全量跑完差不多 20 分钟。

入库时要注意向量和原文的对应关系。每个块我存四个字段:块 ID、原文文本、向量、元数据(来源文档、标题路径、页码)。元数据在展示结果时用来定位原文,非常重要。

import numpy as np from halogen import Index index = Index(dim=1024, metric='cosine') def batch_embed(texts, model, batch_size=64): vectors = [] for i in range(0, len(texts), batch_size): batch = texts[i:i+batch_size] vecs = model.encode(batch, normalize_embeddings=True) vectors.append(vecs) return np.vstack(vectors)

向量归一化这步别省。归一化之后余弦相似度计算可以简化成点积,检索速度快不少,而且数值稳定性更好。

4. 检索效果调优与常见问题排查

4.1 混合检索权重调参实录

Halogen 的混合检索是把向量召回和关键词召回的结果融合。融合方式我用的加权求和,向量权重 0.7,关键词权重 0.3。这个比例不是拍脑袋定的,是拿一批标注好的查询-文档对跑出来的。

调参过程是这样的:先准备 200 条真实用户查询,每条人工标注出最相关的文档。然后网格搜索权重组合,从 0.5/0.5 到 0.9/0.1 逐个试,看哪个组合的召回率最高。结果 0.7/0.3 在准确率和召回率上最平衡。纯向量在语义匹配上强,但遇到专有名词、型号代码这种精确匹配就拉胯,关键词召回正好补上这块。

提示:权重不是一劳永逸的。如果你的知识库专有名词多,关键词权重可以调高到 0.4;如果是口语化查询多,向量权重调到 0.8 更合适。

4.2 常见问题速查表

实际跑起来遇到的问题不少,我整理成表格方便对照排查。

问题现象可能原因排查方法解决方案
检索结果完全不相关embedding 模型加载失败或维度不匹配检查模型输出维度与索引维度是否一致重新确认模型配置,重建索引
部分文档搜不到解析阶段文本为空抽查该文档解析后的字符数走 OCR 流程或换解析库
查询延迟突然飙升索引未加载进内存查看进程内存占用增大内存或优化索引结构
中文查询匹配差分词器未正确配置打印分词结果换用中文分词并加自定义词典
新增文档不生效增量索引未触发检查索引更新日志手动触发或修复增量逻辑
显存溢出batch size 过大监控显存占用调小 batch size 或换 CPU 推理

4.3 几个踩过的坑和独家技巧

第一个坑是编码问题。有些 Word 文档里混了 GBK 编码的字符,Python 默认 UTF-8 读取直接报错。我的处理是读取时加errors='ignore',虽然会丢个别字符,但总比整个文档解析失败强。

第二个坑是索引重建时间。全量重建 38 万个块的索引要将近一小时,期间服务不可用。后来改成双索引切换:新索引在后台建好,建完原子切换,服务不中断。这个改动对生产环境太重要了。

第三个技巧是关于查询扩展。用户输入往往很短,比如“退货”,直接检索召回有限。我在查询前加了一步同义词扩展,“退货”扩展成“退货 退款 退换”,召回率提升明显。同义词表是人工维护的,针对业务术语补充,比通用同义词库准得多。

第四个经验是结果重排。初筛召回 top 50,再用一个小的交叉编码器重排取 top 10。这一步增加约 100ms 延迟,但准确率提升非常值。交叉编码器我用的是一个小参数量模型,显存占用小,推理快。

5. 性能压测与生产环境稳定性保障

5.1 压测方案与关键指标

上线前我做了完整压测。工具用的 locust,模拟 50 并发持续查询。关键指标盯三个:QPS、P95 延迟、错误率。

压测结果:单机 QPS 稳定在 120 左右,P95 延迟 780ms,错误率 0。这个成绩对 6.4 万文档规模来说够用了。瓶颈主要在 embedding 推理,占了总延迟的六成。如果 QPS 要求更高,可以加一张显卡做模型并行,或者把 embedding 结果缓存起来,相同查询直接命中缓存。

缓存这块我做了两级:一级是查询结果缓存,相同 query 五分钟内直接返回;二级是 embedding 缓存,相同文本块不重复计算。两级缓存加起来,重复查询的延迟降到 50ms 以内。

5.2 监控与告警配置

生产环境不能没有监控。我配了三个维度的监控:系统层看 CPU、内存、显存、磁盘;应用层看 QPS、延迟、错误率;业务层看检索命中率和用户反馈。

告警阈值设定:P95 延迟超过 1.5 秒告警,错误率超过 1% 告警,显存占用超过 90% 告警。告警通道走的内部 IM,值班同学能第一时间收到。

日志我分了三个级别:INFO 记录每次查询的基本信息,WARNING 记录慢查询和空结果,ERROR 记录异常。日志按天切割,保留 30 天。排查问题时先看 WARNING 日志,慢查询和空结果往往能暴露索引或模型的问题。

5.3 数据更新与索引维护策略

企业知识库不是静态的,每天都有新文档进来,旧文档作废。我的更新策略是:新增文档走增量索引,几分钟内可检索;文档删除走软删除,标记失效但不立即从索引移除,每天凌晨做一次索引整理,真正清理失效数据。

为什么软删除?因为立即从 HNSW 索引里删数据代价很高,而且容易影响索引结构。软删除加定期整理,兼顾了实时性和性能。

索引整理任务我写成了一个定时脚本,每天凌晨 3 点跑。整理内容包括:清理软删除数据、合并小索引段、重建统计信息。整理期间服务正常可用,只是新文档入库会排队到整理结束。

6. 成本核算与后续扩展方向

6.1 硬件与时间成本盘点

整套方案的成本我算过一笔账。硬件一次性投入主要是那台带显卡的服务器,按市场价大概三万出头。电费按满载 500W 算,一天 12 度电,一个月电费不到 200 块。人力成本主要是搭建和调优,我一个人前后花了大概两周,其中一半时间在调检索效果。

对比原来的 SaaS 方案,按 100 个席位算,一年费用够买两台服务器。自建方案跑满一年就回本,之后都是净省。当然自建有维护成本,但对我们这种有运维能力的团队来说,这点维护量可以接受。

6.2 后续可以扩展的方向

现在这套系统跑得挺稳,但还有优化空间。第一个方向是加多路召回,除了向量和关键词,再加一路基于文档结构的召回,比如按标题层级匹配。第二个方向是引入查询理解,把用户的口语化问题改写成更适合检索的形式。第三个方向是做个性化排序,不同部门的人搜同样的词,期望结果可能不一样,可以根据用户历史行为调整排序。

还有一个想法是把 embedding 模型换成更小的蒸馏版本,推理速度能再快一倍,精度损失控制在 3% 以内。这个需要再跑一轮评测确认。

我个人在实际操作中的体会是,知识库这东西,搭建只是开始,真正的功夫在持续调优。检索效果没有最好只有更好,每次用户反馈“搜不到”都是一次优化机会。把反馈收集起来,定期分析,慢慢就能把匹配度磨上去。另外别迷信大模型,合适的才是最好的,小模型调好了照样能打。

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

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

立即咨询