去年年底我把自己攒了三年的技术笔记、项目文档和会议纪要全部丢进了一个自己写的系统里起名叫llm_wiki。折腾这个项目的初衷很简单资料越来越多但找得到变得越来越难。传统的 Wiki 依赖人工分类和维护标签笔记软件靠关键词搜索但大部分时候我根本记不住准确的关键词只记得当时好像看到过一篇讲向量数据库选型的里面有对比表格这种模糊需求关键词几乎无能为力。llm_wiki要解决的就是这一类问题——把自然语言问答直接架在个人/团队知识库上让 Wiki 从一个写给人看的仓库变成能直接回答问题的助手。这个项目面向的群体其实很广有大量文档需要管理的研发团队、做知识沉淀的运营团队、甚至只是个人笔记重度用户都能从中找到对应的设计思路。核心思路并不复杂就是当下很常见的 RAG 路线但真正把它做成一个可长期使用的系统很多细节比想象中麻烦。1. 传统知识库为什么越来越难用1.1 记得内容但不记得标题的检索困境传统 Wiki 的检索方式基本就三种全文关键词、目录浏览、标签筛选。当你需要找一段明确的技术方案时很好用但人类记忆的特点是场景化、语义化而不是关键词化。我问自己上次那个关于并发限流的方案是怎么设计的这句话里没有一个词能精确匹配到文档标题。关键词检索把并发限流方案拆开搜返回的结果是一堆相关性低下的碎片需要我手动翻好几页才能找到真正的内容。更麻烦的是团队 Wiki 的文档是流动的旧文档可能被废弃、合并、移动但引用它的链接和旧习惯还在。知识库越大信息架构越复杂维护成本指数级上升最终变成大家都往里扔东西、没人能找出来的电子垃圾场。1.2 人的精力不该花在整理标签上我观察过自己和同事维护知识库的行为模式一开始兴致勃勃给每篇文档打标签、建目录坚持两周后就开始乱丢半年后连自己都不愿意回看。这不是自制力问题是维护成本本身反人性。llm_wiki的一个重要设计目标是尽量让维护动作隐藏在流程里而不是变成额外的负担。用户只需要把文档放进去系统负责切分、向量化、关联分析、自动总结摘要用户搜索时直接问自然语言。标签和分类从强制输入变成系统自动生成的辅助索引维护成本降低一个量级知识库才有机会真正活下来。这是我认为基于 LLM 的 Wiki 最核心的价值——不是换了个花哨的搜索框而是把知识管理的摩擦成本压到接近零。2. 系统整体架构设计思路2.1 数据链路全貌llm_wiki的数据链路可以拆成四大块接入层、处理层、存储层、问答层。接入层负责接收各种来源的文档包括 Markdown 文件、PDF、Word、网页剪藏、Git 仓库里的 README 等。处理层做格式解析、清洗、分块、embedding 向量化、摘要生成。存储层分两部分结构化元数据放进 PostgreSQL向量数据放进专门的向量数据库两者通过文档 ID 关联。问答层接收用户问题走检索-重排-生成的 RAG 流程同时记录用户反馈回流到处理层。这个设计借鉴了数据仓库的分层思想每层职责单一、可独立替换。接入层出现问题不影响已入库的数据向量数据库要升级可以离线重建索引问答层的提示词调整不需要动存储结构。项目做到后期你会发现这种解耦是能持续迭代的前提。2.2 为什么向量数据库单独拆出来而不直接用现成的调研阶段对比过几种路线直接用 PostgreSQL 的 pgvector 扩展、用 Elasticsearch 带向量插件、用独立的向量数据库。最终选了独立向量库理由是 pgvector 在千万级向量以下的场景其实够用但llm_wiki的知识库有一个特点——文档更新频繁需要支持增量更新和实时删除向量。独立向量库在这些操作上的成熟度更高支持 filtered search先按元数据过滤再向量检索的性能也更好。实际使用中这个优势体现得很明显比如按团队或项目过滤知识范围时如果用 pgvector 硬做索引命中率会下降延迟明显变高。2.3 元数据设计决定了功能的边界这是一个很多人会忽略但极其重要的设计环节。每个文档块chunk的元数据我保留了这些字段doc_id文档唯一 ID关联原始文件source来源路径/URL回答时可以溯源author上次修改人用于权限控制updated_at最后修改时间影响检索权重tags自动生成的关键词标签section_hierarchy章节路径比如架构设计 存储层 向量数据库选型元数据不是存着好看的它在检索时承担两个关键作用过滤和加权。权限控制靠它时间衰减靠它甚至回答时附带的引用来源也是从元数据里提取的。我见过一些 RAG 项目把元数据设计成先凑合后期再补结果后面想加权限控制向量库里几千条向量过不了滤只能全量重建代价非常大。所以我的建议是第一个版本就把元数据字段设计完整哪怕暂时用不上。3. 文档处理的完整流程3.1 格式解析和清洗文档接入最容易踩的坑就是格式解析。Markdown 和纯文本还好PDF 里多栏排版、表格、页眉页脚会严重污染切片内容。我的经验是 PDF 解析优先用专门的服务而非通用库通用库在处理扫描版和复杂排版时召回率下降得厉害。清洗阶段做了几件事移除导航栏、页脚等重复信息规范代码块格式把表格转成列名: 值的键值描述方便 LLM 理解。这里有个细节表格直接转 Markdown 后在 embedding 时效果并不好转成自然语言的键值配对后检索质量明显提升。比如模型 | 维度 | 价格这种表头转成模型是 xxx向量维度是 xxx单价是 xxx这样的描述后语义检索才能正确匹配到哪个模型便宜这类问题。3.2 分块策略固定窗口还是语义切分这是 RAG 项目里争议最多、影响最直接的一个环节。我两种都试过最终线上跑的是混合策略正文先用段落结构做粗切再对超长段落做滑动窗口细切。一段代码def split_document(text, max_chunk_size600, overlap50): # 先在二级标题层面切分保留标题信息 sections re.split(r(?m)^#{2,3}\s, text) chunks [] for section in sections: # 保留所在章节的标题路径 section_title extract_first_line(section) paragraphs split_paragraphs(section) current_chunk for para in paragraphs: if len(current_chunk) len(para) max_chunk_size: current_chunk para \n else: if current_chunk: chunks.append({ text: current_chunk, section: section_title, source: source_path, doc_id: doc_id }) # 处理超长段落滑动窗口切分 if len(para) max_chunk_size: for i in range(0, len(para), max_chunk_size - overlap): chunks.append({ text: para[i:imax_chunk_size], section: section_title, source: source_path, doc_id: doc_id }) current_chunk if current_chunk: chunks.append({...}) return chunks核心参数是 chunk_size 和 overlap。我实测下来 500~800 token 是一个比较舒服的范围太小了上下文信息不完整太大了 embedding 会被无关信息稀释检索噪音明显上升。overlap 设 50~100 token确保跨切分边界的语义不会断掉。这个参数不是固定的不同领域的文档最优值不一样还是建议在自己的数据集上做 A/B 测试。3.3 标题信息必须保留做分块时最容易犯的错是把标题和正文分开处理只对正文做 embedding。这样会导致一个文档块丢失它在整个文档中的位置信息检索时出现答非所问的跳脱内容。我在 metadata 里专门加了section_hierarchy字段保留从一级标题到当前小节的完整路径。检索命中的时候即使块本身内容不够也能通过标题路径回传上下文。比如一篇文档的小节内容是embedding 模型对比表它的 hierarchy 是技术选型记录 向量检索方案 embedding 模型对比表这条路径本身就有很强的语义信息。问答时把它拼到提示词里模型能理解内容的语境回答准确率提升非常明显。4. RAG 检索链路的具体实现4.1 混合检索向量和关键词搭着用只用向量检索会漏掉精确匹配的场景。比如代码里的函数名、报错信息、配置项这些内容往往是纯粹的字符串匹配语义检索反而不如关键词。线上环境我做了双路召回一路走向量语义检索另一路用 BM25 做关键词检索最后合并取交集和并集。具体合并策略是两条链路的结果各取前 20 个去重后按分数加权排序。权重不是固定的用户查询里包含明显的关键词特征比如带引号、包含 API 名称时BM25 的权重自动调高。这个混合策略实测下来精确命中率比纯向量检索提升了大概 15%尤其在代码类文档上效果显著。代价是查询延迟增加了一点但相对于准确率的提升完全值得。4.2 重排环节是性价比最高的优化点向量召回返回的 Top 20 直接喂给 LLM 不是一个好主意上下文塞得太多反而干扰回答。重排rerank环节在倒序优化里性价比极高——用一个轻量级交叉编码器模型对 Top 20 候选重新打分取 Top 5 作为最终上下文。重排模型的效果远优于单纯依赖向量相似度原因是它能直接看到 query 和文档的完整交叉而双塔结构的向量模型很难捕捉到这种细粒度关系。选型上对比过 bge-reranker 和 cohere rerank最终在成本和效果间取了平衡用的 bge-reranker-base。延迟增加约 50ms但检索精度提升一个档次这个交换是划算的。4.3 提示词设计把不知道写进模板很多人设计 RAG 提示词时只关注怎么让模型利用上下文回答忽视了没有对应上下文时该怎么做。这是幻觉问题的一大根源——模型在知识库里没找到答案但因为提示词没有明确约束它会强行编造一个看似合理的答案。我的提示词模板中固定包含以下约束你是一个知识库助手。回答时只能基于给定的上下文内容不要使用你自己的预训练知识。如果上下文不足以回答问题直接回答知识库中没有找到相关内容并给出你检索到的相近主题建议。这看起来简单实际效果却非常显著。加了这句话之后知识库问答的幻觉率从大约 8% 降到了 2% 以下。另一个细节是要求模型在回答末尾附带引用来源格式类似参考来源[文档标题]章节路径。这样即使答错了用户也能快速回溯到原文核对。5. 自更新机制与质量保障5.1 增量更新而不是全量重建知识库是活的文档在持续新增和修改。初期我图省事每次更新都全量重跑一遍 embedding导致一个几千篇文档的库每次更新耗时十几分钟还占用大量 API 调用额度。后来改成了增量模式。核心实现是用一个sync_status表记录每篇文档的 hash 值和最后处理时间。每次扫描时对比 hash只有变化的文档才重新分块和向量化。删除操作也走同样的链路文档被移除时系统根据 doc_id 批量删除所有对应的向量记录。这个更新策略上线后日常维护成本几乎可以忽略。5.2 用户反馈回流的闭环llm_wiki的问答界面做了三态反馈有用 / 没用 / 不准确。用户点击不准确后系统会把当时的 query、答桅、引用的文档块存到反馈表。运营人员在后台可以批量查看这些失效案例快速定位是检索问题还是文档缺失。这块数据的价值在于它是持续优化 RAG 链路的真实样本。我每个月会抽一批反馈数据做错误分析找出共性问题比如某个领域的文档频繁被问但没有命中说明该领域的文档内容不完整或分块策略不适用。基于这个信号可以去补充文档或者调整该领域的检索参数形成良性循环。5.3 上下文冲突怎么办一个长期维护的 Wiki 一定会出现同一主题多份文档内容矛盾的情况比如技术方案 A 的文档说采用 Redis 做缓存后来方案 B 的文档说改用内存缓存两篇文档同时存在于知识库中。如果不做处理检索系统可能同时召回两篇矛盾文档LLM 就会给出混乱的回答。目前采用两个手段缓解一是文档的updated_at参与重排加权新文档的权重高于旧文档二是定期离线跑一遍相似度聚类把内容高度重合的文档找出来推送提醒做内容合并。第二个手段是半自动的会有专门的审核页面供人工确认。6. 实测效果与踩坑记录6.1 客观评估方法RAG 系统的评估不能只靠主观感受。我在库上标注了一批黄金问题集大约 200 条每一条都对应知识库中一篇具体的答案文档。每次改动检索链路后用这批问题跑一遍回归记录命中率和首答案准确率。这个评估集不参与调参独立维护。测试数据供参考知识库规模约 3000 篇文档、8 万个文档块问答过滤后返回首答案准确率约 86%包含参考答案在 Top 3 推理结果中的覆盖率约 94%。单轮问答的平均延迟约 1.8 秒包含重排和 LLM 生成。6.2 三个最隐蔽的坑第一个坑embedding 模型和查询词长度不一致。我们最初用的 embedding 模型做了 max_length 截断长文档没影响但用户问题超过截断长度时语义信息会被切掉导致检索结果质量下降。后来在接入层加了查询文本长度检测超长时先自动分词截取核心部分。第二个坑向量更新的时序问题。增量更新是异步的文档已经改版但向量还没更新完成时检索返回的是旧内容。这个问题的隐蔽之处在于它不是必现的只在文档更新后的几十秒内出现。解决方案是在检索接口加了一个版本号小于查询时间戳的过滤条件。第三个坑元数据里的时间戳参与向量化。一开始我把 updated_at 拼到了 embedding 文本里想着让模型感知时间。结果发现同一份文档在不同时间生成两个不同的向量重复文档聚类的功能被彻底搞坏了。正确的做法是时间戳只存在元数据里用于过滤和排序永远不进 embedding 文本。6.3 基础设施层面的教训向量数据库在 Docker 环境里的持久化问题坑过一次——容器重启后向量索引损坏只能全量重建。后来强制把存储目录挂载到宿主机并定期做快照。还有一次是嵌入式的批量 API 调用没做限速被限流后部分文档的向量缺失回答时某些主题一直说没有找到相关内容排查了很久才发现是入库环节的问题。这类问题在联调阶段极难发现建议从一开始就在处理层加完整的重试、报警和自动化校验。7. 可能的扩展方向7.1 多租户与细粒度权限个人使用和团队使用的需求差距主要在权限模型上。团队场景下A 部门的知识不应该出现在 B 部门的搜索结果里但公共知识需要共享。基于元数据过滤可以在检索层实现这个能力难点在于权限继承关系的维护需要有一个类似于目录 - 空间 - 团队的层级模型。目前是在权限模块采用拦截器方式在每个用户的检索请求上自动附加其可见范围的条件从根上避免越权数据返回。7.2 多模态内容接入技术 Wiki 里大量内容是截图、架构图、白板照片纯文本检索无法覆盖这部分。后续计划引入图像理解模型对图片自动生成描述文本以伪文本的形式入库参与检索。这样用户问登录时序图里 token 刷新还走不走统一认证就能通过图片描述间接检索到架构图。7.3 与外部工具的打通最后打算做的是把llm_wiki的问答能力以 Webhook 形式暴露出去让飞书/企业微信群里的 bot 直接调用知识库回答问题。很多团队的高频问题其实都是重复的接入 IM bot 后能显著减少打扰也能让知识库的使用频率大幅提升带动大家的贡献意愿。回到最初的问题llm_wiki和传统 Wiki 最大的不同是它不要求人去适应信息架构而是让系统主动理解人的表达习惯。这套系统目前已经跑了将近半年检索质量基本稳定最让我满意的其实是它退化成了一个非常安静的工具——日常使用时你几乎感觉不到它的存在只有在需要某个冷门知识的时候问一句就能拿到答案这种感觉和以前翻半天文档找不到的痛苦相比体验完全是另一个量级。如果你也在头疼知识库的检索问题建议先从最小可用的 RAG 链路开始搭建元数据设计一步到位分块参数后面再慢慢调。不要一开始就追求复杂的自动化流程系统先能用起来再逐步演化成适合自己和团队协作习惯的形态这条路是可以走通的。