1. 从“模型只会背书”到“让模型学会查资料”
先说个我在实际需求里反复见到的场景:管理层决定引入大模型能力,团队先是把公司文档一股脑扔给模型做微调,结果模型对着几千页制度文件“背”完以后,回答还是漏洞百出——要么张三的流程答成李四的流程,要么把两年前已经作废的旧条款当成现行政策。这不是模型不行,而是选错了技术路线。企业知识更新的节奏、权限隔离的要求、回答溯源的需求,都决定了微调不是最优解,真正合适的是 RAG(Retrieval-Augmented Generation,检索增强生成)。
RAG 的思路其实特别朴素:模型不懂企业内部的文档,那就先帮它“查资料”,把查到的片段作为上下文一起交给模型,让模型基于这些材料来回答。整个过程有点像给一个刚转岗的新人配了一位带教导师,新人不需要把所有制度背下来,遇到问题先问导师“对应条款在哪儿”,导师翻出原文,新人在原文基础上组织语言输出。这样一来,模型不必记住企业私有知识,只需要学会“读材料作答”,知识更新、权限控制、答案溯源都变得可控了。
这篇内容适合谁看?如果你正在做知识库问答、企业私有化部署、智能客服、内部文档检索这类项目,或者你已经在用某个大模型但发现“它不懂我们公司的东西”,那么这篇实操记录可以帮助你把 RAG 从概念落到工程。我会先从原理讲清楚,再给出整套可复现的搭建方案,最后把我在企业场景里踩过的坑和排查方法一并倒出来。
2. 理解 RAG 的完整链路与三个关键决策点
2.1 一条流水线上的五个环节
RAG 在企业知识库场景下,本质上是一条流水线。输入是一堆格式各异的文档,输出是“用户问一句、系统答一句,且答得有依据”。这条流水线可以拆成五个环节:文档加载、文本切分、向量化、向量检索、生成回答。
文档加载解决的是“把 PDF、Word、Markdown、HTML 里的内容提出来变成纯文本”。这一步听着简单,实际最脏最累。文本切分决定“模型每次能看到哪些片段”,切大了浪费模型上下文窗口,切小了语义不完整。向量化把文字变成数字列表,让计算机能计算“哪两段话意思接近”。向量检索负责从几十万条片段里快速捞出和用户问题最相关的内容。生成回答则是把问题与检索结果一起交给大模型,让它组织出最终答案。
这个链路里最容易被忽视的是“检索”与“生成”两侧的配合。很多人以为把相关片段塞给模型就行,其实检索质量直接决定生成质量。检索回来一堆不相关的内容,模型再聪明也答不对;检索回来内容相关但残缺不全,模型会自己脑补出看似合理实则虚构的细节。
2.2 三个关键决策点决定了项目天花板
第一个决策点是选择哪种 Embedding 模型。Embedding 模型的作用是把文本转换成向量,行业内通常用 768 维或 1024 维的浮点数列表表示一句话的意思。选型时要看的是它在领域语料上的表现,而不是看它在通用榜上的分数。企业内部文档往往包含大量术语缩写、产品代号、角色名称,通用模型在这些词上容易“脸盲”。
第二个决策点是选择哪种向量数据库。向量数据库负责存储和检索向量,开源方案有 Chroma、Milvus、Qdrant、Weaviate,还有基于 PostgreSQL 的 pgvector 扩展。选型需要评估的是数据规模、查询并发量、部署运维成本和与现有技术栈的匹配度。
第三个决策点是确定切分策略。文本切分直接决定检索到的片段是否语义完整。常见的做法是按固定字符数切、按段落切、按语义边界切三种。固定字符数最简单但容易把一句话拦腰截断;按段落切符合自然阅读习惯但段落长短不一会浪费空间;按语义边界切效果最好,实现复杂度也最高。
这三个决策点,每一个都值得展开说。下面的章节我会分别给出具体选型建议和实测经验。
3. Embedding 模型选型:决定“语义理解”的上限
3.1 理解向量与语义相似度
先建立直觉。一句话“请帮我查一下年假剩余天数”转换成向量后,可能是 768 个浮点数,例如 [0.0321, -0.2143, 0.0876, ...]。另一句话“我还有几天年假没休”会转换成一个语义上相近的向量。向量之间的距离(用余弦相似度计算)越小,说明两句话的意思越接近。
用生活化类比来解释:每个词或每句话像地图上的一个坐标点。相似的语义在“语义空间”里挨得近。Embedding 模型做的就是“把文字翻译成坐标”,翻译得准不准,直接决定了后续检索能不能找到正确片段。
3.2 国产开源模型 vs 商用 API:成本、隐私与效果的三方权衡
在企业知识库场景,我强烈建议优先考虑私有化部署的 Embedding 模型,理由有两个:一是数据不出内网,满足合规要求;二是检索是高频操作,走外部 API 会带来延迟和按量计费成本。
当前环境里值得考虑的开源 Embedding 模型有中文场景下表现不错的文本向量模型,以及多语言模型。不同模型的维度数不同,有的 768 维,有的 1024 维。维度越高表达细粒度语义的能力越强,但存储和计算成本也越高。实测下来,通用问答场景 768 维足够,如果涉及大量领域术语,可以考虑 1024 维的模型。
还要注意一个容易踩的坑:查询语句和入库文档必须使用同一个 Embedding 模型。如果入库时用模型 A 生成向量,查询时用模型 B,语义空间不兼容,检索可以直接放弃。
3.3 不同规模企业知识库的选型参考
我整理了一张选型参考表,基于我在模拟项目中的实测经验:
| 知识库规模 | 文档量级 | 推荐方案 | 说明 |
|---|---|---|---|
| 小型(部门级) | 1 万片段以内 | 768 维开源模型 + Chroma | 单机内存部署,开发快,够用 |
| 中型(公司级) | 1 万到 50 万片段 | 768 维或 1024 维模型 + Milvus/Qdrant | 需要分布式支持,推荐容器化部署 |
| 大型(集团级) | 50 万片段以上 | 1024 维模型 + Milvus 集群 + GPU 加速 | 需要考虑分库分表与检索性能调优 |
这只是参考基准,真实选型还要结合业务并发量。如果知识库只是内部几十个人用,Chroma 完全可以扛住;如果要做面向全体员工的智能助手,查询并发上来了,就得尽早切到 Milvus 这类支持分布式的方案。
4. Chunk 切分:被低估的检索效果命门
4.1 为什么切分比向量化更影响结果
我在排查过很多质量差的 RAG 系统后发现,问题根源往往不是 Embedding 模型选得不好,而是 Chunk 切分太粗暴。文本切分的核心矛盾在于:片段太大,检索命中后包含大量无关信息,且会挤占大模型的上下文空间;片段太小,语义不完整,检索时匹配不到,就算匹配到了模型也读不出完整逻辑。
举个例子。一份员工手册里有这么一段流程:“请假申请需提前一个工作日提交,如遇紧急情况可在当日通过系统补交,但需要直属上级在三个工作日内审批确认。”如果把这段切成了“请假申请需提前一个工作日提交,如遇紧急情况可在当日通过”和“系统补交,但需要直属上级在三个工作日内审批确认”两块,那么用户问“紧急请假怎么走流程”时,第一块能检索到,第二块就丢了。模型只看到前半句,就会漏答“上级审批确认”的环节。
4.2 三种切分方法实测对比
固定字符切分。这是最简单也是效果最不可控的方式。按每 200 或 300 个字符硬切,不感知段落边界。优点是实现简单、性能好;缺点是会切断语义。我拿一批真实制度文档做过对比,固定 300 字符切分,检索召回的前 5 个片段里,有接近三成是语义断裂的。
按段落切分。以 Markdown 的换行符或自然段落为单位切块,保持语义完整性。优点是与文档结构对齐;缺点是多级标题下段落长短差异巨大,有的段落只有十几个字,有的段落上千字。对于超长段落,可以结合“段落内二次切分”的策略,把超过阈值的段落继续切,但保留段落上下文。
按语义边界切分。利用模型识别句子的语义边界,在句子结束处且语义相对完整的地方切断。效果最好,但实现成本也最高。企业场景里有一个实用折中方案:以段落为单位,段落过长时按句号或问号进行二次切分,尽量让每个片段控制在 200 到 500 字之间。
4.3 切分的最佳实践清单
我在实操中总结了几个好用的原则:
- 保留标题层级。切分时把“一级标题 + 二级标题 + 正文片段”的组合作为一条记录,检索时能知道这段内容归属哪个章节,溯源更清晰。
- 设置重叠窗口。相邻片段之间重叠 50 到 100 个字符,能有效避免关键词落在边界处导致漏检。
- 不要混入页眉页脚。PDF 解析后经常带出页码、公司名、页眉信息,这些噪声会污染向量,应当清洗掉。
- 结构化文档优先按表切。Excel、Markdown 表格按行切分往往比按整表切效果好,检索到单行数据时可直接用于回答。
注意:切分策略没有“一套打天下”的方案。同类文档做一次质量抽检,抽几段问自己“如果我只看到这一片段,能理解上下文吗”,这是判断切分效果最快的方法。
5. 实操:端到端搭建企业知识库 RAG
5.1 环境准备与技术栈
先给出一套我在模拟项目里用的技术栈,你可以按自己的环境替换:
- 语言:Python 3.10+
- Embedding:开源中文文本向量模型(768 维)
- 向量数据库:Chroma(数据量小)或 Milvus(数据量大)
- 大模型:本地部署的开源对话模型,或公司已接入的内部模型服务
- 文档解析:PyMuPDF(PDF)、python-docx(Word)、requests + BeautifulSoup(网页)
安装依赖用以下命令:
pip install chromadb sentence-transformers pymupdf python-docx如果选用 Milvus,还需要额外安装 pymilvus 并启动 Milvus 服务,生产方式推荐用 Docker Compose 部署独立集群。
5.2 文档加载与清洗
文档加载这一步,最容易踩坑的是 PDF。PDF 有两种形态:文本型 PDF(文字可以直接提取)和扫描型 PDF(需要 OCR 识别)。文本型 PDF 用 PyMuPDF 提取效果不错;扫描型 PDF 需要接入 OCR 服务,不仅速度慢,识别错误的文字还会直接影响后续检索质量。
加载完成后必须做清洗。我碰到过的最典型问题:从 PDF 里提取出来的文本中包含大量换行符、制表符、多余空格,如果不处理,切分时会出现半句话断行的情况。清洗脚本的核心逻辑是把多行合并成连贯段落,去除页眉页脚和页码。
import re def clean_text(text): # 去除页眉页脚常见模式,例如页码和公司名 text = re.sub(r'\n\d+\n', '\n', text) # 合并断行,但保留段落间的空行 text = re.sub(r'(?<!\n)\n(?!\n)', '', text) # 压缩多余空格 text = re.sub(r'[ \t]+', ' ', text) return text.strip()这一步做得好不好,直接决定后面的数据质量。我见过不少团队在这一步偷懒,结果检索出来的片段全是半句话,用户看到回答自然一头雾水。
5.3 切分与向量化入库
切分逻辑写成一个函数,输入是清洗后的文本,输出是若干 Chunk。以下是一个按段落为主、长段二次切分的实现思路:
def split_into_chunks(text, max_length=400, overlap=50): paragraphs = text.split('\n\n') chunks = [] for para in paragraphs: para = para.strip() if not para: continue if len(para) <= max_length: chunks.append(para) continue # 长段落按句子切分,并添加重叠窗口 sentences = re.split(r'(?<=[。!?])', para) current = '' for sent in sentences: if len(current) + len(sent) > max_length and current: chunks.append(current.strip()) current = current[-overlap:] + sent else: current += sent if current.strip(): chunks.append(current.strip()) return chunks切分完成后,调用 Embedding 模型生成向量,再写入向量数据库。入库时要把来源文档、章节标题、原文片段一并存进 Metadata,这样后面做答案溯源就方便了。
from sentence_transformers import SentenceTransformer import chromadb model = SentenceTransformer('BAAI/bge-base-zh-v1.5') client = chromadb.PersistentClient(path='./knowledge_db') collection = client.get_or_create_collection('company_docs') for idx, chunk in enumerate(chunks): vector = model.encode(chunk).tolist() collection.add( ids=[str(idx)], embeddings=[vector], documents=[chunk], metadatas=[{'source': doc_name, 'section': section_title}] )5.4 检索与生成链路
检索侧的流程是:用户输入问题 -> 用同一个 Embedding 模型对问题编码 -> 在向量数据库里查询 TopK 最相近的片段 -> 把片段作为上下文交给大模型。
def query_knowledge_base(question, top_k=5): question_vec = model.encode(question).tolist() results = collection.query( query_embeddings=[question_vec], n_results=top_k, include=['documents', 'metadatas', 'distances'] ) return results代码写起来不难,难的是“怎么把检索结果组织成模型能看懂的上下文”。我的实践是把多个片段拼接成一段带编号的内容,明确告诉模型哪些是参考资料。提示词模板如下:
请基于以下资料回答问题。如果资料中没有相关信息,请直接回复“知识库中未找到相关信息”,不要编造答案。 资料1:《员工手册》第三章 请假制度 请假需提前一个工作日提交申请,紧急情况可在当日补交,需直属上级三个工作日内审批。 资料2:《员工手册》第四章 考勤管理 考勤异常需在当月提交说明... 问题:紧急请假怎么操作? 回答:这里有一个关键技巧:提示词里必须加上“不要编造答案”的约束。企业场景中宁可让模型说“不知道”,也比它自信地给出一段虚构流程要安全得多。这也是 RAG 系统区别于普通聊天的红线之一。
5.5 检索参数调优
TopK 的值直接影响回答质量。TopK 太小(比如 1),模型看到的上文太少,回答容易偏;TopK 太大(比如 20),大量低相关度片段混入,模型反而被噪声干扰。我在实测中把 TopK 设为 5 到 8 是相对稳妥的区间。
还有一个容易忽略的参数是距离阈值。向量检索会返回相似度分数,设定一个最低相似度阈值(比如 0.6),低于阈值的片段直接丢弃。这样当用户问“帮我写一首诗”而知识库里全是制度文档时,系统会诚实回答“未找到相关信息”,而不是硬从员工制度里拼凑几句像诗的话出来。
注意:相似度阈值需要结合所选 Embedding 模型实测调整。不同模型的分数分布区间可能完全不同,我见过 bge 系列在相关片段上的相似度普遍在 0.65 以上,而有些通用模型要到 0.75 以上才靠谱。
6. 企业落地中的三个硬性问题:权限、幻觉、性能
6.1 知识权限隔离:不能答非所问,更不能越权回答
在企业场景里,RAG 系统不只是“问答工具”,它还涉及一个严肃问题:答出不该答的内容比答不出来更严重。比如员工问薪酬制度,系统如果检索到 HR 内部文件并输出敏感信息,这是事故级别的问题。
权限隔离的通用做法是在切分入库时为每条 Chunk 打上权限标签。Metadata 里存一个access_level字段,例如:public、internal、confidential。查询时根据当前用户的权限级别进行过滤,再走向量检索。这样即使向量库里存了敏感内容,低权限用户也搜不到。
results = collection.query( query_embeddings=[question_vec], n_results=top_k, where={"access_level": {"$in": accessible_levels}}, )权限过滤的粒度也可以更细,比如按部门过滤、按项目过滤。实现思路一致,关键是“检索前过滤优于检索后过滤”。先过滤再检索,既保证安全又节省计算量;先检索再过滤,敏感内容实际上已经出了向量库,在日志里都可能留下痕迹。
6.2 幻觉治理:让模型学会说“不知道”
RAG 系统天生比纯大模型聊天更抗幻觉,因为模型有资料可依。但幻觉并没有完全消失,我总结过三类常见的幻觉:
第一类是“过度推断”。资料里只说了请假的流程,模型自行补充了“请假会影响全勤奖”等内容。治理方法是提示词里强约束“只能基于资料内容回答,不得推断资料未提及的信息”。
第二类是“资料冲突”。知识库中存在新旧版本文档,模型检索到两条互相矛盾的流程,于是自行选择一条融合输出。治理方法是入库前做版本标记,查询时优先过滤到最新版本。
第三类是“编造引用”。模型引用的内容看起来像原文,实际是它自己生成的。治理方法是在提示词中要求模型逐字引用原文片段并标注资料编号,同时在回复末尾附上溯源条目,方便人工核对。
6.3 性能优化:从首字延迟到并发吞吐
RAG 系统的性能瓶颈往往不在大模型生成,而在检索链路和 Embedding 计算。对于几万片段的小型知识库,单机 Chroma 毫秒级响应没问题;当片段量到几十万,就需要切 Milvus 集群,做分片。
还有一个容易被忽视的性能点:查询时对问题做 Embedding 这步是串行的。如果并发高,建议用独立服务部署 Embedding 模型,避免每次都加载模型到内存。实测中把这个步骤接口化后,单机吞吐提升了近一倍。
可选的优化方案是引入缓存层。对高频问题做精确匹配缓存,命中的直接返回历史回答,跳过检索和生成链路。企业知识库里的“年假多少天”“报销流程”这类问题是典型的高频重复问题,缓存能显著降低系统压力。
7. 常见问题与排查技巧实录
7.1 检索召回的内容驴唇不对马嘴
这是我的排查列表里排第一的问题。遇到这种情况,我会按三步定位:
第一步,单独测 Embedding 质量。找 10 对语义相似的问题和文档片段,计算相似度分数。如果分数普遍很低,说明 Embedding 模型不适合领域语料,需要换模型或做领域微调。
第二步,检查切分质量。随机抽 20 个 Chunk 人工阅读,看语义是否完整。如果有大量截断,问题出在切分策略。
第三步,检查查询侧文本。用户问题里如果带口头语、错别字,会直接影响编码效果。可以在查询前做一次简单的文本规范化,比如把“查一下余额”中的语气词去掉。
7.2 回答正确率不错但用户不认可
这种情况多半是“答案对了,但没有出处”。企业用户在内部系统上提问,不只是想看结论,还希望看到结论出自哪份文件、哪一条制度。解决办法是把溯源信息直接展示在回答下方,从 Metadata 里取source和section字段拼成引用列表。
我实践过一个比较好的展示格式:“回答内容 + 参考文档 + 原文片段”。这样既方便用户验证,也让系统显得可信任。
7.3 冷启动:知识库还没建好时怎么快速验证
企业知识库搭建是个大工程,但验证 RAG 效果不需要等全部文档清洗完。我建议先选一个业务领域(比如人事制度)的 20 到 30 份文档,完成从加载到检索再到生成的完整链路,跑通了再按业务模块逐步扩展。先跑通一条业务线,比一口气铺开所有部门文档更稳妥。
7.4 常见问题速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 检索结果明显不相关 | Embedding 模型不适配领域 | 换模型、领域微调 |
| 检索相关但回答跑偏 | 上下文拼接方式不合理 | 优化提示词模板 |
| 回答内容有编造成分 | 缺少“不知道就直说”的约束 | 强化提示词约束与溯源 |
| 查询速度越来越慢 | 向量库数据量增长未分片 | 迁移 Milvus、增加索引 |
| 同一问题回答不稳定 | TopK 过大引入噪声 | 调小 TopK、提高相似度阈值 |
| 新文档更新后检索不到 | 入库链路未触发或切分失败 | 检查加载清洗流程 |
| 用户提到的术语搜不到 | 切分边界切断关键词 | 增大重叠窗口、术语扩展 |
8. 从搭起来到好用:我建议的优化顺序
如果你按上面步骤搭好了一套可用的 RAG,先别急着铺量。我个人的经验是把优化精力按以下顺序投入:先优化切分质量,再优化检索召回,然后做答案溯源的展示,最后考虑接入权限体系。
切分质量影响的是整个系统的地基;检索召回决定模型能“看到”什么;溯源展示决定用户信不信;权限体系决定系统能不能真正上线到生产环境。这个顺序走完一遍,系统的可用度会有明显提升。
在模拟项目中我遇到过这样一个场景:制度文档更新后,旧版本的内容还在向量库里,导致同一个问题出现新旧两套答案。后来我在入库流程里加入了版本标记和“下架”逻辑——新文档入库时把同主题的旧文档标记为不可用,检索时默认过滤掉。这个细节看着小,实际影响非常大,尤其是在制度频繁调整的公司里。
最后再分享一个小技巧:定期做“黄金问答集”回归测试。挑 50 到 100 个典型业务问题,每次调整切分策略或换模型之后,把这些问题跑一遍,看回答质量是变好还是变差。这是我从做推荐系统时学来的方法论——没有评测集,所有优化都靠感觉,最终结果一定不可控。有了这套问答集,RAG 的持续迭代才算有条路可走。