我们团队花了差不多一个季度的时间,把一个原本"只能存资料"的 Wiki 系统,逐渐改造成了一个具备语义检索和智能问答能力的大模型知识库。这个项目的代号就叫 llm_wiki,今天把这套从零开始的设计思路、工具选型、实操踩坑过程完整记录下来。如果你正在做个人知识库,或者团队内部想搞一套"能聊天的 Wiki",这篇文章应该能帮你少走不少弯路。
要理解这个项目在干什么,可以先想一个场景:传统 Wiki 的搜索,本质上是关键词匹配,你得记得文档里有某个词,才能搜到。而 llm_wiki 的思路,是把 Wiki 里的每一篇文档都切成块、做成向量索引,用户提问时先做语义检索,把最相关的几个内容片段捞出来,再交给大语言模型(LLM)生成自然语言回答。这样一来,知识库就从一个"存放资料的仓库",变成了一个"随时可以对话的助手"。
适用的人很明确:一是做知识管理的个人用户,二是团队协作里被文档检索效率折磨的工程师,三是想在公司内部落地 RAG(检索增强生成)方案但不知道从何入手的技术人员。下面我会先讲架构选型背后的思考,再逐步拆解数据准备、组件选型、实操搭建,最后把真实环境中遇到的高频问题列出来。
1. 项目核心思路:为什么"LLM + Wiki"而不是"Wiki + 关键词搜索"
1.1 传统知识库的三个核心痛点
我接手这个项目前,团队已经有一个积累了大量技术文档、会议记录和项目复盘资料的 Wiki,但使用率越来越低。用户反馈很一致:搜不到、看不完、提炼不出重点。
"搜不到"是关键词匹配的天然缺陷。比如文档里写的是"缓存失效问题",用户搜"服务响应变慢",词面不匹配就查不出来。"看不完"是信息过载的问题。一篇技术方案上万字,用户只想知道"最终选型是什么、为什么这样选",传统 Wiki 给不了这种"提取式"答案。"提炼不出重点"则是更深层的需求,用户希望知识库能回答问题,而不是返回一堆文档链接让用户自己判断。
这三个痛点本质上指向同一个方向:知识库的交互方式需要从"检索文档"升级为"获取答案"。而 2024 年以来 LLM 能力的成熟,让这个升级有了低成本、可落地的路径。
1.2 为什么选择 RAG 架构,而不是微调模型
确定要引入 LLM 之后,团队内部其实有过一次争论:是微调一个领域专属模型,还是走 RAG(Retrieval-Augmented Generation,检索增强生成)路线?
微调方案的技术路径是:把历史文档整理成训练语料,选择一个开源底座模型,用 LoRA 等方式做领域适配。听起来很"终极",但落地时问题很多。一是文档更新频率高,每次新增内容都要增量训练,流程太重;二是训练需要显卡资源和 MLOps 工程能力,团队没有专职算法同学;三是微调模型并不天然具备"引用出处"的能力,回答错了很难定位原因。
RAG 的路线逻辑完全不同——文档不进模型,而是做索引、切片、向量化后存入向量数据库。用户提问时,系统先在向量库里做相似度检索,找到最相关的若干文本片段,再把这些片段拼进 Prompt,让 LLM 基于片段内容生成回答。它的核心优势是:不训练模型、知识可溯源、更新只需重传文档索引。对我这种"没有算法团队但又要快速交付"的项目来说,RAG 几乎是唯一解。
1.3 llm_wiki 的整体架构
整个系统按数据流可以分成五层:
- 数据接入层:负责从 Wiki、本地 Markdown、PDF、Word、网页 URL 采集文档。
- 文档处理层:做格式清洗、切块(Chunking)、元数据标记。
- 向量化索引层:用嵌入模型把每个 Chunk 转成向量,写入向量数据库。
- 检索层:接收用户问题,语义检索 + 重排序(Rerank),取出 Top-K 知识片段。
- 生成层:把知识片段拼进 Prompt,调用 LLM 生成最终回答,并附上引用来源。
这个架构本身不复杂,但每一层都有不少细节,尤其是文档切块和检索策略,直接决定了最终回答质量的上限。后续章节我按照这个分层逐步展开。
2. 数据准备与文本处理:决定知识库质量的第一道关卡
2.1 文档接入:先解决"格式统一"这场持久战
llm_wiki 的数据来源非常杂:既有飞书文档导出的 MD 文件,也有历史遗留的 PDF 扫描件、Word 技术方案、网页收藏。我的建议是:第一版不要追求全格式覆盖,先把最高频的三种格式打通——Markdown、带层级标题的 HTML、可复制文本的 PDF。
Markdown 是最理想的输入格式。因为切块时可以借助标题层级来做语义边界,处理代码块、表格、列表也更干净。但现实是很多 Wiki 导出的是 HTML,我一般先统一转换为 Markdown。手动写脚本容易坏,直接用开源的 trafilatura 做网页正文抽取,或者用 pandoc 做格式互转,稳定性好很多。
PDF 是另一个大坑。扫描版 PDF 必须先 OCR,否则后面的切块和向量化质量会很差。我在项目里用的是 PaddleOCR,对中文支持不错。但经验是:能拿到 Word 或 Markdown 源文件的尽量用源文件,OCR 会引入错字,这些错字在语义检索阶段的负面影响比想象中大。
2.2 切块策略:同样一份文档,切成多大、怎么切,结果完全不同
切块是整个流程里最容易被低估的环节。切太大了,一个块里内容太多,向量化后语义被稀释,检索时会捞回来一堆"相关但没重点"的内容;切太小了,上下文不完整,LLM 拿到之后无法理解完整逻辑。
我在项目里的经验值:通用文档按 500 到 800 个 token 切片,重叠窗口设 50 到 100 token。为什么设重叠窗口?因为一句话的前半段可能在 A 块、后半段在 B 块,如果完全不重叠,检索时就会错过语义完整的边界。这一点实际操作中只能用固定 token 切分加滑动窗口实现,简单有效。
更好的切块方案是按 Markdown 标题层级做语义切块。如果一个二级标题下内容很多,就继续往下切;表格单独成一个块;代码块尽量完整保留。这样切出来的块天然具备语义边界,检索命中率明显比无脑按 token 切高。我用的是 LlamaIndex 的 MarkdownNodeParser 做了二次开发,核心逻辑就是给每块打上文档路径和标题锚点,检索结果能直接定位到原文档位置。
2.3 向量化模型选型:中文场景下别闭眼选国外模型
向量化模型决定了"语义相似度"算得准不准,选型时我踩过不少坑。第一版图省事直接用了 OpenAI 的 text-embedding-ada-002,效果在英文场景还可以,但对中文长文本的语义拟合明显弱,尤其专业术语多的技术文档,检索头几名经常偏离主题。
后来换成国产嵌入模型,实测下来 bge-large-zh-v1.5 和 bge-m3 的中文效果都有明显提升。bge-m3 的优势是支持 8192 token 的长文本,还能输出稠密向量和稀疏向量两种表示,特别适合做混合检索——稠密向量解决语义相似,稀疏向量解决精确词匹配,两者结合之后召回率提升显著。
如果你的场景偏向代码问答,可以试试基于代码语料预训练的代码嵌入模型。但我的结论是:通用知识库直接上 bge-m3 就够了,别在这里花太多时间调参,后续 Rerank 阶段更能拉高精度。
3. 核心组件选型:向量数据库与 LLM 接入的取舍
3.1 向量数据库选择:从 Chroma 到 Qdrant 的迁移理由
向量数据库是检索链路的核心存储引擎。第一版为了快,我直接选了 Chroma——它是嵌入式库,轻量、开箱即用,本地就能跑。但项目数据量到几十万条 Chunk 之后,Chroma 的客户端模式和过滤查询性能就顶不住了,尤其按文档目录做元数据过滤时,响应延迟明显上升。
后来迁到了 Qdrant。选它的理由很实际:支持 Rust 底层的高性能过滤查询,部署方式灵活,既能 Docker 单机跑,也能上集群;而且内置了 payload 索引,可以按文档 ID、标签、创建时间做过滤,这对"只搜索某个项目目录下的文档"这类需求很有用。如果你只需要单机跑几百 MB 的知识库,Chroma 也完全够用,没必要一上来就上 Qdrant 增加运维负担。
同时我把 pgvector 也作为备选保留着,因为它就在 PostgreSQL 里,适合团队原来就依赖 Postgres 的场景,少一个组件少一份运维。结论是:按数据量倒推选型,小项目用 Chroma,中等规模用 Qdrant,已有 PG 基础设施的优先 pgvector。
3.2 LLM 接入方式:云端 API、本地部署、还是走 Dify 中间层
llm_wiki 的生成层有两条路:直接调各家 LLM API,或者通过 Dify、FastGPT 这类 LLMOps 平台搭建应用。以我负责的团队场景,答案其实是"先用 Dify 跑通闭环,再决定是否自建 API 编排"。
Dify 的优势在于把知识库接入、检索流程、Prompt 编排、模型管理都界面化了,不需要从零写代码。它的工作流里有一个"知识库检索"节点,可以配置检索方式、Top-K、Score 阈值,再连一个"大模型"节点做生成。如果你还没法确定公司最终选哪家大模型供应商,Dify 里可以同时配置多家 API Key 动态切换,省掉了自己写适配层的活。
但 Dify 不适合所有场景。它的缺点在黑盒层面:检索内部细节暴露有限,复杂的多跳检索、混合检索策略调试不灵活,延迟也偏高。我实测过从提问到输出首 token 大概多出 300ms 以上的编排开销。所以最终生产环境如果要追求极致性能和定制化,建议把检索逻辑抽出来自己写 Python 服务,Dify 只做配置调研和原型验证。
3.3 检索参数细节:Top-K、Score 阈值和 Rerank,怎么调才算合理
检索阶段最容易出现的问题是"没召回"和"召回太杂"。对"没召回",第一反应别急着调阈值,先看向量化质量——是否切块不合理、嵌入模型是否适合领域语言。对"召回太杂",重点检查两件事:Top-K 是否设置得太大、Score 阈值是否设太低。
我的经验值供参考:Top-K 初始设为 5,如果回答引用了大量无关内容,降为 3;如果回答明显缺细节,升到 8。Score 阈值在 Qdrant 中一般设 0.2 到 0.5,这个范围取决于嵌入模型的距离度量,实践中要画一条"召回率-准确率"曲线来定。插一句,直接用固定阈值其实不太灵活,可以结合 Rerank 做二次筛选,先用较宽的 Top-K(比如 20)召回,再用 BGE-Reranker 对候选集重排,取前 3 作为最终上下文。Rerank 这一步对中文知识库的准确率提升非常明显,实测此前 76% 左右的精确率能拉到 88% 以上,值得投入。
4. 实操手记:用 Dify + Qdrant 跑通一个 llm_wiki 问答应用
4.1 环境准备:Docker Compose 一键拉起依赖
我习惯用 Docker Compose 做本地环境联调,整套依赖写在一个 compose 文件里,包括 Qdrant、Dify,以及后续要用的本地嵌入模型服务。这一步实际做的是把底层服务先跑起来,避免后面建知识库时才发现环境问题。
Dify 的官方仓库提供了 docker/docker-compose.yaml,基本能一键启动。Qdrant 我用镜像 qdrant/qdrant,挂载本地目录持久化数据。嵌入模型这块,如果用云端 API 就不用额外部署,如果选本地 bge-m3,我建议起一个独立的服务容器跑本地推理,别和 Dify 主服务混在一起,资源隔离更清晰。
4.2 创建知识库并上传首批文档
登录 Dify 后台,进入"知识库"页面,新建一个名为 llm_wiki 的知识库。这里要选索引方式,Dify 支持高质量模式和经济模式,前者会用嵌入模型做向量检索,后者是关键词索引。要做语义问答就选高质量模式,并在嵌入模型配置里填入 bge-m3 的 API Endpoint。
上传文档没什么特别的,支持 PDF、Markdown 等格式。真正要注意的是切块设置,Dify 默认切块参数偏保守,我在生产里一般把分块长度调到 600 左右、重叠 80,实际效果比默认值好。以下是可参考的切块配置示例:
chunk_size: 600 chunk_overlap: 80 delimiter: "\n## " # 按 Markdown 二级标题感知边界上传完成后 Dify 会自动完成文档分段和索引,可以在"文档分段"列表里检查切出来的块是否合理,比如代码块有没有被切碎、标题层级是否保留。
4.3 构建问答应用:从"知识库检索"到"LLM 生成"的编排
进入"应用"页面,新建一个 Chatbot 应用,模型供应商选你已经配置好的 LLM。这里我选择的是 OpenAI 兼容接口,Dify 可以直接把 base_url 指向你自己的网关。
核心步骤是编排 Prompt。我的 Prompt 模板思路如下:先定义角色,再限定回答来源,最后要求引用出处。一个简化示例:
你是一个基于知识库回答问题的助手。 规则: 1. 只根据"{{#context#}}"中的内容回答,不要使用你内部的既有知识补充。 2. 如果上下文无法回答用户问题,请直接说"根据当前知识库内容无法回答",不要编造。 3. 回答结尾列出引用来源文档的标题。 用户问题:{{#query#}}接着在"编排"面板加上"知识检索"节点,关联之前创建的 llm_wiki 知识库,检索方式选"语义检索",Top-K 设为 5,Rerank 模型选择已启用 BGE-Reranker 的供应商。完成后先发几条测试问题验证。
4.4 验证调优:如何判断"回答得好不好"
验证阶段别直接用肉眼判断"顺不顺",要建立一套简单的评估集。我的做法是挑 30 条高频真实问题,人工标注出每道题希望回答里涵盖的知识点关键词,然后跑一轮,看每条回答是否覆盖了这些关键词,再按"完全覆盖、部分覆盖、未覆盖"三档打分。一轮下来基本能看出是切块问题还是检索问题还是生成问题。
例如下面这是一个最简评估表格,你可以按这个格式自己维护:
| 问题 | 期望知识点 | 实际是否覆盖 | 主要问题 |
|---|---|---|---|
| 服务响应变慢如何排查 | 链路追踪、缓存、SQL慢查询 | 部分覆盖 | 检索到了缓存,但没有定位到 SQL 部分 |
| 节假日活动页部署步骤 | 分支、流水线、回滚步骤 | 完全覆盖 | 无 |
| 如何签署采购合同 | 法务流程、审批节点 | 未覆盖 | 知识库中根本没有该文档 |
有了评估集之后再调参才有依据,而不是靠感觉改 Top-K 或 Rerank 阈值。
5. 常见问题与排查技巧实录
5.1 检索不到任何相关内容,LLM 直接回答"I don't know"
这是 llm_wiki 上线初期最频繁的反馈。排查路径:先用 Dify 自带的知识库"召回测试"功能,输入同样的提问,看是否命中了内容;如果没命中,基本是文档没进索引,可到文档分段或索引状态确认。其次检查元数据过滤器——如果应用权限、知识库隔离导致检索范围被过滤太狠,也会出现空召回。我遇到过最隐蔽的问题是嵌入模型服务在本地部署时超时,向量一直写不进去,但 Dify 没报明显错误,只表现为召回为空,后来通过看 Qdrant 集合计数才发现是零向量。
5.2 回答内容看着合理,但引用的文档里根本没有相关内容
这种情况多半是幻觉,即 LLM 没严格遵循 Prompt 的"只根据上下文回答"规则。我的处理经验是把 Prompt 里的指令强化为两步:先生成"是否可回答"的判断,再生成回答。同时在 Dify 的模型参数里把 Temperature 调到 0.2 以下,减少生成随机性。另外,把 Rerank 阈值调高,可以避免低相关片段混进上下文。注意别把 Temperature 调到 0——有些模型在 0 时反而容易复读或短答,0.1 到 0.3 是比较稳的区间。
5.3 文档更新了,但问答还是旧内容
这是知识库时效性问题。Dify 的更新机制要求你手动在知识库里对变化的文档重新上传或同步,系统不会自动去盯源站变化。我做了个小脚本,每天定时扫描 Wiki 同步目录的最近改动,有变就自动调用 Dify 的知识库文档更新 API。如果你用飞书或类似 Wiki 系统,可以走 Webhook 触发更新。还有一个容易忽略的点:更新索引后要清一下 Redis 缓存,否则可能读到旧向量。
5.4 成本与性能如何平衡
LLM 生成是成本大头,RAG 场景里每次问答都要把上下文 Token 传给模型,上下文字数直接决定费用和延迟。降本的核心是控制检索片段总长度。K 设为 5 时,如果每块 600 token,光上下文就有 3000 token,这还没有算系统 Prompt。实际做法是让 Rerank 之后保留前 2 到 3 个块,再引导 LLM 输出精炼回答。响应延迟方面,首个 Token 时间主要取决于 LLM 供应商和服务地区,Qdrant 本地的检索通常在 50ms 内完成,这部分的优化空间已经很小;延时主要来自生成阶段。把 Rerank 模型部署在 GPU 上,可以大幅减少排序等待时间,这是我自己优化后收益最明显的一步。
最后再分享一个小技巧
整个 llm_wiki 项目做下来,我体会最深的一点是:这个系统的性能瓶颈不在某个爆款模型,而在那些不起眼的基础设定,切块大小、检索阈值、Prompt 约束,每一环都在悄悄影响着最终体验。所以你搭建的时候,建议不要急着堆功能,先用小规模知识库把链路打通,接着老老实实建一个评估集,再围绕评估结果去调参,这样的节奏比盲目加功能稳得多。
如果你已经有现成的 Wiki 或知识库文档,最快验证 llm_wiki 思路的方式,就是用 Dify 搭一个最小原型:上传几十篇最常用的文档,配一个 LLM,问几个业务里真实的问题。只要这条链路能跑通并让你觉得"比翻文档强",再继续往生产环境加能力也不迟。希望这篇手记能给你省下我们当初踩坑的时间。