之前在做知识库问答类功能时,最头疼的问题就是大模型“一本正经地胡说八道”。明明内部资料里写得清清楚楚,模型却经常给出一个看起来合理、实际上完全错误的答案。后来接触并落地了 RAG(Retrieval-Augmented Generation,检索增强生成)方案,才真正把这个问题从根上解决了一部分。本文就围绕 RAG 原理、系统搭建、代码实战和工程落地展开,从零开始带大家完成一套完整的 RAG 项目。内容比较长,建议先收藏再慢慢看。
1. RAG 是什么,它解决了什么问题
1.1 大模型的三个“天生缺陷”
先聊一个很多人都会遇到的场景:你问大模型一个公司内部制度问题,比如“年假怎么休”,模型回复了一段看起来很专业的答案,但和你们公司的制度完全对不上。这不是模型笨,而是它本来就不“知道”你们公司的内部资料。
大模型本质上是一个基于海量公开语料训练出来的概率模型。训练结束之后,它的知识就固定在了某个时间点,这带来三个问题:
- 知识时效性差:训练数据有截止时间,之后发生的新事件、新政策、新产品信息,它都不知道。
- 缺乏私有领域知识:企业内部文档、专业数据库、个人笔记等非公开数据,模型从未见过。
- 容易产生幻觉:当模型不确定答案时,它会根据概率“编”一个看起来合理的回答,而不是承认自己不知道。
这些缺陷在通用对话场景下还能忍受,但在企业知识库、客服问答、法律文书、医疗辅助等场景下,错误答案的代价非常高。
1.2 RAG 的核心思路:先检索,再生成
RAG 的思路其实很朴素:不要逼模型凭记忆回答,而是先从一个外部知识库中检索出与问题最相关的资料片段,把这些片段作为“参考资料”一起丢给模型,让模型基于这些资料来生成回答。
这样做的好处非常明显:
- 答案有据可依,显著降低幻觉。
- 知识库可以随时更新,不需要重新训练模型。
- 内部数据不会进入模型训练,隐私相对可控。
- 相比微调(Fine-tuning),RAG 的成本更低、迭代更快。
你可以把 RAG 理解成“开卷考试”:模型不再闭卷背诵,而是带着一堆参考书进考场,先翻书找到相关段落,再组织语言回答问题。
1.3 RAG 的典型应用场景
RAG 并不是只能做问答机器人,它的应用范围比很多人想象中更广:
- 企业知识库问答:把内部文档、制度、SOP 变成可检索的问答系统。
- 智能客服:结合产品手册和售后资料,自动回答用户问题。
- 文档分析助手:对 PDF、Word、网页等长文档进行摘要、信息抽取和对比分析。
- 专业领域辅助:医疗、法律、金融等场景下,基于权威资料提供参考建议。
- 大模型 Agent 的记忆与工具增强:让 Agent 具备访问私有数据的能力。
下面我们先把 RAG 的工作原理拆解清楚,再动手实现一套可运行的项目。
2. RAG 系统工作原理拆解
2.1 RAG 整体流程
一个标准的 RAG 系统,可以分成两个阶段:知识库构建(索引阶段)和问答阶段(检索生成阶段)。
知识库构建阶段做的事情是:
- 加载文档:读取 PDF、TXT、Markdown、Word 等格式的资料。
- 文本切分:把长文档切成适合检索的文本块(chunk)。
- 向量化:用 Embedding 模型把每个文本块转成向量。
- 存入向量数据库:将向量和原始文本一起存储,建立索引。
问答阶段做的事情是:
- 问题向量化:把用户问题用同一个 Embedding 模型转成向量。
- 相似度检索:在向量库中找出与问题向量最相似的 Top-K 个文本块。
- 构造 Prompt:把检索到的文本块和原始问题组装成提示词。
- 生成回答:把 Prompt 交给大模型生成最终答案。
2.2 Embedding 模型是什么
Embedding(嵌入)是 RAG 中承上启下的关键环节。它的作用是把一段文本映射成一个固定维度的向量,例如 768 维或 1024 维的浮点数数组。语义相近的文本,在向量空间中的距离也更近。
举个例子:
- “今天天气怎么样” 和 “今天会不会下雨” 的向量距离会比较近。
- “今天天气怎么样” 和 “红烧肉怎么做” 的向量距离会比较远。
这样当用户提问时,系统就可以通过计算向量相似度,找到语义上最相关的知识片段。
常见的中文 Embedding 模型有text2vec、bge-small-zh、bge-large-zh、m3e等。英文场景常用的有OpenAI text-embedding-ada-002、nomic-embed-text等。如果希望在本地环境完整跑通,使用Ollama拉起nomic-embed-text是很方便的做法。
2.3 向量数据库的作用
向量数据库用于存储 Embedding 向量,并提供高效的相似度检索能力。
常见的向量数据库和工具包括:
- Chroma:轻量级,支持本地持久化,适合学习和中小项目。
- FAISS:Facebook 开源的向量检索库,性能高,但偏向底层库。
- Milvus / Zilliz Cloud:分布式向量数据库,适合大规模生产环境。
- pgvector:PostgreSQL 的向量扩展,适合已有 PostgreSQL 体系的团队。
- Elasticsearch:通过向量插件支持混合搜索,适合已有 ES 体系的团队。
项目初期用 Chroma 或 FAISS 就够了,等数据量上来再迁移到 Milvus 或 pgvector。
2.4 文本切分的策略
文本切分直接影响检索质量。切得太粗,一个 chunk 包含太多无关信息,检索精度会下降;切得太细,单个 chunk 语义不完整,模型拿到的上下文不足。
常用的切分策略有以下几种:
- 固定长度切分:按字符数或 token 数切分,实现最简单。
- 递归字符切分:按段落、句子、标点逐级切分,尽量保留语义完整性。
- 语义切分:根据句子向量之间的相似度变化确定切分点,效果更好但计算成本更高。
- 结构化切分:针对 Markdown、HTML、PDF 按标题、表格结构切分。
切分时需要设置两个关键参数:
chunk_size:每个文本块的最大长度,通常按 token 数或字符数计算。chunk_overlap:相邻文本块之间的重叠长度,目的是避免一句话被截断到两个块里。
一般来说,chunk_size 在 300 到 800 token 之间,overlap 在 50 到 100 token 之间,具体需要根据文档类型和检索效果调优。
3. 环境准备与项目初始化
3.1 环境要求
本文示例以 Python 为主,核心依赖是 LangChain、Chroma、Ollama。具体版本请根据项目实际情况调整,下面只给出依赖名称和用途。
建议环境:
- 操作系统:Windows 10/11、macOS、Linux 均可。
- Python:3.9 及以上版本。
- 本地大模型运行工具:Ollama(用于加载 Embedding 模型和 LLM)。
- 开发工具:VS Code 或任意 Python IDE。
3.2 安装依赖
先创建一个虚拟环境:
python -m venv rag_env # Windows rag_env\Scripts\activate # macOS / Linux source rag_env/bin/activate然后安装 Python 依赖:
pip install langchain langchain-community langchain-text-splitters pip install chromadb pip install pypdf说明:langchain-community里包含了我们需要的文档加载器、向量库封装等组件;chromadb提供向量存储和检索能力;pypdf用于解析 PDF 文件。
3.3 用 Ollama 准备本地模型
如果你暂时没有大模型 API 的预算,或者不希望把私有数据传到外部服务,用 Ollama 在本地跑模型是很合适的方案。
安装 Ollama 后,拉取需要的模型:
# 本地运行 LLM,示例用 qwen2.5 ollama pull qwen2.5:7b # 本地运行 Embedding 模型 ollama pull nomic-embed-text拉取完成后,可以通过下面的命令验证服务是否正常:
ollama list如果你的开发机器 GPU 显存不够,也可以选择更小的模型,例如qwen2.5:3b或tinyllama。RAG 对 LLM 的推理能力有一定要求,模型太小时,即使检索到了正确答案,也可能组织不好语言。
3.4 项目结构规划
我们用一个简单的目录来组织代码:
rag-project/ ├── docs/ │ └── demo.txt ├── config.py ├── ingest.py ├── query.py └── requirements.txt各文件职责如下:
docs/demo.txt:示例知识文档。config.py:统一管理模型名称、向量库路径等配置。ingest.py:文档加载、切分、向量化、入库。query.py:检索并生成回答。requirements.txt:Python 依赖清单。
4. 手把手完成一套 RAG 实战项目
4.1 准备示例知识文档
在docs/demo.txt中写一段模拟的企业制度文档,内容要包含几个可以被检索到的明确知识点:
公司考勤管理制度 第一条 工作时间 公司实行每周五天工作制,工作时间为周一至周五上午 9:00 至下午 18:00,其中 12:00 至 13:00 为午休时间。 第二条 请假流程 员工请假需提前一天在 OA 系统中提交申请,三天以内的假期由部门负责人审批,三天以上需同时报人力资源部备案。 第三条 年假政策 员工入职满一年后,每年享有 5 天带薪年假。入职满三年后,年假天数增加到 10 天。年假应在当年内休完,未休完部分不累计到下一年度。 第四条 加班与调休 工作日加班超过 1 小时的部分,可以申请调休或按加班工资计算。法定节假日加班按国家规定支付三倍工资。这段文档包含了多个知识点,方便后面验证检索效果。
4.2 编写配置文件
config.py用于集中管理配置。集中管理的好处是后续调整模型或向量库路径时,不需要改业务代码。
# config.py import os # 知识文档目录 DOCS_DIR = os.path.join(os.path.dirname(__file__), "docs") # 向量数据库持久化目录 CHROMA_DIR = os.path.join(os.path.dirname(__file__), "chroma_db") # Embedding 模型名称(Ollama 本地模型) EMBEDDING_MODEL = "nomic-embed-text" # 生成回答使用的 LLM 名称(Ollama 本地模型) LLM_MODEL = "qwen2.5:7b" # 检索返回的文本块数量 TOP_K = 4 # 文本切分参数 CHUNK_SIZE = 500 CHUNK_OVERLAP = 504.3 编写知识库构建代码
ingest.py负责把文档变成向量并入库。这是 RAG 系统最核心的代码之一。
# ingest.py from langchain_community.document_loaders import DirectoryLoader, TextLoader from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain_community.embeddings import OllamaEmbeddings from langchain_community.vectorstores import Chroma import config def build_knowledge_base(): # 1. 加载 docs 目录下的 txt 文档 loader = DirectoryLoader( config.DOCS_DIR, glob="**/*.txt", loader_cls=TextLoader, loader_kwargs={"encoding": "utf-8"} ) documents = loader.load() print(f"加载文档数量: {len(documents)}") # 2. 递归字符切分 text_splitter = RecursiveCharacterTextSplitter( chunk_size=config.CHUNK_SIZE, chunk_overlap=config.CHUNK_OVERLAP, separators=["\n\n", "\n", "。", "!", "?", ";", " ", ""] ) chunks = text_splitter.split_documents(documents) print(f"切分文本块数量: {len(chunks)}") # 3. 向量化并写入 Chroma embeddings = OllamaEmbeddings(model=config.EMBEDDING_MODEL) vectorstore = Chroma.from_documents( documents=chunks, embedding=embeddings, persist_directory=config.CHROMA_DIR ) print(f"向量库存放完成,路径: {config.CHROMA_DIR}") if __name__ == "__main__": build_knowledge_base()这里有几个值得注意的点:
DirectoryLoader可以批量加载目录下所有匹配的文件,避免为每个文档写加载逻辑。RecursiveCharacterTextSplitter的分隔符数组是有优先级的,它先尝试按\n\n切分,再按句子标点切分,这样能最大程度保留语义完整。Chroma.from_documents会同时完成向量化和入库,数据会持久化到persist_directory指定的目录。- Embedding 模型必须和后面查询阶段使用同一个模型,否则检索结果会完全错乱。
运行入库:
python ingest.py正常情况下会输出类似下面的信息:
加载文档数量: 1 切分文本块数量: 8 向量库存放完成,路径: .../rag-project/chroma_db4.4 编写问答检索代码
query.py实现完整的检索增强生成流程。
# query.py from langchain_community.embeddings import OllamaEmbeddings from langchain_community.vectorstores import Chroma from langchain_community.llms import Ollama from langchain.chains import RetrievalQA from langchain.prompts import PromptTemplate import config def create_qa_chain(): # 初始化 Embedding 和向量库 embeddings = OllamaEmbeddings(model=config.EMBEDDING_MODEL) vectorstore = Chroma( persist_directory=config.CHROMA_DIR, embedding_function=embeddings ) # 构造检索器 retriever = vectorstore.as_retriever( search_type="similarity", search_kwargs={"k": config.TOP_K} ) # 初始化本地 LLM llm = Ollama( model=config.LLM_MODEL, temperature=0.1, num_predict=1024 ) # 构造 Prompt prompt_template = """你是一个严谨的知识库问答助手。请仅根据下面提供的资料回答问题。 如果资料中没有相关内容,请直接回答“资料中未找到相关内容”,禁止编造。 资料: {context} 问题:{question} 请用中文回答:""" PROMPT = PromptTemplate( template=prompt_template, input_variables=["context", "question"] ) # 组装 RAG 问答链 qa = RetrievalQA.from_chain_type( llm=llm, retriever=retriever, chain_type="stuff", chain_type_kwargs={"prompt": PROMPT}, return_source_documents=True ) return qa if __name__ == "__main__": qa = create_qa_chain() question = "入职满两年,年假有几天?" result = qa.invoke(question) print("问题:", question) print("回答:", result["result"]) print("\n--- 检索到的参考资料 ---") for i, doc in enumerate(result["source_documents"]): print(f"[{i + 1}] {doc.page_content[:100]}...")代码中的chain_type="stuff"表示把所有检索到的文本块直接拼接进 Prompt。当 Top-K 数量较少、每个 chunk 不大时,这个方式最简单有效。
temperature=0.1让模型输出更稳定,减少随机发挥。知识库问答场景下,稳定优先于多样性。
运行问答:
python query.py预期输出类似:
问题: 入职满两年,年假有几天? 回答: 根据公司考勤管理制度,员工入职满一年后,每年享有 5 天带薪年假,入职满三年后年假增加到 10 天。入职满两年的员工年假仍为 5 天。 --- 检索到的参考资料 --- [1] 第三条 年假政策... [2] 第二条 请假流程...看到这样的输出,就说明这套 RAG 项目已经跑通了。模型不是凭记忆回答,而是基于我们提供的文档内容给出的答案。
4.5 用原生方式理解 RAG 内部流程
LangChain 封装了大量细节,方便是方便,但如果不理解内部原理,出了问题很难排查。下面用最原始的方式演示一遍检索过程:
# simple_retrieve_demo.py from langchain_community.embeddings import OllamaEmbeddings from chromadb import PersistentClient import config def simple_similarity_search(query: str, top_k: int = 2): # 1. 加载向量库 client = PersistentClient(path=config.CHROMA_DIR) collection = client.get_or_create_collection("langchain") # 2. 向量化问题 embeddings = OllamaEmbeddings(model=config.EMBEDDING_MODEL) query_vector = embeddings.embed_query(query) # 3. 在向量库中检索 results = collection.query( query_embeddings=[query_vector], n_results=top_k, include=["documents", "metadatas", "distances"] ) # 4. 打印结果 for i, doc in enumerate(results["documents"][0]): distance = results["distances"][0][i] print(f"第 {i + 1} 条,距离: {distance:.4f}") print(doc) print("-" * 50) if __name__ == "__main__": simple_similarity_search("年假政策", top_k=2)这里的distance表示向量之间的相似度距离,距离越小代表语义越相关。理解这层逻辑之后,再看 LangChain 的RetrievalQA,你就会明白它只是把“向量化、检索、拼 Prompt、调 LLM”这几件事封装起来了,并没有额外的魔法。
5. 检索质量评估:RAG 效果好不好,必须用指标说话
很多同学做完 RAG 后,凭感觉判断“看起来还行”。但在企业级项目中,必须用可量化的指标评估效果,否则上线后出了问题很难定位。
5.1 检索阶段指标
检索阶段评估的是系统能否找到正确答案对应的文本块。
- 命中率(Hit Rate):在 Top-K 结果中,是否包含包含正确答案的文本块。命中率越高,说明检索越有效。
- MRR(Mean Reciprocal Rank):衡量正确答案排在结果中第几位。如果正确答案排得越靠前,MRR 越高。
- 召回率(Recall@K):在 Top-K 结果中,相关文档占全部相关文档的比例。
这些指标需要提前构建测试集。最简单的方法是:从知识文档中抽取若干条“问题-正确答案片段”对,然后逐个测试检索结果。
5.2 生成阶段指标
生成阶段评估的是最终回答的质量。常见的评估维度包括:
- 忠实度(Faithfulness):回答是否严格基于检索到的资料,有没有编造内容。
- 答案相关性(Answer Relevancy):回答是否针对用户问题,有没有答非所问。
- 上下文相关性(Context Relevance):检索到的上下文是否与问题相关。
这部分评估可以使用 LLM 作为裁判(LLM-as-a-Judge),也可以人工抽样评估。企业级项目中建议两者结合:先用 LLM 自动评测大批量数据,再人工抽检重点场景。
5.3 评估工具简介
目前 LLM 应用评估工具比较多,常见的有RAGAS、TruLens,中文项目也可以使用Agile-Data等。RAGAS 提供了一套开箱即用的指标计算框架,适合快速接入。
RAGAS 的核心思路是:把测试集构造成question、answer、contexts、ground_truth四个字段,然后计算上述指标。评估代码很简单:
from ragas import evaluate from ragas.metrics import faithfulness, answer_relevancy, context_precision # 需要准备测试数据集,fields 包含 question/answer/contexts/ground_truth result = evaluate( dataset=test_dataset, metrics=[faithfulness, answer_relevancy, context_precision] ) result.to_pandas()注意:RAGAS 的评估依赖一个可用的 LLM 来打分,所以使用前需要配置好 LLM 接口。
6. 企业级 RAG 落地的常见问题与排查思路
下面整理几个高频问题,都是实战中容易踩的坑。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 检索结果完全不相关 | Embedding 模型不适合领域或语言 | 换用中文/领域专用的 Embedding 模型,例如 bge-zh 系列 |
| 回答内容太泛,没有引用资料细节 | chunk 太大,检索粒度太粗 | 调小 chunk_size,增大 chunk_overlap |
| 答案包含资料中没有的内容 | Prompt 没有约束“只能基于资料回答” | 在 Prompt 中明确禁止编造,并设置低温参数 |
| 同一个问题多次回答不一致 | temperature 设置过高 | 将 temperature 调低,如 0.1 以下 |
| 向量检索耗时长 | 数据量过大,或使用了不合适的向量库 | 引入索引优化、批量检索,或迁移到 Milvus |
| 新增文档后查不到 | 新文档没有重新执行入库流程 | 实现增量入库,对新增文档单独向量化并写入 |
| 中文切分把句子截断 | 分隔符只用了空格 | 增加中文标点分隔符,例如。!?; |
| Ollama 调用时报连接错误 | Ollama 服务未启动或端口不通 | 执行ollama serve启动服务,检查 11434 端口 |
对于最常遇到的“检索结果不相关”问题,建议按下面的顺序排查:
- 查看检索到的原始文本块内容,确认是不是 chunk 切分的问题。
- 报告问题的原文,测试不同提问方式,确认是不是问题表述的问题。
- 尝试换一个 Embedding 模型,比较命中率。
- 检查知识库里是否存在相关文档。如果文档本身缺失,再好的检索也没用。
- 检查 Top-K 值是否太小。K 太小时可能会漏掉正确答案。
7. 企业级 RAG 最佳实践与工程建议
7.1 检索质量优先于生成调优
RAG 的效果上限由检索决定。如果检索到的内容本身就不相关,再好的 LLM 也无法生成正确答案。所以建议把所有精力优先放在:
- 高质量的数据清洗。
- 合理的文本切分。
- 合适的 Embedding 模型。
- 针对性的检索调优。
数据质量是 RAG 的地基。文档里如果存在大量错误信息、重复信息或格式混乱的内容,最终回答质量一定受影响。在上线前,需要对知识库做一轮清洗和去重。
7.2 选择合适的检索策略
基础版 RAG 只做向量相似度检索,但企业级场景中往往不够。推荐以下几种增强策略:
- 混合检索(Hybrid Search):同时使用向量检索和关键词检索,再融合结果。关键词检索擅长处理专业术语、产品型号等冷门词汇,向量检索擅长处理语义相近但字面不同的表达。
- 重排序(Rerank):第一轮先用向量检索召回较多的候选(例如 20 条),再用 Rerank 模型精排,取前几名的结果。重排序能显著提升精度,但对性能有一定消耗。
- 多路召回:从不同数据源或不同检索方式分别召回,再合并去重。
如果使用了 Elasticsearch 或 OpenSearch,可以原生支持 BM25 + 向量的混合检索。对于 Milvus 这类专用向量库,通常需要结合外部搜索服务实现混合检索,或者干脆用 Milvus 自身的能力做两种检索再进行分数融合。
7.3 权限与数据安全
企业知识库中通常包含敏感数据,RAG 系统上线前必须想清楚权限问题。
- 最小权限原则:不同角色只能检索到自己有权限访问的文档。实现方式是在检索阶段根据用户身份过滤文档或知识库集合。
- 数据不出内网:若对数据安全要求严格,应该使用本地部署的 Embedding 模型和 LLM,避免把私有文档上传到外部 API。
- 敏感信息脱敏:在入库前对身份证号、手机号、银行卡号等敏感信息做脱敏处理。
- 访问审计:记录用户查询、检索结果和生成回答的日志,便于安全审计和问题回溯。
7.4 建立评测闭环
企业级 RAG 系统不能“上线即不管”。建议搭建一个持续评测机制:
- 建立测试集:包含问题、标准答案、参考片段。
- 每次修改切分策略、换模型、调参数后,都跑一遍评测。
- 比较检索指标和生成指标,用数据决定是否更新。
评测集也需要定期更新,融入线上用户提出的真实问题,让测试集本身更贴近实际。
7.5 性能与缓存
RAG 的性能瓶颈主要在两个地方:Embedding 计算和 LLM 推理。
- Embedding 阶段可以批量计算,避免循环调用。入库时一次性批量向量化,比逐条处理快很多。
- 高频问题可以做缓存。用户问过的问题如果再次出现,直接返回缓存答案,减少 LLM 调用成本。
- 搜索阶段如果数据量大,建议在向量库中建好索引,并合理设置
nprobe等参数。不同向量数据库参数不同,一般来说,nprobe越大检索越准,但耗时越长,需要根据实际检索延迟做权衡。
7.6 从 RAG 到 Agentic RAG
当 RAG 变得复杂后,会有人引入 Agent 的思想。所谓 Agentic RAG,就是让大模型自主决定“什么时候需要检索、检索什么、检索多少次”,本质上能给 RAG 增加规划能力。
举个例子,用户问“对比一下 A 方案和 B 方案的优缺点”,传统 RAG 可能只会检索一次,拿到的上下文不够全面;Agentic RAG 可以先拆解问题,检索 A 方案相关资料,再检索 B 方案相关资料,最后汇总回答。
但如果刚入门,建议先扎实掌握基础 RAG,不要急着上 Agent。基础链路稳定之后,再根据业务复杂度决定是否引入 Agent 规划。
8. 总结与下一步学习路线
到这里,我们已经完整实现了一个 RAG 系统:从文档加载、文本切分、向量化入库,到检索、构造 Prompt、生成回答,以及评估方法和企业级落地建议。
回顾一下核心关键点:
- RAG 的核心是“先检索,再生成”,用外部知识库约束大模型的生成过程。
- Embedding 模型和文本切分策略直接决定检索质量。
- 向量数据库是存储和检索的载体,小项目用 Chroma / FAISS,大规模场景用 Milvus / pgvector。
- Prompt 中要明确“只能基于资料回答”,这是降低幻觉的最直接手段。
- 上线前必须建立评测指标和测试集,用数据评估效果,而不是靠感觉。
如果你接下来想继续深入,建议按照下面的路线学习:
- 尝试替换不同的 Embedding 模型,对比检索命中率。
- 加入混合检索和 Rerank,观察效果和耗时变化。
- 从 LangChain 的封装中跳出来,用原生 API 实现一次完整的加载、切分、向量化、检索流程。
- 学习如何设计增量入库和文档更新策略。
- 研究如何对不同角色做文档权限隔离。
- 等基础链路稳定后,再探索 Agentic RAG 和复杂多轮交互场景。
RAG 是一个实践性很强的方向,代码可以复制本文的示例跑通,但真正理解它、用好它,还是需要不断拿真实业务数据去测试和调优。希望这篇文章能帮你在 RAG 落地的路上少踩一些坑。有不清楚的细节,欢迎在评论区留言交流。