最近在做企业内部的知识库查询系统,本来打算直接把几千页产品手册和技术文档喂给大模型做微调,结果数据准备到一半就发现不对劲。标注成本实在太高了,文档每周还在更新,总不能每周都重跑训练。后来把方案切换成 RAG 知识库路线,也就是检索增强生成(Retrieval-Augmented Generation),用户提问时先从文档库里检索相关片段,再让大模型基于这些片段组织答案。整套系统从选型、切分、向量化到检索调优,前后迭代了三周左右,效果提升非常明显。这篇文章把我整个实战过程、关键参数和踩过的坑完整记录下来,给正准备做知识问答、企业内部文档检索、客服助手这类场景的朋友一个可以直接复用的参考方案。
1. 项目概述与核心需求解析
1.1 为什么选 RAG 而不是微调
当时团队内部有两种路线争执了很久。一种路线是微调开源大模型,把几千页文档整理成“问题-答案”对,让模型学会回答固定格式的内容。另一种就是RAG,把文档切成小块,存入向量数据库,查询时先检索相关内容再让大模型阅读后作答。
从结果来看,微调在这个场景下有几个很难绕开的硬伤。首先是语料标注成本,一份设备操作手册要拆成几百个标准问答对,没有业务专家参与根本做不准。其次是更新频率,我们的技术规范每个月都有版本变更,每次微调都需要准备新语料、重跑训练、做回归测试,整个链路太重。最后是幻觉问题——微调模型本质上是把知识压缩进参数里,遇到训练数据覆盖不到的新版本内容,模型很容易一本正经地编一个答案出来。
RAG 的核心思路其实很好理解:既然大模型知识有限,那就给它一个“外挂资料库”。每次问答前先去检索一段最相关的原文,把原文作为参考资料塞进提示词里,让模型扮演一个“读资料回答问题”的角色。知识不存在于模型参数里,而是存在于可随时替换的文档库里。文档更新了,直接更新向量库里的内容就行,完全不需要重新训练模型。
从成本角度算一笔账也很直观。微调一次商用模型 API 的训练费用先不说,光是准备数据的人力消耗就是几周的活。RAG 方案只需要做一次文档清洗和向量化,之后每次更新文档就是增量式重跑一遍向量化,单次耗时以分钟计算。当然这不是说微调没用,如果场景是让模型学习固定的输出风格、公文格式、专业术语习惯,微调依然有它的价值。但如果是“根据已有资料回答问题”,RAG 是性价比高出很多的方案。
1.2 RAG 知识库的完整处理链路
一个标准 RAG 请求从用户提问到生成回答,中间大概要经过八个环节。我把每个环节的关键输入输出和容易出问题的地方整理了一下:
| 环节 | 核心输入 | 核心输出 | 常见坑点 |
|---|---|---|---|
| 查询理解 | 用户原始问题 | 改写后的标准查询 | 口语化问题直接拿去检索容易歪 |
| 向量化检索 | 标准查询文本 | 候选文档片段 TopK | 向量模型选不好召回率很低 |
| 关键词检索 | 标准查询文本 | 候选文档片段 TopK | 精确型号查不到 |
| 候选融合 | 两路候选结果 | 合并去重后的候选集 | 重复内容淹没有效片段 |
| 重排序 | 候选集+查询 | 相关性最高的片段 | 不重排时噪声很大 |
| 提示词组装 | 相关片段+来源引用 | 完整提示词 | 上下文超长或格式混乱 |
| 大模型生成 | 提示词 | 回答文本 | 模型不遵循约束会瞎编 |
| 日志反馈 | 问答对 | 效果评估数据 | 没记录就不知道哪里要优化 |
链路里每一步都可能影响最终效果。之前网上很多教程讲 RAG 就讲到“存入向量库、检索、回答”三步,实际跑起来却发现效果远不如预期。原因往往就出在查询理解、候选融合和重排序这几个容易被忽略的中间步骤上。
2. 环境准备与工具选型
2.1 向量数据库选型与场景匹配
向量数据库是整个 RAG 系统的核心依赖,选错了后面迁移成本很高。我按照项目体量把候选方案分成了三类:
第一类是轻量级嵌入式向量库,适合原型验证、个人项目和单机小规模场景。它的特点是部署零成本,直接在本地跑一个库文件,数据集量级在几十万条向量以内问题不大。我们刚开始做 Demo 用的就是这一类,好处是上手快,坏处是并发能力和数据管理能力比较弱,不适合多人同时使用。
第二类是单机版向量数据库服务,适合生产环境的小规模内部系统。支持持久化存储、索引配置和基本的过滤条件,并发几百路没有问题。我们的知识库在切分成向量之后大概是五十万个片段,用这类方案非常合适。它的运维成本不高,单个进程就能跑起来,也支持数据备份。
第三类是分布式向量数据库集群,适合千万甚至亿级向量的大规模场景。多副本、高可用、水平扩展都是它的强项,但部署运维复杂度也上了一个台阶,没有专职运维团队的话不推荐一上来就上。
选择的时候建议先评估三个指标:预计向量条数、峰值并发量、团队运维能力。向量条数可以用“文档总字符数 ÷ 平均切分长度”粗算。比如一百万字文档,每五百字切一块,大概就是两千个向量。选型也要考虑后续迁移成本,最好提前确认向量库是否支持导出和重新导入接口,避免项目做到一半被锁定。
2.2 嵌入模型与大模型的选择原则
嵌入模型决定检索召回的上限,这是我在实战中体会最深的一点。很多人选嵌入模型只看排行榜分数,但排行榜分数高不代表在你的业务数据上好用。内部文档有大量专业术语和固定表达,通用模型在训练时未必见过这些说法。我的建议是建一个五十条左右的评测集,每条包含“标准问题、正确答案所在文档片段、预期是否有匹配”,拿两三个候选嵌入模型分别跑一轮,看哪个的召回准确率更高再定下来。
嵌入模型的其他参数也要留意。向量维度越高,存储和计算成本越大;但对语义表达确实更细腻。中文场景建议优先选择在中英双语语料上训练的开源嵌入模型,纯英文模型处理中文内部文档时经常会出现语义偏差。另外要确认嵌入模型的最大输入长度,如果切分后的文本块超过模型上限,入库时会直接报错或截断。
大模型的选择逻辑不太一样。这里主要考虑的是上下文窗口大小、指令遵循能力和推理成本。上下文窗口决定了能塞多少参考资料,但也不需要盲目求大,因为参考资料塞得太多,模型反而会被不相关内容干扰。我个人的标准是:指令遵循能力比基础智力更重要,RAG 场景里的模型核心任务只有一个——“基于给定资料回答问题,不越界、不编造”。
2.3 文本切分策略是知识库质量的起点
文本切分在整个 RAG 链路里容易被轻视,但它对最终效果的影响几乎是决定性的。切分太大,检索到的片段可能横跨多个主题,噪声多;切分太小,单个片段语义不完整,模型拿到一块残缺内容根本读不出有效信息。
主流的切分方式大致有四种。固定长度切分最简单,按字符数硬切,但很容易把一句话或一个表格拆得七零八落。按段落切分更符合阅读习惯,但现实文档里的段落长度差异很大,有些段落几百字,有些只有一句话。递归切分是在段落、句子、子句之间逐级回溯,比如先尝试按段落切,段落太长就继续按句子切,这样既能保持语义边界又能控制长度。基于结构的切分则是利用 Markdown 标题、PDF 标题层级、Word 样式来确定边界,对结构清晰的文档效果最好。
我在实际操作中采用的是“结构优先、长度兜底”的组合策略。扫描文档如果自带了标题结构,就先把每个二级标题下的内容作为一个大块,再按设定长度二次切分。如果文档没有明确层级,就纯按递归式切分。这样既能保证每个块的语义相对完整,又能控制块的长度在合理范围内。
2.4 编排框架要不要引入
做 RAG 之前我特意对比过“手写管线”和“使用框架”两种路线。框架的好处是上手快,很多轮子都给你造好了,文档加载、切分、向量化、检索全是现成方法,十几行代码就能跑通一个 Demo。坏处是框架的抽象层级比较高,一旦效果不对,你很难判断问题出在哪一步。我见过很多人用框架跑通了流水线,但检索结果非常离谱,却不知道是切分参数不对还是向量模型选错了。
我的建议是分阶段决策。快速验证阶段可以用框架先跑通端到端流程;进入调优阶段之后,把切分、检索、重排序这几个关键环节替换成自己的实现。这样既能享受框架的便捷,又能保证核心环节的可控性。我个人最终采用的是轻量自研管线,把每一步都拆成独立的函数,出了问题直接在对应环节打日志排查。
3. 核心实现与关键参数解析
3.1 文档加载与清洗的实操细节
清洗文档这一步看着不起眼,其实能解决很多检索质量问题。我们项目里主要处理三种格式:PDF、Word、Markdown。
PDF 文档要分两种情况处理。如果 PDF 本身带有文本层,可以直接用解析工具提取;如果是扫描件,就要先做 OCR 再提取文本。扫描件的识别质量直接影响后续效果,清晰度不足的扫描件建议先做图像增强预处理。提取完文本之后还要做一轮噪声清洗:去掉页眉页脚、页码、常见水印文字、目录中的点线引导符。这些噪声如果混进向量库,很容易在检索时被误召回。
表格处理是清洗环节最麻烦的部分。直接按文本顺序读取表格,内容会被拆得乱七八糟。我的做法是把表格转成“字段: 值”的描述文本,例如“型号: HT-3000;最大功率: 2.5kW;噪声等级: 58dBA”。这样既保留了表格里的关键信息,又能让嵌入模型理解字段与值的对应关系。转换逻辑可以用简单的规则实现,遇到复杂表格我建议人工复核一遍,确保字段没有错位。
Markdown 文档清洗相对简单,重点是把标题层级信息保留下来,后面做切分时可以直接利用这些结构信息。Word 文档需要先转换成纯文本格式,再检查是否有嵌套表格和文本框内容丢失。清洗完成后建议做一次全文抽查,肉眼确认提取内容没有大段乱码或缺失。
3.2 切分参数:800字加150字重叠的依据
我最终的切分参数是每个块 800 个字符,块与块之间重叠 150 个字符。这个参数不是拍脑袋定的,前后对比实验选出来的。
切分大小对检索效果的影响很微妙。块太小,比如 200 字,检索到的片段可能只是一句话的一部分,模型拿不到完整上下文;块太大,比如 2000 字,检索到的一个块里可能混了五六个主题,和查询真正相关的内容只占一小部分,模型容易被无关内容带偏。800 字对中文文档来说是一个比较均衡的值,既能覆盖一个完整的技术描述段落,又不会引入太多噪声。
重叠区域的设置同样重要。如果切分没有任何重叠,当查询内容恰好落在两个块的边界附近时,很可能两边都检索不到完整答案。150 字的重叠相当于每个块和前一个块共享大约 18% 的内容,边界处的关键信息被截断的概率就会大幅降低。重叠也不是越大越好,重叠太多意味着同样的内容会被反复存储,浪费向量库空间,也增加了检索时返回重复内容的概率。
不同文档类型可以适当调整参数。对于表格类文档,因为每条记录本身就是完整的语义单元,我建议把表格单独处理,不套用统一切分规则。对于代码片段、命令行帮助这类结构化内容,按代码块整体切分比按字符硬切效果好得多。
3.3 向量化入库与索引配置的实现
向量化入库的核心流程可以用一段很简单的伪代码概括:
# 伪代码:向量化入库流程 chunks = load_and_split_documents("data/产品手册.pdf") # 逐批调用嵌入模型,避免一次性加载过大 vectors = [] for batch in chunks_batched(chunks, batch_size=32): batch_vectors = embedding_model.encode(batch) vectors.extend(batch_vectors) # 写入向量库,附带文档ID和块序号 vector_store.write( vectors=vectors, payload=[{"doc_id": c.doc_id, "chunk_index": c.index, "text": c.text} for c in chunks] )嵌入模型对输入长度有限制,批量编码时要注意控制 batch 大小,避免内存溢出。我给 batch_size 设的是 32,实际可以根据机器显存或内存调整。入库的时候一定要附带文档 ID 和块序号,这两个字段后续做增量更新和来源追溯都要用到。
向量库中的索引参数也需要调整。常见的近似最近邻索引有两个关键参数 M 和 efConstruction。M 控制每个节点的最大连接数,值越大检索越精确但索引越大;efConstruction 控制建索引时的搜索广度,值越大建索引越慢但质量越好。我用的是 M=16、efConstruction=200 这一组参数,建索引速度和召回质量比较均衡。查询时的 efSearch 我设置为 40,这个值控制查询时参考的候选节点数,值越大召回越准但响应越慢。
距离度量方式我选择的是余弦相似度。文本向量的模长本身受文本长度影响较大,余弦相似度只关注方向,对向量模长不敏感,更适合判断文本语义是否相关。如果你的嵌入模型在训练时用的就是内积或欧氏距离,那就跟随模型默认设置,不要强行改。
增量更新是知识库落地必须考虑的问题。我的方案是给每个文档算一个哈希值,写入向量库时记录在 payload 里。当文档重新上传或内容更新后,先比对哈希,如果发生变化就删除该文档下所有旧向量,再重新切分、重新向量化并写入。这样就避免了旧版本内容和新版本内容同时存在于知识库里的尴尬情况。
3.4 检索阶段的多路召回与重排序
只靠向量检索一条路,很容易漏掉一些关键词精准匹配的场景。比如用户直接问“HT-3000 的保修期”,向量检索可能把语义相近的“售后保障时间”也召回来,但精确到具体型号的片段反而不一定排在最前面。所以我采用了“向量检索 + 关键词检索”两路召回再融合的方案。
向量检索负责语义理解,适合处理“设备坏了找谁修”这类表述模糊的问题。关键词检索负责精确匹配,适合处理包含型号、编号、专有名词的查询。两路结果合并之后需要做去重,因为同一个文档片段可能同时被两路召回。去重之后,候选集还在二十条左右,直接全部塞进提示词显然不行,这时候需要重排序。
重排序我采用独立的重排序模型,把“用户查询 + 候选片段”拼在一起做深度语义匹配,输出一个相关性分数。和向量检索的粗排相比,重排序模型能看到完整的查询和完整的内容,打分更准确。代价是速度慢一些,但重排序只需要处理二十条候选,耗时完全可以接受。
重排序之后只保留 Top 3 片段作为参考答案。为什么保留三条而不是更多?因为模型生成回答时,上下文越长,注意力越容易被分散。三条高质量片段通常已经覆盖了答案所需的上下文,再多反而可能引入矛盾信息。如果三条片段都来自同一文档的不同部分,我会额外检查它们的块序号是否连续,如果不连续说明检索结果可能遗漏了中间的关键内容。
检索阶段还有一个容易被忽略的细节:对用户查询先做改写。原始问题往往是口语化的,比如“保修多久啊?”直接拿去向量检索效果很差。我会先让大模型把口语化问题改写成标准查询——“设备保修期限是多少”,然后再送入检索环节。这一步的收益非常明显,检索精准度提升了一大截。
3.5 提示词设计与答案质量控制
提示词是 RAG 链路的最后一环,也是防止模型瞎编的最后一道防线。我最终使用的提示词模板如下:
你是一个企业内部知识库助手,必须严格基于【参考资料】回答用户问题。 要求: 1. 如果参考资料包含答案,请用简洁的语言回答,并在句末标注信息来源编号。 2. 如果参考资料不包含答案,请直接回复“知识库中暂未找到相关信息”,禁止自行推断。 3. 不要输出与参考资料无关的补充内容,不要使用外部知识。 4. 如果多个参考资料信息矛盾,以编号靠前的资料为准并说明存在不同说法。 【参考资料】 {context} 【用户问题】 {question}这套提示词有几个关键设计。第一是“必须严格基于参考资料”这句话,它在指令层面把模型的发挥空间限制住了。第二是要求标注来源编号,这让回答变得可追溯,用户也能自己判断答案的可靠性。第三是明确定义了“找不到就承认找不到”,避免模型为了面子硬答。
模型参数也需要配合。我把 temperature 设置为 0.1,几乎关闭了随机性,让模型每次对同一问题的回答尽量一致。max_tokens 设置为 512,防止模型把答案写成一篇长文。对于回答超过 512 个 token 的问题,建议在检索阶段就优先选择更聚焦的片段,而不是靠增大 max_tokens 硬撑。
生成答案之后还可以加一层后校验。我写了一个简单的规则检查,如果回答文本里存在“我猜”“可能”“也许”这类模糊用词,再结合检索片段确实没有相关内容,就自动改为“知识库中暂未找到相关信息”。这个兜底规则虽然简单,但能把幻觉率再压下去几个百分点。
4. 常见问题与排查技巧实录
4.1 检索质量很差的排查思路
遇到“答非所问”的情况,绝大多数根源不在大模型,而在检索链路。我的排查顺序是从下游往上游走,先看检索结果再推问题。
第一步,单独测试检索。不经过大模型,直接把用户问题送入检索链路,看返回的 Top 5 片段是否和问题相关。如果检索结果本身就不相关,问题一定出在“查询理解、切分、向量模型、相似度计算”这四个环节里。如果检索结果相关但生成答案不对,问题才出在提示词或大模型参数上。
第二步,检查查询改写是否合理。把历史对话里的口语化问题收集起来,看看改写后的标准查询是否完整保留了关键信息。型号、编号这类关键实体在改写过程中丢失是最常见的问题。
第三步,回头看切分结果。打开几个候选片段的原文,确认它们是不是完整的语义单元。如果发现片段把关键信息截断成两半,或者一个片段里混杂了多个主题,那就是切分参数的问题。这种情况我会调整切分大小和重叠,再重新向量化入库。
第四步,排查向量库本身的配置。确认相似度计算方式、索引参数、查询阈值设置是否合理。有的向量库默认返回分数很低的结果,需要查看日志确认 Top 片段分数是否低到异常。
4.2 性能优化与缓存设计
RAG 知识库的响应延迟主要来自四个部分:查询理解、检索、重排序、大模型生成。其中大模型生成通常占比最大,但检索和重排序的耗时也不能忽视。
第一个优化手段是缓存。对于高频重复的问题,直接缓存最终答案,命中后跳过整个 RAG 链路。缓存键可以用“规范化后的问题文本 + 文档库版本号”的组合,文档库一旦更新,旧缓存自动失效。实际运行中这个方案能命中三成左右的重复问题,对体验提升非常明显。
第二个手段是多级检索。先做一轮轻量级向量粗筛,只保留前五十个候选,再做关键词过滤,把明显不相关的片段删除。粗筛阶段使用较低的 efSearch 值,比如 20,重查阶段再用更高的值。这样可以在保证召回质量的同时缩短检索耗时。
第三个手段是限制上下文。提示词里并不是塞的片段越多越好,我通常只保留重排序后的前三片段,大约两千字左右,把大模型需要处理的输入长度控制在合理范围内,生成延迟也会降下来。
第四个手段是并发优化。如果用户量上来了,嵌入模型的调用和向量库的查询都会成为瓶颈。可以加一层连接池,同时把向量库的查询操作和重排序操作放到独立线程池里去执行,避免阻塞主流程。
4.3 常见问题速查表
| 现象 | 可能原因 | 解决建议 |
|---|---|---|
| 检索结果与问题完全无关 | 嵌入模型适配性差 | 换针对中文优化的模型,用业务评测集验证 |
| 正确答案在库里但回答错误 | 提示词约束不足 | 强化“仅依据参考资料回答”的指令 |
| 召回片段内容不完整 | 切分大小太小或重叠不足 | 增大切分长度,增加重叠比例 |
| 召回片段噪声太多 | 切分大小太大或跨主题 | 缩小切分长度,按结构切分 |
| 精确型号、编号查不到 | 向量检索不擅长精确匹配 | 增加关键词检索路,或多路召回融合 |
| 回答引用了不相关内容 | 重排序缺失或阈值过低 | 增加重排序环节,提高相关性阈值 |
| 文档更新后旧内容还在 | 增量更新逻辑不完整 | 按文档哈希删除旧向量再写入 |
| 同一个问题结果不稳定 | temperature 过高 | 将 temperature 调低至 0.1 左右 |
| 响应延迟过高 | 候选集太大或上下文过长 | 减少 TopK,限制片段数,增加缓存 |
实战里最容易被忽视的是“阈值”这个概念。很多向量库默认返回 TopK 结果,但返回的结果分数可能低到不可用。我建议在检索环节设置一个最小相似度阈值,低于阈值的片段直接丢弃。这个阈值需要通过测试集来标定,一般来说 0.4 到 0.6 之间比较常见,具体数值和嵌入模型强相关。
5. 实操心得与后续可扩展的方向
5.1 三个让我印象深刻的预处理教训
第一个教训是关于表格的。我最初直接按文本顺序切分 PDF 表格,结果一个规格参数表的每一行被拆成若干碎片,检索“功率大小”时召回的内容是一堆数字和冒号,完全不可读。后来改成“字段: 值”的文本化描述,效果立竿见影。现在遇到复杂表格,我会先做人工抽查,确认转换后的文本语义完整。
第二个教训是关于扫描件 OCR。当时有一批旧版技术手册是扫描 PDF,直接提取文本得到的是一堆乱码。我加了一道 OCR 预处理之后,准确率上来了,但 OCR 后的文本里有大量识别错别字,比如把“功率”识别成“公率”。这些错别字严重影响嵌入模型对语义的理解。后来我建了一个针对错误拼写的自动纠错规则表,结合领域词库做二次校正,才算把问题基本解决。
第三个教训是关于重复内容的。知识库里有大量文档是同一内容的多版本拷贝,最初入库时没有去重,导致检索结果里前五条全是同一段话的复制品,真正的第二候选被挤到很后面。后来我在入库前先做文档间相似度比对,内容重复超过九成的文档只保留最新版本,这个问题就消失了。
5.2 几个值得尝试的增强方向
如果你已经在跑一个基础版 RAG,以下几个方向可以继续深入。
第一是多级路由。不是所有用户问题都适合走 RAG 链路,比如“你是谁”“帮我算一下 2+2”这类问题,直接用大模型原生能力回答就好。加一个意图分类器做路由,把“知识库问题”和“非知识库问题”分流,可以避免不必要的检索开销。
第二是引用溯源。当前版本只返回答案文本,用户对答案存疑时很难判断依据是什么。在回答中标注信息来源和原文链接,能显著提升知识库结果的可信度。这个功能对内部系统的落地价值尤其大。
第三是定期重新嵌入。嵌入模型更新之后,旧向量不一定还能和新模型对齐。我的建议是每个季度做一次全量重新嵌入,当然这会消费一定算力,需要在效果和成本之间做权衡。
第四是反馈闭环。我建议在问答接口里增加“有帮助/没有帮助”的反馈按钮,把标记为“没有帮助”的问答对收集下来,定期分析是检索召回的问题还是生成约束的问题。这个闭环跑起来之后,系统的优化就不再是拍脑袋了。
做 RAG 知识库这件事,技术本身并没有多高深,但把它打磨到能真正落地的过程中,小坑比想象中多得多。如果你正在做类似项目,我的建议是先建立自己的评测集,用真实问题和真实文档去测每一版改动,不然很容易陷入“感觉效果变好了但又说不出哪里好了”的状态。希望这份实战记录能帮你少走一些弯路。