最近这两周我一直在折腾本地私有 RAG 的搭建,从架构选型到把一条完整的"文档入库 → 切块 → 向量化 → 检索 → 问答"链路跑通,中间推翻过两版方案,也踩了不少坑。现在趁着记忆还热乎,把整个思维护盘复盘出来,既是对自己踩坑过程的整理,也能给打算做本地 RAG 的朋友一份可参考的路线图。
这篇文章是系列的第一篇(01),重点覆盖"从零到一"阶段最关键的内容:为什么选本地私有 RAG、技术栈怎么选、文档解析和切块怎么做、检索链路怎么搭、第一版效果长什么样。至于更进阶的调优、多轮对话、Agent 化这些话题,我会在后续几篇里单独展开。
写之前先交代一下需求背景,方便不同场景的朋友对照参考。我所在的团队长期积累了大量内部文档,包括产品需求文档、技术设计文档、会议纪要和各类 FAQ,总量几千份,散落在不同的 Wiki 和本地目录里。团队日常高频诉求是"帮我查一下 X 功能的现状""XX 方案的决策背景是什么"。这类问题用传统关键词搜索很难回答,因为答案往往分散在多份文档里,需要语义层面的理解。另一方面,企业数据不能直接传到公有云,所以在我的场景里,"本地部署 + 私有化"不是偏好,而是硬性条件。
1. 为什么最终选择本地私有 RAG:三个决策关键点
1.1 数据隐私是硬约束,不是偏好
说实话,在真正动手之前,我曾经纠结过这个问题:直接用在线的大模型知识库工具,效果又好又省事,为什么非要自己本地部署?答案归结起来就三个字:不敢传。
我们手里的文档包含大量内部产品路线图、未公开的功能设计、客户反馈原始数据。这些内容如果交给公有云上的 RAG 服务,相当于把核心资产放到了别人的机房里。很多服务商确实会声明"数据不用于训练""传输过程加密",但"声明"和"可审计的证据"之间差着十万八千里。企业合规部门对数据出域的审查越来越严格,与其每次要走的审批流程拖两周,不如在源头就把数据留在本地。
这个约束直接决定了后面所有的技术选型:嵌入模型必须能在本地跑,向量数据库必须本地部署,大模型推理也必须本地部署。任何依赖外部 API 的方案,不管演示效果多惊艳,在第一轮评审里就被我直接划掉了。如果你是个人开发者、文档不敏感,那完全可以不用走这条路,本地部署的运维成本并不低;但一旦数据安全成为刚需,这条路就是唯一解。
1.2 算清成本账:按 Token 付费与本地部署的真实差距
把成本摊开算一下会更有体感。假设一个 20 人左右的团队,每月产生 20 万次文档问答请求,平均每次请求消耗的上下文 Token 在 4000 左右,按目前主流大模型 API 的价格折算,仅 Token 费用一个月就在数千元级别,这还没算向量化、重排这些附加环节的调用量,一年下来就是好几万。
本地部署的硬件成本则是一次性的。一台带 24GB 显存显卡的工作站,加上存储和电费,第一年的总投入大概和云 API 一年的费用相当,但从第二年开始就是纯节省。更关键的是,本地推理没有按量计费的心理压力,测试和迭代时你可以毫无顾忌地反复跑实验,这种"边际成本为零"的体验,对方案调优的帮助是巨大的——它会让你更愿意尝试不同的切块策略、检索参数,而不是每次都心疼钱。
当然,本地部署有它的隐性成本,最大的隐性成本是运维:模型更新、依赖环境维护、硬件故障,都需要有人负责。我个人的建议是:如果团队规模很小、文档量不大、数据不敏感,直接用云 API 最划算;但如果数据敏感、调用量大,或者你打算长期在这套系统上做功能迭代,本地部署几乎必然是最终归宿。
1.3 可控性与可定制性:长期维度上的真正价值
成本只是其中一个维度,真正让我坚定选择本地路线的,是可控性。自建之后你会发现,RAG 的效果好坏其实是一个"全链路工程问题":从文档解析到切块粒度,从嵌入模型到检索方式,从重排策略到 Prompt 模板,每个环节都有大量可以调节的旋钮。第三方服务把这一切封装成黑盒,省心是省心,但一旦检索效果不达标,你只能干瞪眼,既看不到中间结果,也没有任何干预手段。
本地部署的另一个隐藏好处是数据资产沉淀。你可以用内部文档微调嵌入模型,可以统计哪些文档被高频命中,可以追踪用户问题的分布规律。这些东西在第三方平台上要么拿不到,要么导不出来。数据在自己手里,才有持续优化的可能性。
2. 技术栈全解:从嵌入模型到向量库的选型思路
2.1 一条主链路看懂 RAG 的完整工作流
第一次接触 RAG 的朋友,最容易被各种术语绕晕。我习惯用一条主链路来理解它,总共五个环节:
离线部分:
- 文档解析:把 PDF、Word、Markdown 等各种格式转换成纯文本,并保留必要的结构信息。
- 切块(Chunking):把长文档切成大小合适的文本片段,这是后续检索精度的基础。
- 向量化(Embedding):把文本片段编码成高维向量,让语义相似的文本在向量空间里距离更近。
- 入库:把向量、原文和元数据一起写入向量数据库,建立可查询的索引。
在线部分:
- 检索与生成:用户提问 → 问题向量化 → 相似度检索 →(可选重排)→ 拼装 Prompt → 大模型生成回答。
记住这条链路,后面所有技术选型本质上都是在回答"每个环节用什么工具"这一个问题。下面我按环节逐个说。
2.2 嵌入模型选型:决定语义理解上限的底座
嵌入模型是整个系统的语义底座,它决定了"相似"的定义。对中文场景来说,我优先考虑的是中英双语模型,因为企业内部文档几乎必然是中英混合的,产品名词、技术术语往往直接保留英文原文。
我当时重点对比了三个方向:
| 模型 | 特点 | 适合场景 | 备注 |
|---|---|---|---|
| BGE 系列(bge-m3) | 支持中英等多种语言,1024 维,支持稠密+稀疏联合检索 | 中文 RAG 的主流选择,综合能力最强 | 最终选择 |
| text2vec 系列 | 轻量、体积小 | 纯中文场景 | 多语言能力较弱 |
| m3e 系列 | 效果均衡,历史流行 | 中小规模知识库 | 维护活跃度一般 |
最终我选了 bge-m3。一个重要原因是它支持"稠密检索 + 稀疏检索"的混合方式,可以在不额外引入 BM25 的情况下获得更好的召回效果,这个点后面检索链路部分会详细展开。
顺带提醒一句:嵌入模型不是越大越好,而是越匹配越好。它需要跟你的切块粒度配合。如果文档切得比较小(200-300 字),一个 300M 左右的中型模型完全够用;如果追求超大块(1000 字以上),再考虑更大的模型。盲目追求大模型只会拖慢索引速度,对精度提升并不明显。
2.3 向量数据库选型:先跑通再追求规模
向量数据库的选型直接决定了你要承担多少运维成本。我按"上手难度"排序,对比过几个主流方案:
- Chroma:纯 Python 实现的轻量级向量库,一条 pip 命令装完,数据存在本地目录,API 极简。个人项目和小团队首选,缺点是数据量到百万级向量、或者并发高的时候性能不足。
- Qdrant:Rust 写的,性能好,有官方 Docker 部署方案,API 也比较简洁,是中等规模项目的合理选择。
- Milvus:功能最全、规模最大,但组件多(依赖 Etcd、MinIO 等),部署和维护成本高,适合千万级向量和分布式场景,个人折腾属于杀鸡用牛刀。
- pgvector:如果团队已经有 PostgreSQL,直接在现有库里加扩展是最省事的方案,还能跟业务数据做 SQL 联查,缺点是向量索引的性能优化需要自己多花心思。
我的选择是先用 Chroma 起步,原因很简单:第一版的目标是跑通链路,而不是追求极致性能。等文档量真正涨到十万级以上,再平滑迁移到 Qdrant 也不迟。技术选型最忌讳"一步到位"的完美主义,先用最简方案拿到结果,再根据实际瓶颈做演进,这个节奏才是务实的。
2.4 本地 LLM 推理方案:从 Ollama 开始
本地大模型推理这一层,按部署难度可以列几个选项:
Ollama是目前个人本地推理的事实标准。一条命令就能把模型拉下来运行,自动管理模型权重和依赖,还提供跟 OpenAI 兼容的 API 接口,应用层接入非常方便。我强烈建议从它开始。
llama.cpp是底层的 C++ 推理引擎,效率极高,支持各种量化格式的模型,但对新手不太友好,需要手动编译和配置。vLLM吞吐量极高,适合高并发生产环境,但显存占用大、配置复杂。LM Studio提供图形界面,适合完全不想碰命令行的场景。
考虑到我们的场景是"私有知识库问答",并发量不会特别夸张,用 Ollama 最合适。我用的模型是 Qwen2.5 系列的 7B 版本,量化到 Q4 后大约占用 5GB 显存,在 24GB 显存的机器上可以同时跑嵌入模型和推理模型,互不干扰。量化是一个关键操作:把模型从 FP16 压缩到 Q4,体积缩小 75%,精度损失在日常问答任务上几乎感知不到,显存占用却大幅下降。
2.5 编排框架:第一版建议先手写
最后一个选型问题是:要不要用 LangChain 这类编排框架?我的答案可能有点反主流:第一版不要用,至少不要依赖框架的"高级特性"。
LangChain 的优点是把很多组件封装好了,开发者可以快速拼出一条链路;缺点是抽象层太多,出了问题你很难定位是哪个环节出了问题。第一版建议自己用几十行 Python 把链路写出来,每一步的输入输出都清清楚楚,这对理解 RAG 的原理大有帮助。等链路吃透了,再判断是否需要引入框架来简化开发,或者干脆保持手写。
这里顺带说一个很多人在困惑的问题:RAG 和 MCP(模型上下文协议)的区别。简单理解,RAG 解决的是"从你的私域文档里检索信息"的问题,MCP 解决的是"让模型调用外部工具和数据源"这个更大范围的标准协议问题。两者不是二选一的关系,RAG 完全可以作为 MCP 服务中的一个工具暴露出去。我在跑通第一版之后才意识到这一层,先不要混在一起,否则概念负担太重,容易把自己绕晕。
3. 文档解析与切块策略:决定检索效果的第一道关卡
3.1 文档解析:第一个被低估的拦路虎
大多数人做 RAG 的第一个反应是直接上切块,但现实会教育你:文档解析才是第一个拦路虎。我们几千份文档里有 PDF、Word、Markdown,还有不少扫描件,格式五花八门。
先说结论:不同文件格式要区别对待,不能用一套代码通吃。
- Markdown / txt:直接读文本,保留标题层级信息。这是最省心的格式,也是后续切块质量最高的来源。
- Word(.docx):用 python-docx 把段落和表格提取出来。表格内容要特别注意——Word 表格如果被当成纯文本按顺序读出来,语义就乱了,最好把表格转成 Markdown 表格格式再接回正文。
- PDF:这是重灾区。文本型 PDF 可以用 PyMuPDF 或 pdfplumber 提取,按页面组织文本;但扫描型 PDF 必须走 OCR。我用的 OCR 方案是 PaddleOCR,对中文的支持很成熟。OCR 的代价是速度慢且可能出错,所以只要能拿到原始文档,尽量不要依赖扫描件。
一个容易被忽略的细节:解析后的文本要处理页眉页脚、页码信息。否则每个切块里都会混入"第 X 页""公司内部资料"这类噪声,直接污染 embedding 的质量。这些看似不起眼的清理工作,其实对最终效果的贡献比很多"高级调优"都大。
3.2 切块策略:没有银弹,只有反复试
切块是整个 RAG 中"玄学"最多的环节。核心矛盾在于:块太大,语义包含得多,但检索时噪声大、命中不精准,还会占用大量上下文窗口;块太小,检索粒度是细了,但上下文容易丢失,模型回答问题看不到完整脉络。
我第一版实验了三种策略:
- 固定字数切块:比如按 500 字切,重叠 50 字。实现简单,但对文档结构无感知,容易把列表、表格、代码块切得七零八落,是效果最不稳定的一种。
- 按段落切块:以 Markdown 标题和空行作为天然边界。对结构良好的文档效果极佳,但遇到没有结构的文档就退化成固定字块。
- 父子块(Parent-Child Chunking):先把文档按大块(比如 2000 字)切好作为"父块",再在大块内切 300 字的"子块"用于检索。命中子块后,把整个父块喂给模型。这个方案兼顾了检索精度和上下文完整性,是我最终采用的主策略。
切块重叠(overlap)也是一个细节。相邻块之间加 20-50 字的重叠,可以避免一个完整句子的语义被硬生生切断——中文里一个完整观点跨块分布的情况非常常见。重叠比例一般控制在 10% 左右,不用贪多,否则重复内容太多会降低索引效率。
3.3 元数据设计:让检索从"盲人摸象"到"精准定位"
如果说切块决定了检索的"准",那元数据就决定了检索的"稳"。知识库里既有产品文档,又有技术文档,还有会议纪要时,在向量相似度之外加一层元数据过滤,往往是效果提升的最大杠杆。
我给每个块设计的元数据包括:文档类型(产品/技术/会议/FAQ)、所属项目、文档标题、版本号、更新时间、源文件路径。这样在查询时可以指定"只看技术文档""只看某项目相关"或"只看今年 3 月之后的文档",直接砍掉大量无关候选,召回质量自然就上去了。
更进阶的做法是把文档层级信息也存进去。比如一个章节的标题是"某功能的权限设计",那么该章节下的所有子块都继承这个标题,形成一条路径元数据。当检索命中任何一个子块时,模型都能知道它在文档结构中的位置,回答起来会更有"上下文感",而不是从半截话里硬猜。
4. 检索链路搭建:向量召回与重排的实践细节
4.1 向量化实践:几个直接影响效果的小细节
向量化这步本身不复杂,用 bge-m3 的 SentenceTransformer 接口,几行代码就能把文本批量转成向量。但有几个实践细节值得专门说一下。
第一个是归一化。向量入库前一定要做 L2 归一化,这样计算余弦相似度时可以直接用点积,性能高得多。bge-m3 的输出本身已经是归一化向量,但如果你换其他模型,记得手动补一步。
第二个是查询指令(query instruction)。BGE 系列模型有个使用要求:对查询文本需要加一个固定的指令前缀,比如 bge-m3 的查询侧指令是"为这个句子生成表示以用于检索相关文章"。加不加这句话,召回效果差异很大。很多人漏掉这一步,然后抱怨模型不好用,其实是用错了姿势。这一点在官方模型卡片里写得很清楚,但实操中发现没几个人认真看。
4.2 向量库写入与查询:两个关键决策点
用 Chroma 的话,写入端基本就是 create_collection 之后 add 一批带 id、embedding、document、metadata 的数据。查询端核心是 query 方法,传 query_embeddings 和 n_results。具体的 API 代码网上文档很全,框架版本更新也快,我就不在这里贴大段代码了,重点说两个决策点。
第一个决策点是检索数量 n_results 一开始要设大一点。Top-3、Top-5 的效果和待选池大小直接相关。我习惯先查 20 条,再交给重排模型精选,而不是一上来只查 5 条。召回(Recall)和精排(Precision)本来就是两个阶段的事,不要混在一个步骤里解决。
第二个决策点是相似度阈值要通过实测确定。Chroma 默认返回的是距离分数,不是相似度。你需要根据实际数据统计出"多少分以上才是有效命中"。这个阈值千万别拍脑袋定,我见过有人设 0.7 结果大量垃圾结果也通过了,也有人设 0.9 结果什么都搜不到。正确的做法是拿一批已知正确的问题去测试,画出分数分布,找一个能把"有效命中"和"无关干扰"区分开的分界点。
4.3 从纯向量到混合检索:语义相似不等于关键词命中
纯向量检索最大的问题是什么?是"语义相似 ≠ 关键词精确匹配"。比如你问"内存不足怎么解决",文档里写的是"OOM(内存溢出)的处理办法",向量检索大概率能找到;但你问"error 1234 是什么意思",文档里如果写的是"Error 1234: connection refused",向量检索反而可能匹配不到,因为错误码这种字符串的语义信息太稀疏了,向量空间里找不到可以依靠的语义支撑。这种情况,必须靠关键词匹配来兜底。
目前有两个成熟做法:
- 引入BM25 稀疏检索,与向量检索做加权融合,融合算法用 RRF(Reciprocal Rank Fusion)最常见。
- 用支持"稠密 + 稀疏"混合的嵌入模型,bge-m3 本身就支持输出稀疏向量,可以少维护一套 BM25 索引。
我推荐直接走 bge-m3 的稀疏向量路线,少一套组件就少一个故障点。融合权重建议从稠密 0.7、稀疏 0.3 起步,再根据实验结果调整。混合检索加上之后,那些"问法跟原文写法完全不同"的查询,召回率提升非常明显。
4.4 重排:投入产出比最高的一环
重排(Rerank)是我这一轮调优里觉得投入产出比最高的一环。向量检索看重的是"大方向上的相似",经常出现"看起来相关、实际上答非所问"的候选。重排模型会用更精细的交互式匹配,重新计算每一条候选与问题的相关度,效果通常立竿见影。
本地可用的重排模型,我推荐 bge-reranker-v2-m3,中文支持好,用 FlagEmbedding 库就能加载。重排的逻辑不复杂:把用户问题和候选块组成 pair,模型输出相关度分数,按分数排序取前 5 条。
注意重排是对"候选列表"重新排序,所以它必须放在向量检索之后、拼装 Prompt 之前。重排模型是一个独立进程,显存占用也不大。加了重排之后,我第一版实测的答案准确率提升了大概 15-20 个百分点,这是全链路里最惊喜的一次调优——如果你目前只做纯向量检索且效果不理想,优先加重排,性价比远高于换更大的 LLM。
5. 从召回结果到问答输出:提示模板与引用溯源的设计
5.1 Prompt 设计的核心:把模型摁在检索结果里
检索做得再好,最后一步如果 Prompt 没写好,效果照样崩盘。我的 Prompt 模板核心就三条约束:
第一,明确告诉模型"只依据提供的参考资料回答,不要使用内部知识"。这是防止幻觉的第一道防线。大模型被问到自己"知道"的东西时,很容易自作主张往外倒知识,必须用指令把它摁在检索结果范围内。
第二,要求"如果资料不足以回答,直接说不知道,并列出缺少的信息"。这一步看起来有点笨,但对企业场景极其重要。团队要的是可信的答案,宁可答不上来,也不能误导决策。
第三,指定输出格式。比如要求先给结论,再附依据和来源。这样后端可以直接解析结果,前端渲染也统一。
一个我在实践中改进过的细节:把命中的多个文本块编号(如 [1][2][3]),Prompt 里要求模型在陈述某个观点时标注对应的编号。这就是下一步引用溯源的基础。
一个简化版的系统提示词示意:
你是一个企业知识库问答助手。请只根据以下参考资料回答用户问题。 要求: 1. 如果参考资料中没有相关信息,请明确回答"资料中未找到",并列出你搜索的范围。 2. 回答必须引用参考资料编号,格式为[序号]。 3. 给出结论后,可以补充相关背景,但同样需要带引用。 参考资料: [1] 低代码平台权限设计_v2.3.docx [2] 2024年Q3技术评审会议纪要.md 用户问题:{query}5.2 引用溯源:企业场景的必选项,不是加分项
引用溯源在企业场景里不是加分项,是必选项。用户看到一条 AI 回答,第一反应一定是"这结论哪来的,靠谱吗"。没有引用来源,再多的解释都是空的。
实现引用其实不复杂:检索环节把命中的文本块连同它的元数据(文档标题、路径、页码)一起返回;Prompt 阶段给每个块编号;模型回答时带上编号;后处理阶段把编号映射成具体的文档链接或路径。前端展示时,每个结论旁边挂一个"来源"跳转,用户一点就能看到原文出处。
这个能力还有一个额外好处:当你发现某个回答质量差时,可以通过引用溯源直接定位是哪个源文档、哪一段切块提供了错误信息。这比对着整库瞎猜高效得多,也是后续持续优化的重要抓手。
5.3 第一版效果实测:超出预期与翻车的地方
链路全部打通后,我拿 20 个高频业务问题做了第一轮评估,结果大致可以分三档:
- 完全正确:11 个,主要集中在"某功能怎么配置""某字段含义是什么"这类有明确文档对应的问题。
- 部分正确:6 个,多数是结论对但细节不全,典型的例子是问题答案跨了多份文档,只检索到其中一部分,导致回答缺少后半段。
- 明显错误:3 个。其中两个是源文档本身信息已经过期,模型忠实引用了过时内容;一个是切块把表格拆散,导致语义丢失。
这个结果在我的预期之内。它说明链路基本通了,剩下的问题不在链路本身,而在数据和切块的优化——这也正是系列第 02 篇要重点展开的内容。不过有三个立竿见影的优化我当时就做了:给元数据加入版本过滤、上调重排阈值、对表格类文档单独走"块内完整保留"策略。
6. 踩坑实录:从 0 到 1 过程中最痛的五个问题
6.1 OOM:本地部署的第一杀手
第一次跑全量索引时,我直接内存爆了。几千份文档解析后一股脑塞进内存做 embedding,进程中途被杀。原因很蠢:一次性把所有文档都加载进来了。
正确的做法是流式处理:一批一批地读文档、切块、向量化、入库,每处理完一批就释放内存。批次大小我最后定在 50 份文档左右。另外一个经验是 embedding 也要分批,不要一次性喂几千个句子给模型,不仅内存顶不住,速度也不会更快,反而会因为内存交换拖慢整个流程。
6.2 中文编码与路径问题
第二个高频踩坑点是编码。Python 读取文档时,如果源文件是 GBK 编码(老旧的 Word 导出文件很常见),直接用 UTF-8 读会抛异常或者读出一堆乱码。处理方式很简单:读取时做编码探测,用 chardet 判断后,再用对应编码解析。还有一个容易忽略的小坑是 Windows 环境下文件路径带中文的问题,路径拼接时记得用 pathlib 而不是手工字符串拼接,否则各种转义问题会让人抓狂。
6.3 表格与扫描件:单独处理,别混为一谈
表格问题我再展开一下。把一张多行多列表格直接按行读成纯文本,会丢掉列与列之间的对应关系。测试中发现,对"字段名-含义-取值枚举"这种常见表格,先转换成 Markdown 表格,再以表格为单位切块(不夹杂其他正文),检索准确率提升非常明显。
扫描件 OCR 同理。如果源文档本身质量差,OCR 出来的文本错字连篇,这部分内容建议单独打标,设置一个"质量低"的元数据标记,不要跟高质量文本混在一起进向量库,否则会拉低整体检索质量。后期可以对低质量数据做专门的修正流程,而不是让它们拖累全局。
6.4 切块参数要按文档类型动态调整
我后来把文档按类型配置了不同的切块参数:结构化良好的 Markdown 文档按标题层级切,块大小控制在 300-500 字;PDF 文档按段落切,块大小 500 字加 50 字重叠;表格文档单独走表格级保留策略。一开始图省事,用一套固定参数跑所有文档,结果部分类型的召回率惨不忍睹。
这个教训让我意识到:切块参数不是全局变量,而应该是元数据驱动的、按文档类型动态选择的策略。与其花大量时间调一套万能参数,不如把文档分好类,为每类分别调参,效果稳定得多。
6.5 全量索引速度:增量更新比什么都重要
几千份文档首次索引,纯 CPU 跑嵌入模型花了接近两个小时。这个速度对一次性初始化可以接受,但每次修改切块策略都要全量重跑,那就非常痛苦了。我的解决方式是在向量库里加上文档级来源标记,重新索引时只删除并重建对应来源的向量,而不是清库重来。另外一个加速技巧是用 GPU 跑 embedding,速度能提升 5-10 倍。如果你的机器有显卡,这一步值得优先做——它会让你后续调整策略时的迭代效率完全不一样。
最后分享一个我在这个阶段最大的体会:搭建本地私有 RAG,真正的难点从来不在某个单一技术点上,而在"全链路思维"。每当你觉得效果不好,不要急着怀疑大模型不够聪明,先按链路逐段排查——是文档没解析对、切块把语义切断了、还是检索召回不足、或是 Prompt 没有约束好。用这种分段定位的方式去调优,大部分问题都能找到清晰的根因。
这一篇就先写到这里。第 02 篇我会重点展开三个方向:检索评估体系的搭建(怎么建立一套可量化的评测集)、多轮对话下 RAG 的上下文管理、以及把 RAG 能力封装成 Agent 工具时需要注意的问题。如果你也在搭本地 RAG,欢迎在评论区聊聊你踩过最深的坑。