首页 / 资讯中心 / 文章详情

Ubuntu 22.04 自建企业知识库:Halogen + pgvector 部署与调优实战

Ubuntu 22.04 自建企业知识库:Halogen + pgvector 部署与调优实战 ★ FEATURED ARTICLE
1. 为什么要在 Ubuntu 上自建企业知识库1.1 从 6.4 万份文档说起一个真实的需求场景我所在团队维护着一个积累了六年多的企业知识库里面沉淀了产品文档、技术方案、会议纪要、客户案例、运维手册等各类资料总数大概在 6.4 万份左右。这些文件散落在共享盘、邮件附件、个人电脑和几个不同的协作工具里格式从 Word、PDF 到 Markdown、Excel 五花八门。最头疼的问题是找东西全靠关键词搜索搜出来的结果要么不相关要么漏掉了真正有用的那份。举个例子客户问“你们那个数据同步方案在高并发下怎么保证一致性”我在旧系统里搜“数据同步 一致性”返回的是三年前的一份会议记录而真正详细的技术方案因为标题里写的是“CDC 链路设计”而完全没被命中。这种体验逼着我们必须换一套真正能理解语义的检索系统。这就是我决定在 Ubuntu 上部署 Halogen 并搭建企业知识库的直接原因。Halogen 是一套面向私有化部署的知识库问答框架核心能力是把非结构化文档切分、向量化再通过向量检索和大模型生成答案。它解决的不是“存文件”的问题而是“让文件里的知识能被准确找到并直接回答问题”的问题。适合谁来参考任何手里有几千到几十万份内部文档、希望数据不出内网、又想让检索体验上一个台阶的团队不管你是运维、后端开发还是技术负责人这套思路都能直接抄。1.2 为什么选 Ubuntu 而不是别的系统企业知识库部署对操作系统的要求其实很朴素稳定、包管理方便、社区资料多、长期支持版本省心。Ubuntu 22.04 LTS 正好全中。LTS 版本有五年官方维护意味着我不用每隔半年就折腾一次系统升级apt包管理让 Docker、Python、GCC 这些依赖的安装几乎零心智负担遇到问题时社区里关于 Ubuntu 的排查资料是最丰富的这一点在深夜排障时价值极高。我试过在别的发行版上部署类似系统最大的坑是某些 Python 包的预编译轮子不兼容得从源码编译一编就是半小时。Ubuntu 22.04 的 glibc 版本和主流 wheel 的兼容性最好pip install基本不会卡在编译环节。另外如果你打算用 GPU 做 embedding 推理Ubuntu 对 NVIDIA 驱动的支持也是最成熟的虽然驱动卸载偶尔会让人抓狂但安装和使用的路径非常清晰。提示生产环境务必选 Ubuntu 22.04 LTS 或 24.04 LTS不要用非 LTS 版本。非 LTS 的支持周期只有九个月知识库这种长期运行的服务经不起频繁的系统迁移。1.3 Halogen 在整条链路里扮演什么角色很多人会把 Halogen 和 Dify、RAGFlow 这类工具混在一起比较。我的理解是Halogen 更像是一个专注在检索增强生成RAG链路上的执行引擎它把文档解析、分块、embedding、向量存储、检索、重排、生成这几个环节串成一条可配置的流水线。你不需要自己从零写 LangChain 代码也不用纠结向量库选哪个它把常见组合都封装好了。整条链路的核心是 embedding 和向量检索。embedding 模型负责把一段文字变成一串高维向量语义相近的文字在向量空间里距离更近。向量检索就是在这个空间里找“离问题最近”的文档片段。Halogen 的价值在于它把这条链路的工程细节处理好了分块策略、批量 embedding、索引构建、检索时的 top-k 和相似度阈值都有合理的默认值同时留出了调参入口。我最终跑起来的架构是这样的Ubuntu 22.04 作为底座Docker 跑 Halogen 服务PostgreSQL 加 pgvector 做向量存储embedding 模型用 BGE-M3 本地部署生成模型走内部的大模型接口。整套系统完全在内网运行6.4 万份文档全量索引后单次检索加生成的响应时间稳定在 2 到 4 秒。2. 部署前的环境准备与关键选型2.1 Ubuntu 基础环境配置的避坑清单拿到一台干净的 Ubuntu 22.04 机器后别急着装 Halogen先把基础环境理顺。我踩过的坑里有一半是环境变量和依赖版本引起的。第一件事是更新系统并安装基础工具sudo apt update sudo apt upgrade -y sudo apt install -y build-essential git curl wget vim htopbuild-essential里包含 GCC 和 make很多 Python 包在没有预编译轮子时需要它。我遇到过ubuntu安装gcc失败的情况绝大多数是因为 apt 源没更新或者磁盘空间不足先跑df -h看下根分区再跑apt update基本能解决。第二件事是配置环境变量。知识库服务通常需要设置OPENAI_API_KEY之类的变量但我们是私有化部署用的是内部模型接口所以变量名可能不同。关键是不要把密钥写死在代码或 docker-compose 文件里用.env文件管理并确保.env在.gitignore里。我见过有人把密钥提交到仓库结果被迫全量轮换非常被动。第三件事是确认时区和 locale。知识库里的文档时间戳如果时区不对检索时按时间过滤会出诡异问题sudo timedatectl set-timezone Asia/Shanghai sudo locale-gen zh_CN.UTF-8 en_US.UTF-82.2 Docker 与 Docker Compose 的安装要点Halogen 官方推荐用 Docker 部署这能省掉大量依赖冲突的麻烦。安装 Docker 不要用apt install docker.io那个版本太旧用官方脚本curl -fsSL https://get.docker.com | sudo sh sudo usermod -aG docker $USER执行完usermod后必须重新登录否则当前会话里docker命令还是要加sudo。这个细节很多人会忽略然后疑惑为什么权限不对。Docker Compose 现在已经是 Docker 插件形式安装 Docker 时一般会自带docker compose命令。验证一下docker compose version如果提示找不到命令单独装一下 compose 插件。装完后建议配置 Docker 的日志轮转否则跑几个月后日志能把磁盘撑满。编辑/etc/docker/daemon.json{ log-driver: json-file, log-opts: { max-size: 100m, max-file: 3 } }改完sudo systemctl restart docker生效。这个配置我强烈建议在部署任何长期服务前都加上血泪教训。2.3 向量数据库选型为什么我最终用了 pgvector向量库的选择是知识库部署里最关键的决策之一。市面上常见的有 Milvus、Qdrant、Weaviate、Chroma 和 pgvector。我一开始想用 Milvus因为它专为向量检索设计性能强。但实际评估后我选了 pgvector原因有三点。第一6.4 万份文档切块后的向量规模大概在 80 万到 120 万条之间这个量级对 pgvector 来说毫无压力。pgvector 在千万级向量下配合 HNSW 索引检索延迟仍在毫秒级。Milvus 的优势要到亿级向量才明显体现我们远没到那个规模。第二运维成本。pgvector 就是 PostgreSQL 的一个扩展我只需要维护一个数据库备份、监控、权限管理全部复用现有 PostgreSQL 的经验。Milvus 要额外维护一套分布式组件对只有一两个人的小团队来说是负担。第三元数据过滤。企业知识库检索经常需要按部门、文档类型、时间范围过滤pgvector 可以直接用 SQL 的WHERE子句和向量距离排序结合写起来非常自然。Milvus 的标量过滤虽然也支持但语法和调试体验不如 SQL 直观。安装 pgvector 很简单用官方镜像services: postgres: image: pgvector/pgvector:pg16 environment: POSTGRES_PASSWORD: your_password POSTGRES_DB: knowledge volumes: - ./pgdata:/var/lib/postgresql/data ports: - 5432:5432启动后进库执行CREATE EXTENSION vector;就启用了向量能力。2.4 embedding 模型选型BGE-M3 的实际表现embedding 模型决定了检索质量的上限。我对比过几个主流选择OpenAI 的 text-embedding-3 系列效果确实好但数据要出内网直接排除BGE-large-zh 中文效果不错但只支持中文BGE-M3 支持多语言、支持长文本8192 token而且可以本地部署最终选了它。BGE-M3 的向量维度是 1024这个维度在存储和检索效率之间比较平衡。部署方式我用的是 HuggingFace 的 text-embeddings-inference 镜像GPU 推理docker run -d --gpus all -p 8080:80 \ -v /data/models:/data \ ghcr.io/huggingface/text-embeddings-inference:latest \ --model-id BAAI/bge-m3 \ --max-batch-tokens 16384这里--max-batch-tokens要根据显存调。我用的是一张 16G 显存的卡设 16384 比较稳。如果显存小调低这个值否则会 OOM。实测下来6.4 万份文档全量 embedding 大概跑了 40 分钟速度可以接受。注意embedding 模型一旦选定索引里的向量就和模型绑定了。如果以后换模型必须全量重新 embedding因为不同模型的向量空间不兼容。所以选型时要想清楚别中途换。3. Halogen 部署与知识库索引构建实操3.1 Halogen 服务的部署与配置Halogen 的部署我走的是 Docker Compose 路线把 Halogen 服务、PostgreSQL、embedding 服务编排在一起。核心配置文件docker-compose.yml大致长这样services: halogen: image: halogen/knowledge-base:latest depends_on: - postgres - embedding environment: DATABASE_URL: postgresql://postgres:your_passwordpostgres:5432/knowledge EMBEDDING_ENDPOINT: http://embedding:80/v1/embeddings EMBEDDING_MODEL: BAAI/bge-m3 LLM_ENDPOINT: http://your-internal-llm/v1/chat/completions LLM_MODEL: your-model-name volumes: - ./data:/app/data - ./logs:/app/logs ports: - 3000:3000几个关键点解释一下。EMBEDDING_ENDPOINT指向本地 embedding 服务格式要符合 OpenAI 兼容接口text-embeddings-inference 默认就是这个格式。LLM_ENDPOINT指向内部大模型接口同样要求 OpenAI 兼容。如果你的内部模型接口不是这个格式需要加一层适配或者用 one-api 这类网关转换。volumes里的./data用来放上传的原始文档和解析后的中间文件./logs放日志。这两个目录要确保磁盘空间充足6.4 万份文档的原始文件加解析产物我这边占了大概 120G。启动命令docker compose up -d docker compose logs -f halogen看到服务正常监听 3000 端口就可以进 Web 界面了。3.2 文档批量导入与分块策略6.4 万份文档不可能手动上传Halogen 提供了批量导入的 API。我写了个 Python 脚本遍历共享盘目录按扩展名过滤后调用导入接口import os import requests API_BASE http://localhost:3000/api SUPPORTED {.pdf, .docx, .md, .txt, .xlsx} def import_file(path): with open(path, rb) as f: files {file: (os.path.basename(path), f)} data {collection: enterprise-kb} resp requests.post(f{API_BASE}/documents/import, filesfiles, datadata) return resp.status_code for root, _, files in os.walk(/mnt/shared/knowledge): for name in files: ext os.path.splitext(name)[1].lower() if ext in SUPPORTED: status import_file(os.path.join(root, name)) print(f{name}: {status})导入过程中最影响检索质量的是分块策略。Halogen 默认按固定 token 数切分带一定重叠。我调整成了按语义段落切分最大块 512 token重叠 64 token。为什么是 512因为 BGE-M3 虽然支持 8192 token但块太大时一个块里混了多个主题检索时相似度会被稀释反而不准。512 token 大约对应中文 300 到 400 字正好是一个完整段落的长度。重叠 64 token 是为了防止关键信息正好被切在边界上。比如一句话被切成两半前半句在块 A 末尾后半句在块 B 开头检索时两个块都可能命中但都不完整。有重叠就能缓解这个问题。实操心得分块参数没有万能值要拿真实文档试。我的做法是挑 20 个典型问题用不同分块参数各跑一遍检索看 top-5 命中率。512/64 这组参数在我们文档上命中率最高但你的文档结构不同结论可能不一样。3.3 向量索引构建与性能调优文档导入后Halogen 会异步做 embedding 和索引构建。80 多万条向量写入 pgvector 后必须建 HNSW 索引否则检索会走全表扫描慢到无法接受。CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops) WITH (m 16, ef_construction 64);m和ef_construction是 HNSW 的核心参数。m控制每个节点的连接数越大索引越精确但占用内存越多ef_construction控制构建时的搜索深度越大构建越慢但索引质量越高。16 和 64 是比较通用的起点。如果检索召回率不够先把ef_construction提到 128 重建索引试试。检索时的ef_search参数也影响召回率和延迟SET hnsw.ef_search 100;默认值是 40我调到 100 后召回率明显提升延迟从 8ms 涨到 15ms完全可接受。这个参数是查询时生效的可以按需调整不用重建索引。索引构建那晚我盯着日志看了一个多小时80 万条向量建 HNSW 索引大概花了 25 分钟。建完后磁盘占用增加了约 1.5G内存里索引常驻大概 2G 左右。这些数字供你规划资源时参考。3.4 检索链路的参数调优与效果验证索引建好后检索链路还有几个参数要调。Halogen 的检索流程是问题 embedding → 向量检索 top-k → 重排 → 生成答案。top-k 我设的是 20也就是先召回 20 个候选块再用重排模型精排最后取前 5 个送给大模型生成。为什么先召回 20 再精排因为向量检索是粗筛它快但不够准。重排模型我用的是 BGE-reranker-v2-m3会对问题和每个候选块做精细的相关性打分把真正相关的排到前面。20 这个数字是召回率和延迟的平衡点再大延迟会明显上升。相似度阈值我设的是 0.35。低于这个分数的块直接丢弃避免把不相关的内容喂给大模型导致胡编。这个阈值要根据你的 embedding 模型调BGE-M3 的余弦相似度分布大概是这样真正相关的块通常在 0.5 以上勉强相关的在 0.4 到 0.5不相关的低于 0.35。验证效果我用了 50 个真实问题人工标注了标准答案所在的文档。调整参数前后对比参数组合top-5 命中率平均响应时间默认参数62%2.1s512分块top20重排84%3.2s再加阈值0.3586%2.8s命中率从 62% 提到 86%响应时间只增加了不到 1 秒这个 trade-off 非常值。4. 上线后遇到的典型问题与排查实录4.1 检索结果不相关的排查思路上线第一周有同事反馈搜“报销流程”返回的是“采购流程”的文档。这类问题排查要分三层看。第一层embedding 是否正常。我写了个小脚本把“报销流程”和“采购流程”分别 embedding算余弦相似度。如果相似度高于 0.9说明模型区分不开这两个概念那是模型能力问题。实测下来相似度是 0.72模型能区分问题不在这一层。第二层分块是否合理。我去看了命中的那个块发现“采购流程”文档里有一段讲“费用报销”被切成了一个独立的块。这个块和“报销流程”的相似度确实高被召回是合理的。问题在于重排没有把它压下去。我检查了重排模型发现它给这个块的分数是 0.68给真正“报销流程”文档的块是 0.71差距太小。第三层重排模型是否适合中文。我原来用的重排模型对中文长文本的区分度一般换成 BGE-reranker-v2-m3 后两个块的分数拉开到 0.52 和 0.83问题解决。这个排查过程告诉我检索不准不要只盯着向量库重排模型的影响可能更大。4.2 服务内存占用过高与 OOM 处理跑了大概两周后Halogen 服务开始偶尔 OOM 被杀。docker stats看内存占用从 2G 慢慢涨到 8G。这是典型的内存泄漏或缓存无上限问题。排查发现两个原因。一是 embedding 服务的批处理缓存没有上限文档导入高峰期缓存了大量中间结果。二是我在 Halogen 配置里开了检索结果缓存但没设 TTL 和最大条数。解决办法给 embedding 服务加--max-client-batch-size限制给检索缓存设CACHE_MAX_SIZE10000和CACHE_TTL3600。改完后内存稳定在 3G 左右。提示任何带缓存的组件都要设上限。我现在的习惯是看到配置里有 cache 相关选项第一反应就是找 max_size 和 ttl没有就自己加。4.3 文档更新后索引不同步的解决知识库不是一次性的文档会更新。最初的实现是文档更新后重新导入但发现旧向量还在索引里导致同一份文档出现新旧两个版本检索时可能命中旧内容。Halogen 支持按文档 ID 删除旧向量再插入新向量但需要开启upsert模式。我在导入脚本里加了文档指纹文件路径加修改时间每次导入前先按指纹删除旧记录def upsert_file(path): doc_id hashlib.md5(path.encode()).hexdigest() requests.delete(f{API_BASE}/documents/{doc_id}) import_file(path)这样保证同一份文档在索引里只有一份最新版本。代价是每次更新都要重新 embedding但对于增量更新来说量不大可以接受。4.4 常见问题速查表现象可能原因排查方法解决检索结果不相关重排模型弱 / 分块过大看命中块内容换重排模型 / 调小分块服务 OOM缓存无上限docker stats 看内存曲线设缓存 max_size 和 ttl检索慢没建 HNSW 索引EXPLAIN 看执行计划建 HNSW 索引文档更新后检索到旧内容旧向量未删查同 doc_id 记录数开启 upsert 模式embedding 服务超时批处理太大看 embedding 日志调小 max-batch-tokens中文乱码locale 未配locale命令生成 zh_CN.UTF-85. 让知识库真正好用的几个进阶技巧5.1 混合检索向量加关键词的双保险纯向量检索有个天然短板对专有名词、产品型号、错误码这类精确匹配不敏感。比如搜“ERR-5023”向量检索可能返回一堆语义相近但错误码不同的文档。解决办法是混合检索向量检索和关键词检索各跑一遍结果融合。Halogen 支持配置混合检索权重。我的设置是向量检索权重 0.7关键词检索权重 0.3。对于包含明显专有名词的问题关键词那一路能兜住底。实测下来混合检索让包含错误码、型号类问题的命中率从 71% 提到了 93%。5.2 元数据过滤缩小检索范围6.4 万份文档全库检索即使向量检索很快召回的内容也可能跨部门、跨年份。加元数据过滤能大幅提升相关性。我在导入时给每份文档打上了部门、文档类型、年份三个标签检索时可以按需过滤{ query: 数据同步方案, filter: { department: 技术部, year: {$gte: 2023} }, top_k: 20 }这样搜出来的都是技术部近两年的文档噪音少了很多。元数据过滤和向量检索结合是 pgvector 相比专用向量库的一大优势SQL 写起来很顺手。5.3 定期评估检索质量的方法知识库上线不是终点检索质量会随着文档增长而漂移。我建了一个评估集包含 100 个真实问题和对应的标准答案文档 ID。每个月跑一次自动评估计算 top-5 命中率和 MRR平均倒数排名。def evaluate(eval_set): hits 0 mrr 0 for q, expected_doc in eval_set: results search(q, top_k5) doc_ids [r[doc_id] for r in results] if expected_doc in doc_ids: hits 1 rank doc_ids.index(expected_doc) 1 mrr 1 / rank return hits / len(eval_set), mrr / len(eval_set)命中率下降超过 5 个百分点就说明该调参或重新审视分块策略了。这个评估集是我花了两天人工标注的但非常值它让检索质量从“感觉还行”变成了可量化的指标。5.4 备份与恢复别等出事才想起来知识库的价值在于数据备份策略必须提前定。我备份三样东西PostgreSQL 数据库含向量、原始文档目录、Halogen 配置文件。数据库用pg_dump每天凌晨全量备份保留 30 天pg_dump -h localhost -U postgres knowledge | gzip /backup/knowledge_$(date %F).sql.gz原始文档目录用 rsync 同步到另一台机器。配置文件纳入 Git 管理。恢复演练我做过一次从备份恢复到服务可用大概花了 40 分钟其中数据库恢复占了大头。这个时间要心里有数别等真出事才发现恢复要几个小时。注意pgvector 的向量数据在 pg_dump 里是正常导出的恢复后索引需要重建。如果数据量大重建索引的时间要算进恢复时间。我的 80 万条向量重建 HNSW 索引花了 25 分钟。6. 关于这套方案的一些个人体会这套系统从部署到稳定运行前后折腾了大概三周。回头看最花时间的不是 Halogen 本身的部署而是分块策略调优和检索效果验证。部署本身一天就能跑通但要让 6.4 万份文档真正“好用”需要在分块、重排、阈值这些细节上反复试。我最大的体会是知识库的效果上限由 embedding 模型决定但实际效果由分块和重排决定。很多人把精力全花在选模型上却忽略了分块策略这个更影响结果的因素。同样用 BGE-M3分块从 1024 调到 512命中率能差 15 个百分点。另一个体会是评估集要尽早建。我一开始靠感觉调参调来调去不知道有没有变好。建了评估集之后每次改动都有数字反馈调参效率高了一个数量级。如果你正准备搭知识库建议在导入文档的同时就开始标注评估集哪怕只有 30 个问题也比没有强。最后分享一个小技巧Halogen 的日志里会记录每次检索的 query 和命中的文档定期分析这些日志能发现很多问题。比如我通过日志发现有相当一部分查询是“XX 怎么配置”这种操作类问题而这类问题的答案往往散落在多个文档里。针对这种情况我在生成环节加了多文档聚合的提示词让大模型综合多个块的内容回答效果比单块生成好很多。
阅读完成 · 觉得有帮助?
咨询建站