先讲个我自己的经历。去年我在一家做设备运维的企业帮他们搭内部知识库,需求听起来很简单:把几百份设备手册、维修工单、培训文档扔进去,让员工用自然语言提问,系统给出带依据的回答。当时老板还特意强调了一句:数据绝对不能出内网,云端大模型接口想都不要想。这就是典型的RAG私有化落地场景——检索增强生成(RAG)负责把企业私有知识检索出来,交给大模型生成回答。但真正做起来才发现,从0到1把RAG系统跑通不难,难的是把它调到一个业务上真能用的状态。这篇博文就说说我整套的落地过程,从选型、拆解、代码实现到检索质量调优,全部是基于实际项目的经验,不是PPT架构图。
这篇文章适合谁看?你自己有文档要整理、想在本地搭一套知识库问答系统,或者公司已经在用LangChain、Ollama跑实验但效果不太稳定的人。无论你用的是Python还是Java,文里的思路和排错方法都是通用的,代码部分我会给一套完整的Python示例。
1. 先想清楚:RAG解决了什么问题,私有知识库为什么不能直接问大模型
1.1 一个典型的企业知识管理场景
很多公司内部都有这种状况:技术文档散落在共享盘、Wiki、OA系统里,有的甚至是老员工脑子里的经验。新员工入职想查一个设备参数,先问同事,同事说"去那台服务器上找文件,文件名好像是xxx",结果找了一上午。
如果直接把这个问题抛给通用大模型,比如让ChatGPT或者国内的大模型API来回答,它完全没法答——因为企业内部设备型号、保养周期、质检标准这些信息,根本不存在于公开数据里。就算你强行把文档塞进上下文让模型"记住",也不现实,模型上下文窗口有限,几百份文档的体量早就超了。这就是RAG存在的根本原因:先检索,再生成。回答问题前,先从一个私有知识库里把最相关的几个片段捞出来,把它们和原始问题一起拼成Prompt,让大模型基于这些资料作答。
1.2 RAG的核心原理:借书这件事怎么拆开看
我习惯把RAG比喻成一个靠谱的图书管理员。传统问答是让一个记忆超强的"背书机器"回答问题,它什么都学过但不一定学过你们公司的东西。RAG不一样,它不指望大模型记住你所有内部资料,而是你每次提问时,现场让管理员去书架上翻出几本最相关的书,再把这几页递给"背书机器",让它照着这几页内容说话。
这个"书架"就是向量数据库。它的工作原理是:文档被切分成片段(称为chunk),每一段通过嵌入模型转成一个高维向量;提问时,再把用户问题转成同维度的向量,然后在数据库里做相似度计算,找出最接近的Top-K个片段。向量之间的相似度,通常用余弦相似度或内积来衡量,数值越高代表语义越接近。
1.3 为什么是RAG而不是微调
每次聊到私有知识库,总有人问:直接把大模型微调一下不就行了吗?我的回答通常是:分场景。模型微调适合让模型改变说话风格、学会特定输出格式、短期记忆一些频繁出现的术语,但用在知识检索上问题很大。
第一,微调本质上还是在"背资料",模型会把你喂给它的知识泛化和混合,它可能无法精确告诉你某句话出自哪份文档;第二,文档更新一次,模型就要重新训练一次,企业文档月月变,这成本谁都受不了;第三,企业内部数据动不动就是GB级别,微调很可能过拟合或丢失细节。RAG则完全相反,更新知识库就是往数据库里重新加一次文档,几乎零成本。
所以我的结论很直接:主知识入口走RAG,微调只在极少数场景考虑(比如让模型学会某个特殊术语体系或固定输出JSON格式)。两者不是替代关系,是互补关系,但绝大多数企业第一步应该走RAG。
2. 基础设施选型:模型、向量库与框架的搭配逻辑
2.1 本地模型与在线API怎么选
"数据绝对不能出内网"这句话定了调子,模型就得本地跑。我用的方案是Ollama + 量化后的开源模型。Ollama是目前最简单的本地模型运行工具,一条命令就能把模型拉下来跑起来,兼容OpenAI接口格式,做RAG链路时很省事。
模型本身,中文场景我优先推荐Qwen系列(千问),比如qwen2.5-7b-instruct,4bit量化后大概5GB左右。显存16GB的显卡就能跑,没显卡的话纯CPU跑也不是不行,就是慢一点,7B模型CPU推理大概每秒几个token,小规模内部用勉强能接受。如果公司有更好的显卡,比如3090、4090或者A100,可以上14B甚至72B的量化版本,回答质量会明显上一个台阶。
为什么选Qwen而不是Llama?中文语料质量决定的。Llama 3.1在中英文混合场景下表现也不错,但涉及中文专有名词、行业术语时,Qwen的稳定性明显更好。我们当时对比过50个内部技术问题,Qwen的准确率高出大约10个百分点。
2.2 嵌入模型:中文场景不能随便用默认配置
很多人容易忽略嵌入模型,觉得随便找个开源的用就行。其实这是RAG链路里最容易被"卡脖子"的一个环节。向量化质量直接决定了你的检索能不能把正确答案捞出来。
英文社区喜欢用OpenAI的text-embedding-3-small或者开源的BGE系列,但中文场景我强烈建议直接用BGE-M3或BGE-large-zh。BGE-M3是智源开源的,支持中文和多语言,最大输入长度8192 token,这个很关键——后面讲文本拆解时你就能理解,输入长度限制直接影响了chunk_size怎么设计。另一个常用选择是text2vec-large-chinese,老牌中文嵌入模型,效果也不错,但支持的上下文长度较短。
这里有个经验:嵌入模型的选择要跟后续检索测试挂钩,不要只看网上的benchmark。你可以提前拿20个内部的真实问题,配上正确的文档片段,跑一遍检索测试看命中率,哪个模型命中率高就用哪个。我用BGE-M3替换掉chroma内置的all-MiniLM之后,hit rate(也就是正确内容出现在Top-5检索结果里的比例)从62%涨到了81%,效果非常明显。
2.3 向量数据库:从Chroma起步,到Milvus和ES的升级路径
向量数据库这一层,我见过太多人一上来就纠结。其实思路很清晰:
| 阶段 | 选型 | 理由 |
|---|---|---|
| 起步/PoC验证 | Chroma 或 FAISS | 部署简单,Chroma自带持久化,FAISS适合单机检索 |
| 中小规模生产(百万级向量以内) | Milvus Lite 或 PostgreSQL+pgvector | 支持过滤、备份、权限控制,运维成本可控 |
| 大规模或已有ES栈 | Milvus 或 Elasticsearch | 支持集群扩展、高并发,ES 8.x直接带kNN检索能力 |
我当时在PoC阶段用的Chroma,因为本地安装一条pip命令搞定,开发和调试效率很高。等文档量到5万+片段、并发请求上来了,才迁到Milvus。建议你千万别一上来就搭分布式集群,向量数据库本质就是个存储和检索组件,先把链路跑通才是正事。
2.4 框架选择:LangChain、LlamaIndex还是裸写
说实话,现在框架选择有点"乱花渐欲迷人眼"的感觉。LangChain生态最丰富,文档加载器和各类工具的集成最全,但抽象层多,出问题时要扒源码。LlamaIndex专注于文档索引和检索,设计思路对RAG更纯粹,调优时概念清晰。至于裸写,适合你只想本地跑个小Demo练手,对原理理解更深,但生产环境我不会推荐。
我的建议是:用LangChain入门,配合少量裸代码做关键节点控制。你不需要理解框架的每个抽象类,但要看懂RAG链路的每一步在干什么。另外,如果你是Java技术栈,LangChain4j是更合适的入口。热词里有人搜"langchain4j easy rag",这确实是Java生态里目前最顺手的RAG库,内置了文档加载、拆分、嵌入、检索的整套流程。
提示:框架只是工具,别被框架绑住。RAG的核心链路始终就那几步:加载、拆分、嵌入、存储、检索、生成。任何框架出了问题,沿着这条链路排查比翻文档快得多。
3. 文档接入与文本拆解:效果好不好,一半看这里
3.1 常见格式怎么解析
企业知识库里的文档格式五花八门,PDF、Word、PPT、Excel、扫描件,还有大量的Markdown和HTML。PDF解析是这里最大的坑——很多PDF看起来是文字,实际上是图片或者扫描件,直接按文本提取会得到一堆空字符串或者乱码。
我推荐的方案是这样:
- PDF文字版:先用pdfplumber或PyMuPDF(fitz)提取文字。PyMuPDF速度快,几百页的PDF几秒钟就能读完。
- PDF扫描版:需要走OCR流程。开源方案是PaddleOCR,中文识别效果好,配合图像预处理(灰度化、二值化、去噪点)以后准确率能达到90%以上。这一步属于重活,建议单独建一个任务队列跑,别阻塞在主流程里。
- Word/PPT:python-docx和python-pptx就能处理,需要注意提取表格时要保留结构,最好拼成Markdown表格再切分,这样大模型能理解行列关系。
- HTML/网页:BeautifulSoup提取正文,去菜单模板噪音。
3.2 chunk_size与overlap的设计原理与推荐参数
文本拆解是整个RAG链路里最需要手工调的一部分。拆得好不好,直接决定检索能不能命中。拆得太长,向量化后语义容易混杂多个主题,检索时相似度被稀释;拆得太短,上下文不完整,大模型回答时缺乏背景。
我用的组合方案是"递归字符拆分器"(LangChain里的RecursiveCharacterTextSplitter),它按照段落→句子→单词的优先级依次尝试切分,尽量保持语义完整性。关键参数是chunk_size和overlap。中文场景我推荐chunk_size取400~600个字符(不是token),overlap取80~120个字符。
为什么是400~600而不是更长?因为很多嵌入模型(比如text2vec)对输入长度有512 token的限制,中文一个字大概对应0.7~1.5个token,你设个1000字符的chunk,向量化时会被截断,后面的信息直接丢失了。BGE-M3支持8192 token,但长chunk也会带来检索噪音。400~600字符既能保证主题相对单一,又能让嵌入模型完整编码。
3.3 中文场景拆解的坑:标点、代码、表格、长文档
中文文本拆解有几个特有的坑,文档里不会写,但踩一次就记住了。
第一个坑是标点。中文里句号、问号、感叹号都是天然的切分点,但顿号和分号很容易被忽略。如果你用的是按字符长度硬切的拆分器,很可能把一个句子的主语和宾语切断,向量化之后语义残缺。递归拆分器会优先按段落切,这个配置一定要主动开启"按段落优先"。
第二个坑是代码片段。技术手册里经常有配置代码块,这些内容不能被切成碎块,否则检索时语法全乱。我建议在拆分前先识别出代码块,把它们单独拎出来作为一个chunk,或者用特定的分隔符保护起来,不参与递归切分。
第三个坑是表格。直接把表格转成纯文本会丧失行列结构。我当时预处理的时候把表格转成"键值对"格式——比如列头是"产品编号"对应值"ABC-123",一行转成一个键值串,检索时大模型能看明白对应关系,效果比纯文本好得多。
第四个坑是长文档的章节层级。100页的文档如果按固定长度硬切,很容易把"第3章"的内容切到"第2章"的chunk里。这里有两种处理方式:一是前置用文档标题做主线切分,先按章节层级把文档拆成若干大段,再在每个大段里按chunk_size细分;二是做父子chunk结构(后面检索优化部分详讲)。我强烈推荐第一种,实现简单,效果立竿见影。
3.4 本地文本拆解工具推荐
热词里有人搜"本地的rag文本拆解工具",这里补充一下。如果你不想自己写拆分逻辑,可以用LangChain提供的现成拆分解(HTMLSectionSplitter、MarkdownHeaderTextSplitter),也可以直接用Unstructured库,它能统一解析PDF、Word、HTML等格式,配合分区识别(partition_pdf、partition_docx)效果很不错。还有一个轻量的工具叫Tika,Apache出的,Java和Python都能调,解析各种文档格式很省心。
我的组合是:PyMuPDF处理PDF + Unstructured处理复杂文档 + 自定义的递归拆分器做最终切分。这套组合在本地完全离线运行,数据不会出内网。
4. 核心链路代码:从文档到问答的完整实现
4.1 环境准备与依赖安装
先说环境。我用的Python 3.10版本,依赖主要涉及LangChain生态和Ollama的客户端库。安装命令如下:
pip install langchain langchain-community langchain-chroma pip install chromadb pip install ollama pip install pymupdf pdfplumber unstructured python-docx pip install pypdf本地模型的部署用Ollama,下载并安装后运行:
ollama pull qwen2.5:7b-instruct ollama pull bge-m3这里需要说明一下:bge-m3可以作为嵌入模型在Ollama里运行,Ollama支持将模型作为embeddings接口调用,LangChain里对应的类是OllamaEmbeddings。qwen2.5:7b-instruct负责最后的答案生成。
4.2 文档加载与拆分的代码实现
下面是一套我实际在用的核心代码,为了这篇文章做了精简,但主干逻辑都保留了。
import os from langchain_community.document_loaders import PyMuPDFLoader, UnstructuredWordDocumentLoader from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain_community.embeddings import OllamaEmbeddings from langchain_community.vectorstores import Chroma # 1. 文档加载 doc_path = "./docs/设备运维手册.pdf" loader = PyMuPDFLoader(doc_path) documents = loader.load() # 2. 文本拆分:chunk_size和overlap都是经验值,根据文档类型调整 text_splitter = RecursiveCharacterTextSplitter( chunk_size=500, chunk_overlap=100, separators=["\n\n", "\n", "。", "!", "?", ";", ",", " ", ""], length_function=len, ) chunks = text_splitter.split_documents(documents) print(f"拆分后片段数量: {len(chunks)}") # 3. 嵌入模型与向量库 embeddings = OllamaEmbeddings(model="bge-m3") vectorstore = Chroma.from_documents( documents=chunks, embedding=embeddings, persist_directory="./db/chroma_db" ) print("向量库写入完成")这段代码有几个细节值得展开。第一,separators的顺序不是随意的,它决定了递归拆分时优先按什么切分。我先按段落、再按换行、再按句号等标点,这样才能尽量保住语义完整性。第二,length_function=len表示按字符数计算长度,如果你希望按token数切分,需要换成一个tokenizer函数,比如:
def token_len(text): return len(ollama_tokenizer.encode(text))但本地部署常用字符数,简单直接,误差不大。
4.3 向量化入库与检索问答的代码实现
录入完成后,就可以做检索问答了。这里我建议先把检索器单独拉出来,方便后面测试不同的检索策略:
# 4. 检索器与问答链路 retriever = vectorstore.as_retriever( search_type="similarity", search_kwargs={"k": 5} # 返回Top-5片段 ) from langchain.chains import RetrievalQA from langchain_community.llms import Ollama llm = Ollama( model="qwen2.5:7b-instruct", temperature=0.1, num_ctx=4096, ) qa_chain = RetrievalQA.from_chain_type( llm=llm, retriever=retriever, chain_type="stuff", # 把所有检索片段一次性塞进Prompt return_source_documents=True, ) query = "这台设备的保养周期是多少?" result = qa_chain({"query": query}) print("回答:", result["result"]) print("来源片段:", result["source_documents"])这段代码是我最初版本的雏形,但它已经能回答大部分问题了。chain_type="stuff"适合检索片段较少的情况(Top-5),片段多了之后上下文装不下,就需要换map_reduce或refine策略,或者把无关片段过滤掉。这里先不展开,检索优化章再细说。
4.4 Prompt设计:如何让大模型只依据给定资料回答
如果你直接跑上面的代码,会发现有时候模型还是会胡说,或者回答问题时不引用原文。问题出在Prompt上。LangChain默认的Prompt太开放了,没约束模型必须基于检索资料回答。
我用的Prompt模板是这样:
from langchain.prompts import PromptTemplate prompt_template = """ 你是一个企业内部知识库的问答助手。请严格基于下面提供的参考文档回答问题。 要求: 1. 如果参考文档中能找到答案,请直接回答,并标注对应的文档来源。 2. 如果参考文档中找不到答案,请明确回答"根据现有资料无法回答",不要自行编造。 3. 回答时尽量使用简洁、准确的语言,涉及数据或参数时不要修改原文。 参考文档: {context} 问题:{question} """ PROMPT = PromptTemplate( template=prompt_template, input_variables=["context", "question"] ) qa_chain = RetrievalQA.from_chain_type( llm=llm, retriever=retriever, chain_type="stuff", return_source_documents=True, chain_type_kwargs={"prompt": PROMPT}, )这里我加了两个硬性约束:第一是"只能依据参考文档回答",第二是"找不到就说找不到"。这两条极大减少了幻觉,也让回答有了可追溯性。实际使用中,员工看到回答下方配的引用来源,对系统的信任度会高很多。很多RAG系统上线后被吐槽"答案不靠谱",一半以上是Prompt约束不够导致的。
5. 检索质量调优:hit rate低、答非所问的排查与优化
5.1 先量化:什么是hit rate,怎么算
跑通链路之后,接下来要做的事就是量化检索质量。热词里有人搜"rag hit rate",这是个特别关键的指标。我定义hit rate的方式很简单:对一批预先标注好的问题(每个问题对应1~2段正确文档片段),用你的检索器去Top-5里捞,如果正确答案在里面,就算命中。总命中数除以问题总数就是hit rate。
我举一个实际数字。第一批测试我准备了60个内部问题,初始hit rate只有58%。也就是说将近一半的问题,Top-5检索结果里根本没有正确答案,后面给大模型吃再好的Prompt也没用——资料里没这内容,它只能瞎编。所以调RAG系统,第一步不是调模型和Prompt,而是先把hit rate提到85%以上。
5.2 hit rate低的常见原因与排查链路
hit rate低的时候,我有固定的排查顺序,建议你按这个链路走:
- 查看检索返回的片段和问题到底差多远。把问题、Top-5片段原文打印出来,人肉判断相似度。如果片段明显相关但不准确,说明chunk切分有问题;如果片段完全不相关,说明嵌入模型或检索策略有问题。
- 检查chunk是否被切断在不合适的位置。如果正确答案明明在文档里,但被切成两半,每半都不完整,检索时相似度自然低。这种情况把重叠区调大,或者换用按标题层级切分。
- 确认query的措辞和文档内的措辞差异大不大。比如文档里写的是"空压机"而用户问的是"空气压缩机",向量检索对这种同义词匹配很吃力,尤其是embedding模型不够强的时候。这种情况可以用下面的查询改写方案解决。
- 检查是否命中了错误片段。有时候Top-5里确实有正确内容,但排在第6、7,也在视野之外。这说明别的无关片段相似度更高,可能是chunk太长导致语义混杂了。
5.3 混合检索与重排序:最有效的两板斧
我把这两板斧单独拿出来说,是因为它们解决了我项目里八成以上的检索问题。
第一板斧是混合检索。向量检索擅长语义匹配,但它在精确关键词匹配上反而不如传统的BM25算法。比如用户搜"型号XYZ-2000的操作规程",向量检索可能把"XYZ-2000"和"操作规程"拆开理解,反而找了一堆其他型号的内容。BM25按词频和稀有度打分,对这类精确词很敏感。混合检索就是把向量检索和BM25的结果合并,再做归一化重排。
LangChain里可以这么实现:
from langchain.retrievers import BM25Retriever, EnsembleRetriever bm25_retriever = BM25Retriever.from_documents(chunks) bm25_retriever.k = 5 vector_retriever = vectorstore.as_retriever( search_type="similarity", search_kwargs={"k": 5} ) ensemble_retriever = EnsembleRetriever( retrievers=[bm25_retriever, vector_retriever], weights=[0.3, 0.7] # BM25权重低一点,语义检索主导 ) qa_chain = RetrievalQA.from_chain_type( llm=llm, retriever=ensemble_retriever, chain_type="stuff", return_source_documents=True, chain_type_kwargs={"prompt": PROMPT}, )我这里BM25给0.3的权重,偏重语义。如果你的文档里精确型号、编号居多,可以适当调高BM25权重到0.5。这个权重没有绝对标准,拿测试集多跑几组对比就行。
第二板斧是重排序(Rerank)。混合检索完了,Top-5里有5个片段,其中可能只有2个相关。如果直接把5个全塞给大模型,无关片段会造成噪音,甚至误导回答。重排序的思路是:先粗筛出候选(比如Top-20),再用一个专门的交叉编码器(cross-encoder)模型对每个候选和问题做精细的相关度打分,最后取分数最高的Top-3~5。
中文场景推荐用bge-reranker-base或bge-reranker-large。我记得当时引入重排序之后,hit rate从76%直接跳到90%,这是整个调优过程中提升最明显的一步。LangChain里用法如下:
from langchain_community.cross_encoders import HuggingFaceCrossEncoder from langchain.retrievers import ContextualCompressionRetriever from langchain.retrievers.document_compressors import CrossEncoderReranker reranker = HuggingFaceCrossEncoder(model_name="BAAI/bge-reranker-base") compressor = CrossEncoderReranker(model=reranker, top_n=3) compression_retriever = ContextualCompressionRetriever( base_compressor=compressor, base_retriever=ensemble_retriever, ) qa_chain = RetrievalQA.from_chain_type( llm=llm, retriever=compression_retriever, chain_type="stuff", return_source_documents=True, chain_type_kwargs={"prompt": PROMPT}, )这一步做完,之前5个片段直接给模型的模式变成了"先捞20个,再精挑3个",模型吃进去的资料更干净,回答质量自然上去。代价是多了一次交叉编码器的计算,单次查询大概增加几十毫秒到一两百毫秒,对于企业内部问答这个延迟完全可接受。
5.4 检索优化的进阶手段:查询改写、父子文档与元数据过滤
混合检索和重排序是基础套餐,如果hit rate还是不够,还有几个进阶手段可以叠加。
查询改写是解决"用户问法和文档表述不一致"最直接的方式。比如用户问"这台设备多久保养一次",直接检索"多久保养一次"效果一般,但如果你先用大模型把问题改写成更贴近文档的表述——"设备保养周期是多少?是否有保养规程?"——检索命中率会明显提高。实现上就是在检索前加一个LLM调用,这一步虽然增加了一次推理开销,但在复杂知识库里性价比很高。
父子文档策略是处理"检索到了相关内容但上下文不够"的好办法。做法是:索引时同时存两个层级的chunk——小的子chunk用于匹配(比如200字符),大的父chunk用于喂给大模型(比如2000字符)。检索时先用小子chunk向量化匹配,命中后把对应的父chunk整个作为上下文送给模型。这样做的好处是:小chunk检索精度高,不掺杂物;大chunk上下文完整,不缺背景。尤其在设备手册这种"操作方法"和"警告说明"常常跨章节呼应的场景里特别有效。
元数据过滤更适合结构化知识库。比如你已经把文档按"产品线""文档类型""发布时间"打了标签,检索时就先根据用户问题判断该查哪个产品线,比如先限定"产品线=注塑机",再在过滤结果里做相似度检索。这样检索空间从全库缩小到某一类文档,速度和准确率都会好很多。
如果这些手段都试过,hit rate还在80%以下,我建议你回头检查嵌入模型和chunk_size——大概率是基础配置出问题了。进阶手段救不了底层配置的坑。
6. 从基础RAG向前一步:GraphRAG、本体RAG与Agentic RAG
6.1 知识割裂是怎么发生的,GraphRAG为什么能改善
先讲一个我在项目里遇到的实际问题:有一次用户问"设备A的电气系统和液压系统之间怎么联动",检索器分别找到了电气系统的操作手册和液压系统的操作手册,答非所问且内容割裂——两个系统明明在一个章节里有联动的描述,但因chunk切分把那段描述分开了,而且关键词不匹配,没能检索到。
这就是RAG的"知识割裂"问题:文档被切成小块后,小块之间原本存在的关联关系就丢了。热词里有"解决知识割裂 rag"这串字,说明这不是我一个人的痛点,而是做RAG的人都绕不过去的问题。
GraphRAG的解决思路是:不仅存文本向量,还要在建索引时做一层实体关系抽取——把文本里的"实体"(设备、部件、参数、流程)提出来,再把"关系"(A包含B、A控制C、A的转速范围是X)也提出来,存成一张知识图谱。检索时,如果用户问的关系型问题命中图谱子图,就把相关实体和关系一并作为上下文送给模型。这样一来,文档之间的联系被显式建模,知识割裂问题从根上就缓解了。
GraphRAG的实现并不像听起来那么遥远。微软开源的GraphRAG项目可以直接在本地部署,它的做法是先让大模型抽取实体关系构建图,再对社区做总结,最后针对问题在图上召回。缺点是构建索引的token消耗很大,适合文档量中等(比如几十份)但关联密集的场景。
6.2 本体RAG:领域结构化约束的价值
热词里还有"ontology rag"和"rag graphrag llm wiki 本体rag",说明关注的人不少。本体RAG(Ontology RAG)和GraphRAG的区别在于:GraphRAG的图谱是"从数据里自动抽出来的",关系可能比较泛化;本体RAG则预先定义一个领域模型,比如"设备-部件-参数-维护记录"这套概念体系,然后让抽取过程严格按这些预定义的类型和关系走。
打个比方,GraphRAG像让一个新人自己去理解文档里的关系然后画关系图,可能画得乱七八糟;本体RAG是在画之前有人告诉他"你只需要画设备、部件、参数、记录这四类节点,关系只能是包含、属于、关联这三种"。结构化程度更高,召回更可控。
我在一个航空维修项目里试过本体RAG:先让业务专家定义了"飞机-系统-部件-故障现象-维修措施"的本体,再按这个本体抽取文档内容。结果检索精度大幅度提升,因为查询里有任何设备型号或部件名称,就能在图上沿着确定的关系路径走到对应的维修措施,而不是靠文本相似度碰运气。但代价也很明显:前期建本体需要业务专家参与,不是纯技术活,投入不小。
6.3 Agentic RAG:让模型自己决定怎么查
基础RAG的模式是"一问一检一答",只有一个检索动作。Agentic RAG则是把大模型当成一个智能体(Agent),它可以根据问题的复杂性自行决定:是先检索一次,还是拆分成多个子问题分别检索,或者检索一次不满意再换关键词检索一次,甚至可以选择调用外部工具(比如查数据库、查API)。
热词里的"rag智能体"和"agentic rag"指的就是这个方向。它的典型场景是复合问题,比如"去年Q3所有设备故障中,注塑机占比多少?"这种问题需要先检索故障记录,再做一个统计计算,光靠单次文本检索+生成是答不好的。Agentic RAG里,大模型可以先把问题拆成两条检索路径,分别查,再把结果合并计算。
LangChain里实现Agentic RAG其实已经有成熟框架了,核心是构建一个create_retriever_tool然后塞进AgentExecutor。这个方案的优势是灵活,缺点是引入了大模型自身的判断能力,执行链路变长、token消耗变多。我的经验是:如果基础RAG的hit rate已经稳定在90%左右,再考虑Agentic RAG提升体验;如果基础RAG还没调好,先别碰。
6.4 什么时候才需要考虑这些进阶方案
每次听到别人说"我要直接上GraphRAG"时,我都会先问一个问题:你现在的基础RAG,hit rate多少?如果还没有用混合检索和重排序,那我劝你先别急着上图谱。GraphRAG的索引构建成本是普通文本向量的十倍以上,而且调试复杂度高,模型抽取实体关系的质量不稳,后期维护成本不低。
我的建议是分三个阶段走:第一阶段做好基础RAG(文本拆分+向量检索+Prompt约束),目标是hit rate达到80%;第二阶段上混合检索+重排序+父子文档,目标90%以上;第三阶段,如果你的文档确实存在强关联、强层级关系,且用户问的问题很多跨章节,这时候再引入GraphRAG或本体RAG。把每个阶段的目标量化清楚,就不会在技术选型上迷失。
收尾:几个我踩过的坑和经验
最后分享几个我实际落地过程中总结出来的体会,不一定都在正文里,但肯定对你的项目有帮助。
第一,文档质量比算法重要得多。同一个RAG系统,喂经过整理的Markdown文档和喂一堆扫描版PDF,效果天差地远。我后来专门安排了一个实习生做人肉文档清洗,把重点文档转成统一的Markdown格式,效果直接翻倍。别迷信算法,先把数据源弄干净。
第二,运维侧要留心增量更新。企业文档不是一次性导入就完事的,经常要新增或者修改。我当时给系统写了一个同步模块,定期扫描文档目录,计算文件hash,只有变化的部分才重新切分和嵌入。这个模块看着不起眼,但半年后的效果是:知识库一直保持最新,员工查到的永远是当前版本的流程。忘了做增量更新的话,系统会越来越不准。
第三,不要追求"一步到位"的完美架构。我见过很多团队一上来就想搭Milvus集群、部署三台GPU、上GraphRAG,结果三个月还没上线。真正应该做的是先用轻量方案把一个部门的知识库跑通,让业务人员用起来,有了真实反馈再迭代。RAG系统的价值和架构复杂度不是线性的,先把最小可用产品做出来,永远是对的。
如果你正准备在企业里落地私有知识库,我建议你保存好这篇文章里的代码骨架,从一个小部门的三五十份文档开始,先把hit rate调明白,再谈扩展。这套路我走过,它不炫技,但确实稳妥。