让我们先把这个技术路线想清楚。最近总有人问我:RAG都火了这么久了,怎么还要和Agent一起提?直接拿向量数据库检索不就行了?我的回答是:如果你只是做一个“查文档问答机器人”,纯RAG确实够用;但如果你想让它自动规划、主动调工具、多步推理、从多个数据源交叉验证信息,那RAG只是Agent的一只“手”,真正的大脑还在Agent的规划能力里。这篇文章就从一个已经跑过完整项目的博主视角,把AI Agent和RAG结合起来,手把手拆解打造专属知识库的全过程。
适合三类人看:一是正在做个人知识库、想引入AI问答能力的效率工具党;二是企业里要做内部文档问答系统、但不想上来就搞大模型的工程师;三是对LangChain/LlamaIndex/Dify这些框架有兴趣、想理解背后原理的学习者。我会把向量化流程、检索优化、HyDE、Graph RAG、Agentic RAG这些热点词都串起来讲,最后给一套可以直接“抄作业”的落地配置。
1. 内容整体设计与思路拆解
1.1 为什么纯RAG不够用,Agent才是关键
先坦白讲一个问题:纯RAG的召回质量很不稳定。你做知识库问答,最怕的不是模型答不出,而是它“一本正经地胡说八道”——检索回来的片段没逻辑关联,或者多个问题叠加在一起时,检索器不知道到底该找哪段话。
这个时候Agent的价值就出来了。Agent干的事情是“拆问题”:用户问“我们公司过去三个季度的销售数据怎么样,其中华南区增长最快的是哪个产品线?”这个query直接扔给检索器,召回质量大概率是崩的。但Agent可以先拆解成三步:
- 第一步,查销售数据汇总表;
- 第二步,筛选华南区数据;
- 第三步,对比产品线增长率并排序。
每一步调用一次RAG检索,而且可以把上一步的结果当作下一步检索的上下文。这就是热词里经常提到的“Agentic RAG”——检索不再是单次动作,而是被Agent编排进整体规划流程中的可控子任务。
从架构上看,RAG给Agent提供了“记忆的来源”,Agent给RAG提供了“决策的骨架”。两者结合之后的整体设计,通常分三层:
- 数据层:处理原始文档,完成解析、清洗、切分、向量化,落地到向量数据库。
- 检索层:负责召回,包括向量检索、关键词检索、混合检索、重排序。
- Agent层:负责解析用户意图、编排检索动作、组装上下文、调用LLM生成最终答案。
这个分层看起来简单,但每层都有无数个“魔鬼细节”。接下来我逐个拆。
1.2 架构选型:LangChain、LlamaIndex还是Dify
很多新手第一个问题就是:那我该直接用哪个框架?我先把我试过的三个方案实事求是的说一下。
LangChain是通用型Agent编排框架,生态最全,文档也多,但版本演进太快,API经常breaking change,如果你要生产级稳定性,得花时间锁版本。它的优势在于自由度极高——你可以用LCEL(LangChain Expression Language)把检索、prompt、模型、输出解析串成一条chain,也可以自建Agent Loops。
LlamaIndex则是更贴近“知识库场景”的选择,它在索引结构和检索细节上做得比LangChain更深,比如内置了各种Node Parser、Metadata Extractor、SubQuestionQueryEngine,非常适合以文档为中心的RAG应用。
Dify则是平台化的代表,属于“低代码/可视化”路线,热词里也提到了Dify知识库流水线、以及Dify升级后无法保存知识库等问题。Dify适合团队协作、需要快速迭代的场景,它的知识库管理界面、文档分段策略、召回测试工具都是现成的,但定制能力受限,尤其是你想要实现自定义Agent中间步骤时,反而会绕一些弯。
我的推荐结论是——个人项目或学习用,优先LangChain;纯文档问答、要精细控制检索质量,LlamaIndex更好;团队需要快速交付,选Dify,但要做好被平台限制的心理准备。当然,你也可以在Dify里挂外部Agent服务,但这就涉及平台能力边界了。
1.3 部署路线:本地模型还是API模型
知识库场景里还有一个绕不开的问题:LLM用云上API还是本地部署?这直接决定数据隐私、成本和技术复杂度。
如果你的文档是公开资料、不涉密,直接用OpenAI、Claude或国产大模型API都行。优点是效果稳定、免运维;缺点是所有上下文都要发到外部,数据安全是个隐患。企业内部合规部门那关很难过。
如果你处理的是内部技术文档、客户信息等敏感数据,建议走本地部署路线。现在主流选择是Ollama + Qwen2.5 / Llama 3.1 / GLM-4等开源模型,显存需求大概在7B模型需要8GB显存起,14B模型建议16GB以上。
我个人的实践是“双轨制”:开发调试阶段用API模型,速度足够快,方便调prompt;上线到内网环境再切本地模型,配合vLLM做推理加速。这样两者优点都能拿到,环境切换只需要改一个base_url,非常方便。
2. 核心细节解析与实操要点
2.1 文档解析与数据清洗,决定RAG上限
有一个被严重低估的环节——数据清洗。很多人做RAG,上来就把PDF往LangChain里一扔,文档加载器报个错就换库重试,结果召回效果差也不知道问题出在哪。
数据层最重要的原则是:检索结果的质量,受文档解析质量的制约。如果你喂进去的PDF是扫描件,那就不能直接用PyPDFLoader,得先走OCR流程。如果文档里有复杂的表格结构,传统的文本解析会把表格拍扁成一行行无意义的字符,检索的时候根本对不上。
我把数据清洗拆成四个步骤:
- 格式转换:PDF/Word/PPT统一转成Markdown,便于后续切分和保留结构。工具上推荐MinerU、PaddleOCR处理扫描件,docling处理Word类文档。
- 结构标准化:统一标题层级,去除页眉页脚、页码、重复文本,保留列表信息。
- 语义比较完整的切块:这一点后面单独说,切分是召回效果最直观的影响因素之一。
- 元数据留档:记录来源文件名、章节路径、更新日期,便于召回后做引用溯源。
有人在热词里还提到了“卡帕西知识库”,应该是指Karpathy的推文或视频内容整理成知识库的案例。这类个人知识库的文档往往来自多个平台——YouTube字幕、Twitter文本、博客长文,格式差异极大,更需要在清洗阶段统一样式。
2.2 向量化流程与Embedding模型选型
热词里有一组“rag向量化流程”,这个必须讲透。向量化不是把一段文字塞给模型就能完事的,整个流程是:
- 文本切块(splitting)
- 向量化(embedding)
- 入库(indexing)
- 检索(retrieval)
**先讲切块。**切分太大会导致信息混杂,比如一个chunk里有两三件完全不同的事,query只问其中一件,那这个chunk的整体向量会“被平均”,被迫偏离了用户的真实意图;切分太细又会让上下文破碎,比如一个表格被切成几十个碎片,检索回来缺上下文,LLM理解不了。
我的切块参数经验值,以中文场景为主:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| chunk_size | 500~800字符 | 对中文来说大约300~500字,过长会稀释向量 |
| chunk_overlap | 50~150字符 | 保留上下文连接,防止信息断档 |
| 分隔符优先级 | 段落 > 标题 > 换行 > 句号 | 优先按结构切,最后才按长度硬切 |
以LangChain为例,用RecursiveCharacterTextSplitter:
from langchain.text_splitter import RecursiveCharacterTextSplitter text_splitter = RecursiveCharacterTextSplitter( chunk_size=600, chunk_overlap=100, separators=["\n\n", "\n", "。", "!", "?", ".", "!", "?", ";", ";", ",", ","], length_function=len, ) chunks = text_splitter.split_text(document_text)**再讲Embedding选型。**我实际对比过几款常见的Embedding模型,包括text-embedding-3-small、bge-large-zh-v1.5、m3e-base、bge-m3和Cohere的embed-v3。就中文场景而言,BGE系列明显能打,尤其是bge-m3,支持中文、英文、代码等多语言混合,检索效果很稳。
有一款我最近很喜欢,就是Qwen3-Embedding-0.6B/4B/8B系列,在多个中文检索任务上表现很突出,如果你想本地部署、又不想在效果上妥协太多,这个是很值得尝试的选择。
关于维度,一般中文模型输出1024维或768维就够用了,不需要迷信1536维或更高。向量维度直接影响存储占用和检索延迟,高维度未必带来更高精度。
2.3 Dense Vector Search的真相:余弦相似度与Top-K
热词里有“rag中dense vector search”,翻译过来就是稠密向量检索。它的核心逻辑是把你query用一个Embedding模型转成向量,然后在向量数据库里找与这个query向量最相似的doc向量。衡量相似度常用余弦相似度(CosSim)或欧氏距离。
原理上你只需要记住一句话:向量是语义坐标,检索是坐标近邻。同为“如何提升手机续航”这个问题,“手机电量掉得快怎么办”这种query可能比“手机电池健康度如何维护”在向量空间里离得更近。所以找回什么、找回的顺序如何,不完全是“词面匹配”,而是“语义空间匹配”。
实际操作中,Dense Search有个问题:它对专有名词、缩写、代码变量名非常不敏感。比如你知识库里存了很多“A800”相关的文档,用户问“英伟达的卡可以用吗”,向量检索很可能找不到“A800”。这种场景就需要混合检索——把关键词检索(BM25)和向量检索结合起来,最后再做Rerank重排。
我的做法是:先并行召回,取并集,再用Rerank模型重排,选出前5~8个片段。这套流程在很多评测里能把命中率从六成提到九成以上。在LangChain里可以组合实现:
from langchain.retrievers import BM25Retriever, EnsembleRetriever from langchain.vectorstores import Chroma from langchain.embeddings import HuggingFaceBgeEmbeddings embeddings = HuggingFaceBgeEmbeddings(model_name="BAAI/bge-m3") vectorstore = Chroma.from_documents(docs, embeddings) bm25_retriever = BM25Retriever.from_documents(docs) bm25_retriever.k = 10 vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 10}) ensemble_retriever = EnsembleRetriever( retrievers=[bm25_retriever, vector_retriever], weights=[0.3, 0.7] )这里权重[0.3, 0.7]代表BM25和Dense Search的召回比例,实际调参时可以观测不同权重下的命中率变化。我常见的最优区间是BM25占0.2~0.4,向量检索占0.6~0.8,因为大多数场景还是语义为主、词面为辅。
2.4 Rerank:决定提问质量的最后一公里
排序模型Rerank的作用,是把混合召回的结果重新打分排序。它不是另一个Embedding模型,而是一个跨编码器(Cross-Encoder),它会直接拿query和document拼在一起做深度交互,所以精确度高很多,但计算量也大,不适合全库检索,只适合对少量候选做重排。
推荐两个实用的Rerank模型:bge-reranker-v2-m3(中文效果好、支持超过100万token的输入长度)和Cohere Rerank(API调用,非常简单)。在LangChain里接入:
from langchain.retrievers import ContextualCompressionRetriever from langchain.retrievers.document_compressors import CrossEncoderReranker from langchain_community.cross_encoders import HuggingFaceCrossEncoder model = HuggingFaceCrossEncoder(model_name="BAAI/bge-reranker-v2-m3") compressor = CrossEncoderReranker(model=model, top_n=5) compression_retriever = ContextualCompressionRetriever( base_compressor=compressor, base_retriever=ensemble_retriever )这里top_n=5表示重排后只保留前5个片段。如果你最终给LLM的上下文窗口不大,保留5~8个是常见配置。重排之后检索质量会有肉眼可见提升,尤其是在知识库文档多、主题杂的时候。
3. 实操过程与核心环节实现
3.1 实战案例:用LangChain搭建一个“个人知识库”Agent
现在进入可以动手的部分。下面这个项目,我命名为agentic-kb,是我在自己机器上跑过多次的最终版本,你可以直接参考。核心流程见代码注释,细节我在代码后面逐一解释。
项目目录:
agentic-kb/ ├── data/ # 原始文档 ├── src/ │ ├── ingest.py # 文档加载与向量化 │ ├── agent.py # Agent编排与检索 │ └── config.py # 模型与参数配置 ├── requirements.txt └── main.py第一步:向量化入库(ingest.py)
import os from langchain_community.document_loaders import DirectoryLoader, TextLoader from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain_community.embeddings import HuggingFaceBgeEmbeddings from langchain_community.vectorstores import Chroma # 1. 加载目录下所有txt/md文档 loader = DirectoryLoader("./data/", glob="**/*.txt", loader_cls=TextLoader) docs = loader.load() # 2. 按语义结构切块 splitter = RecursiveCharacterTextSplitter(chunk_size=600, chunk_overlap=100) chunks = splitter.split_documents(docs) # 3. 初始化embedding模型 embeddings = HuggingFaceBgeEmbeddings( model_name="BAAI/bge-m3", model_kwargs={"device": "cuda"}, encode_kwargs={"normalize_embeddings": True}, ) # 4. 存入Chroma向量库 vectorstore = Chroma.from_documents( documents=chunks, embedding=embeddings, persist_directory="./chroma_db", ) print(f"已入库 {len(chunks)} 个文档块")第二步:Agent编排(agent.py)
这里我不用复杂的Agent框架,而是用LangChain的create_retrieval_chain+create_history_aware_retriever做一个最容易被理解的版本:它有一个“重写query”的中间步骤,用户提问时,Agent会结合历史会话把模糊query改写为具体的检索query,再去知识库里检索。
from langchain_community.vectorstores import Chroma from langchain_community.embeddings import HuggingFaceBgeEmbeddings from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder from langchain.chains import create_history_aware_retriever, create_retrieval_chain from langchain.chains.combine_documents import create_stuff_documents_chain from langchain_openai import ChatOpenAI # 1. 加载已有向量库 embeddings = HuggingFaceBgeEmbeddings(model_name="BAAI/bge-m3") vectorstore = Chroma(persist_directory="./chroma_db", embedding_function=embeddings) retriever = vectorstore.as_retriever(search_kwargs={"k": 8}) # 2. 用于“重写query”的prompt contextualize_prompt = ChatPromptTemplate.from_messages([ ("system", "根据聊天历史和最新用户问题,提炼出一个不依赖历史就能独立检索的查询语句。"), MessagesPlaceholder("chat_history"), ("human", "{input}"), ]) # 3. 用于“生成答案”的prompt answer_prompt = ChatPromptTemplate.from_messages([ ("system", "你是一个知识库问答助手。仅根据以下资料回答问题,不要编造资料中没有的内容:\n\n{context}"), MessagesPlaceholder("chat_history"), ("human", "{input}"), ]) # 4. 初始化LLM llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.1) # 5. 构建Agent/RAG链路 history_aware_retriever = create_history_aware_retriever(llm, retriever, contextualize_prompt) question_answer_chain = create_stuff_documents_chain(llm, answer_prompt) rag_chain = create_retrieval_chain(history_aware_retriever, question_answer_chain)第三步:启动入口(main.py)
from src.agent import rag_chain if __name__ == "__main__": q = "我们团队协作指南里关于代码评审的要求有哪些?" result = rag_chain.invoke({"input": q, "chat_history": []}) print("答案:", result["answer"]) print("引用来源:") for doc in result["context"]: print("-", doc.metadata.get("source", "unknown"))这套链路里我认为最有价值的是create_history_aware_retriever这个中间层。因为用户在实际问答中不会每次都把上下文说全,比如先问“Q2财报里毛利率是多少”,再问“和Q1相比呢?”第二个问题如果不结合历史,检索器根本不知道要搜什么。加了重写步骤后,Agent会把“和Q1相比呢”重写成“Q2财报毛利率与Q1的对比”,检索质量立刻不一样。
3.2 用Dify快速搭一个可视化知识库流水线
如果你不想写代码,Dify是目前最省事的方案。它在热词里也出现得很多——Dify知识库流水线,Dify升级后无法保存知识库等问题。我自己的Dify实践心得如下。
Dify创建知识库的流程大致是:
- 创建知识库:上传文档,支持PDF、TXT、Markdown等格式,也可以连接Notion、网站同步。
- 分段设置:Dify有自动分段和自定义分段两个选项。自动分段按段落和语义切块,自定义分段需要自己设置标识符,比如
\n换行符分割。 - 索引方式:Dify支持高质量(使用Embedding)、经济(关键词索引)和混合(向量+全文)三种模式。知识库问答场景必选高质量或混合模式。
- 召回测试:这是Dify最好用的功能——随时用一句话测试当前知识库的召回结果,观察Hit Testing里返回了哪些chunk,快速判断索引效果。
Dify的Agent能力也很直白:在“编排”页面,你可以给Agent配置多个工具节点,其中“知识检索”就是一个典型的RAG节点。你可以同时配多个知识库,Agent根据用户问题自动选择调用哪个库。这其实就是最简版的多知识库Agent路由。
但Dify的坑也要提醒:我遇到过升级后保存知识库报internal server error,网上搜了一圈,多数是版本升级后旧的向量索引字段不兼容。解决办法是备份后用Docker重新初始化数据库,然后重新做一次索引。说白了,平台化工具虽然省事,但一旦出问题排查路径很长,你得有一定的Docker和PostgreSQL/Weaviate基础。
3.3 更进一步:Graph RAG与Ontology RAG
热词里出现了graph rag和ontology rag,包括agentic rag,这几个词确实代表了RAG发展的几个方向。
Graph RAG的核心思路:传统RAG把文档切成互不相干的chunk,然后靠向量相似度去匹配,但文档中的实体和关系被完全打散了。Graph RAG则是先通过LLM抽取文档中的实体和关系建图,检索时沿着图谱路径进行多跳查询。举个实际例子,你问“张三的团队里谁负责过和李四合作的项目”,传统RAG只能靠关键词“张三”“李四”去召回,运气成分很大;Graph RAG可以直接从图谱中查询张三-属于-团队X-成员-李四-合作项目-项目Y的路径,答案确定性强得多。
Ontology RAG则是更“规则化”的Graph RAG。它要求先定义一套知识本体,比如“人、项目、公司、职位、合作关系”这些类型以及它们之间允许的关系边,然后用LLM按这个本体模板抽取信息。好处是知识库变得可查询、可约束、可推理;代价是需要提前做领域建模,配置成本高。
Agentic RAG把Agent引入检索决策链。不只做一次检索,而是允许Agent在回答过程中多次检索、交叉验证、甚至调用不同的外部API。比如用户问“帮我调研一下AI Agent在客服领域的落地案例”,一个Agentic RAG系统会自己决定:先检索“AI Agent客服案例”,看召回结果不足,再改写为“智能客服机器人案例”“大模型客服实践”等,直到凑够足够的上下文才作答。
这三类方向不是互斥的,你可以把Graph RAG当作检索层的一种特殊索引,把Ontology RAG当作预处理阶段的约束,把Agentic RAG当作整体控制流程。我们上一节用LangChain搭的只是最基础的Agentic RAG雏形,真正的Agentic RAG还需要工具调用和多轮决策,后面有机会再单独写一篇。
4. 常见问题与排查技巧实录
4.1 为什么知识库回答总是“答非所问”
这是我被问得最多的一个问题。遇到答非所问,先别急着调prompt,按照下面顺序排查:
- 第一步:召回测评。把用户问题拿到召回测试里跑一遍,看召回的前5个chunk跟问题是不是真的相关。如果chunk就不相关,问题一定出在索引侧,调prompt是白费力。
- 第二步:检查切块粒度。如果召回的相关chunk里混着大量无关内容,大概率是chunk_size太大,信息太杂。把chunk_size从800降到400试试。
- 第三步:检查Embedding模型是否匹配。中文知识库却用了一个纯英文Embedding模型,召回效果必然差。
- 第四步:检查检索模式。如果你用的是纯向量检索,试试混合检索+重排。
- 第五步:才看prompt。确认prompt里有没有明确“只根据材料回答”,有没有禁止“编造”。
4.2 RAG绩效评估怎么做:指标与流程
热词里有“rag知识库指标有哪些”和“rag测评怎么做”。我先直接说结论:RAG评测不能只看最终答案的“像不像”,要拆环节测。
我把评测分成三个层面:
检索层,核心指标是:
Recall@K:正确答案是否出现在召回的前K个片段中;MRR:第一个正确答案的排序位置;NDCG@K:考虑了排序位置的加权指标;Precision@K:前K个结果里有多少是相关的。
生成层,核心指标是:
- 忠实性(Faithfulness):生成的答案是否忠于上下文,不编造;
- 答案相关性(Answer Relevancy):答案是否针对问题;
- 上下文相关性(Context Relevancy):被召回的上下文是否足够回答问题。
端到端评估,最常用的是用RAGAS框架跑完整评测。RAGAS支持你把这个metrics一次性算完,只需要准备question、answer、contexts、ground_truth四个字段:
from datasets import Dataset from ragas import evaluate from ragas.metrics import faithfulness, answer_relevancy, context_precision, context_recall eval_dataset = Dataset.from_dict({ "question": ["什么是RAG?"], "answer": ["RAG是检索增强生成..."], "contexts": [["检索增强生成是一种结合检索和生成的技术..."]], "ground_truth": ["RAG是检索增强生成(Retrieval Augmented Generation)..."], }) result = evaluate(eval_dataset, metrics=[faithfulness, answer_relevancy, context_precision, context_recall]) print(result)评测数据集建议准备50~100条真实用户问题,覆盖常见的、边界性的、模糊的提问。没有评测,优化就是瞎调;有了这组指标,你才能判断改动是变好了还是变差了。
4.3 Dify类平台常见报错清单
这里把平台化工具常见的坑一次性列个表:
| 现象 | 原因 | 排查方法 |
|---|---|---|
| Dify升级后无法保存知识库 | 版本升级导致向量库索引冲突 | 备份数据后用Docker重新初始化,重建索引 |
| 修改知识库报Internal Server Error | API Key过期或数据库连接异常 | 看Dify容器日志,确认Postgres和向量库状态 |
| 文档上传后索引为0 | 文档格式不支持或分段失败 | 换用PDF/TXT,或手动设置分段标识符 |
| 检索结果总是旧的 | 知识库更新后未触发重新索引 | 在知识库设置里手动执行“重新索引” |
| 用了一段时间后回答质量骤降 | 向量库数据膨胀,产生了大量脏数据 | 定期清理已删除文档对应的向量,重建索引 |
4.4 几个值得一试的周边工具
最后补充几个跟知识库强相关的工具。热词里面出现的AnythingLLM很适合做个人知识库桌面应用,它支持本地模型和大模型API混合使用,直接把文档拖进去就能问答。Obsidian知识库搭建则是笔记党的最爱——用Obsidian管理Markdown笔记,再搭配Smart Connections或Copilot for Obsidian插件,就能实现基于笔记的本地语义检索。
如果你有“代码Agent”需求,比如Claude Code Agent如何调用已有知识库,我的建议是不要搞太复杂的RAG,直接把项目文档转成Markdown放到项目根目录的CLAUDE.md文件里,让Agent在对话开始时自动读取。Agent对长上下文的利用方式比RAG更适合,简单粗暴但效果非常好。
5. 个人实操心得与扩展建议
这套东西我前前后后折腾了几个月,几个时间段里反复调参的有效结论收个尾。
第一,检索层优先级永远高于生成层。很多人一上来就研究prompt技巧,其实RAG效果差大多数是索引和检索没做好。把召回测试跑透,Top-5精确率提到80%以上,生成层的压力就小很多。
第二,不要迷信单一Embedding模型。没有最佳模型,只有最适配数据集的模型。建议你手头准备两个模型,一个通用型的(如bge-m3),一个领域微调过的(如果你有预算和时间),在真实业务数据上跑评测,选效果好的上线。
第三,给知识库留“成长空间”。从一开始就设计好元数据方案——每个chunk记录来源、更新时间、目录层级。这样后续要做过滤、做权限、做增量更新,都不用推倒重来。
第四,Agent和RAG的结合,核心在检索的“主动性”。现在业界更前沿的做法是Self-RAG和CRAG,让模型自己判断该不该检索、检索结果可不可信、要不要启动二次检索。这套动态检索逻辑,建议你以后往这个方向深挖,它能解决大量真实场景里的“僵化问答”问题。
最后分享一个落地技巧:在做增量更新时,可以按文档摘要建立“内容指纹”缓存,同一份文件内容没变就不重新向量化,文档变了才重新切块入库。这样可以极大降低向量库膨胀和维护成本,也是我后来在几十万级文档场景下保住检索性能的关键。
希望这套从原理到实践的拆解,能帮你在“AI Agent + RAG 打造专属知识库”这件事上少走些弯路。踩过坑的、有更好方案的,欢迎评论区交流。