LangChain RAG 检索增强生成
RAG 解决的问题非常具体:LLM 不知道你公司内部的文档、你上传的 PDF、你的私有数据库里的内容——它的知识来自训练数据,截止到某个时间点。RAG 让 LLM 在回答前先去"查资料",然后基于查到的内容作答。
我调试过很多 RAG 系统,其中一个教训让我印象深刻:花了两周优化 Prompt、调整 temperature、换更好的模型,系统还是经常给出错误答案。后来发现根本原因是分块太粗糙,检索出来的内容跟问题根本不相关。95% 的 RAG 问题出在检索环节,不在生成环节。所以这页会重点讲检索质量,不是 Prompt 怎么写。
#工作原理
阶段一:建库(一次性)
文档 → 分块 → 向量化 → 存入向量数据库
↑
Embedding 模型把文字变成数字向量
阶段二:查询(每次提问时)
用户问题 → 向量化 → 在数据库里找最相似的块 → 塞进 Prompt → LLM 回答
关键理解:"向量相似"等于"语义相近"——不是关键词匹配,是找意思最接近的内容。这是 RAG 比普通搜索聪明的地方。
#最小可用示例
5 步搭一个能跑的 RAG:
pythonfrom langchain_openai import ChatOpenAI, OpenAIEmbeddings from langchain_community.vectorstores import Chroma from langchain_community.document_loaders import TextLoader from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import StrOutputParser from langchain_core.runnables import RunnablePassthrough from dotenv import load_dotenv load_dotenv() # 1. 加载文档 loader = TextLoader("./knowledge.txt", encoding="utf-8") documents = loader.load() # 2. 分块 splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50) chunks = splitter.split_documents(documents) # 3. 向量化 + 存入 Chroma embeddings = OpenAIEmbeddings() vectorstore = Chroma.from_documents(chunks, embeddings) # 4. 创建检索器 retriever = vectorstore.as_retriever(search_kwargs={"k": 3}) # 5. RAG Chain def format_docs(docs): return "\n\n".join(doc.page_content for doc in docs) prompt = ChatPromptTemplate.from_template(""" 基于以下上下文回答问题。如果上下文中没有相关信息,就说"我在文档中没有找到相关内容",不要编造。 上下文: {context} 问题:{question} 回答: """) rag_chain = ( {"context": retriever | format_docs, "question": RunnablePassthrough()} | prompt | ChatOpenAI(model="gpt-4o-mini", temperature=0) | StrOutputParser() ) answer = rag_chain.invoke("这个项目的主要功能是什么?") print(answer)
#文档加载:各种格式
python# 文本 from langchain_community.document_loaders import TextLoader loader = TextLoader("./doc.txt", encoding="utf-8") # PDF(常用,装 pypdf 包) from langchain_community.document_loaders import PyPDFLoader loader = PyPDFLoader("./doc.pdf") # Word 文档(装 python-docx) from langchain_community.document_loaders import Docx2txtLoader loader = Docx2txtLoader("./doc.docx") # 网页 from langchain_community.document_loaders import WebBaseLoader loader = WebBaseLoader("https://example.com/article") # CSV from langchain_community.document_loaders.csv_loader import CSVLoader loader = CSVLoader("./data.csv") # 批量加载整个目录 from langchain_community.document_loaders import DirectoryLoader loader = DirectoryLoader("./docs/", glob="**/*.md", show_progress=True) # 加载 documents = loader.load()
#分块:这里最影响 RAG 质量
分块策略是 RAG 里最被低估的环节,也是出问题最多的地方。
块太大:一个块里混了好几个不同主题的内容,检索出来的"相关块"会引入很多无关信息,导致 LLM 回答时被干扰。
块太小:一个完整的概念被切断了,检索出来的块缺少上下文,LLM 没法给出完整答案。
经验值参考:
| 内容类型 | chunk_size | chunk_overlap |
|---|---|---|
| 普通文档、README | 500-1000 | 50-100 |
| 技术文档、手册 | 1000-2000 | 150-200 |
| 对话记录、FAQ | 200-500 | 20-50 |
| 代码(按函数/类切) | 用语言专用 splitter | — |
pythonfrom langchain_text_splitters import RecursiveCharacterTextSplitter splitter = RecursiveCharacterTextSplitter( chunk_size=1000, chunk_overlap=200, # 相邻块之间保留一些重叠,保证切断处的上下文 separators=["\n\n", "\n", " ", ""] # 按段落 → 换行 → 空格的顺序切 ) chunks = splitter.split_documents(documents) print(f"共 {len(chunks)} 个块")
代码文档用专用 splitter:
pythonfrom langchain_text_splitters import RecursiveCharacterTextSplitter, Language python_splitter = RecursiveCharacterTextSplitter.from_language( language=Language.PYTHON, chunk_size=2000, chunk_overlap=200, ) # 按 Python 语法(类、函数)切,不会把一个函数从中间截断
#向量数据库选型
| 数据库 | 部署方式 | 适合场景 | 一句话 |
|---|---|---|---|
| Chroma | 本地 | 开发、原型、小项目 | 零配置,本地文件存储 |
| FAISS | 本地 | 追求速度、不需要持久化 | Meta 出品,检索极快 |
| Pinecone | 云端 SaaS | 生产、需要扩展性 | 托管服务,省运维 |
| Weaviate | 自托管/云 | 需要混合搜索 | 向量+关键词混合 |
我的建议:开发用 Chroma,零配置启动,非常适合验证想法。上生产时再评估——如果数据量不大(几万个块以内),Chroma 持久化版本完全够用,不一定要花钱买 Pinecone。
python# Chroma:本地持久化 from langchain_community.vectorstores import Chroma vectorstore = Chroma.from_documents( documents=chunks, embedding=OpenAIEmbeddings(), persist_directory="./chroma_db", # 持久化路径 ) # 下次重启加载已有数据 vectorstore = Chroma( persist_directory="./chroma_db", embedding_function=OpenAIEmbeddings(), )
python# FAISS:追求速度 from langchain_community.vectorstores import FAISS vectorstore = FAISS.from_documents(chunks, OpenAIEmbeddings()) vectorstore.save_local("./faiss_index") # 加载 vectorstore = FAISS.load_local("./faiss_index", OpenAIEmbeddings())
#检索策略
默认的相似度检索够用,但有两个场景值得了解:
MMR(Maximum Marginal Relevance):如果检索出来的 5 个块都在说同一件事,它们提供的信息是重叠的。MMR 在保证相关性的同时增加多样性——不让检索结果都是"差不多的内容":
pythonretriever = vectorstore.as_retriever( search_type="mmr", search_kwargs={ "k": 5, # 最终返回 5 个 "fetch_k": 20, # 先取 20 个候选 "lambda_mult": 0.7 # 越高越看重相关性,越低越看重多样性 } )
相似度阈值过滤:检索到的内容相关性低于阈值时直接丢掉,不传给 LLM:
pythonretriever = vectorstore.as_retriever( search_type="similarity_score_threshold", search_kwargs={"score_threshold": 0.7, "k": 5} )
这个设置可以减少 LLM 拿到无关内容后编造答案的概率。实际项目里我几乎都开着这个过滤。
#在 Prompt 里显示来源
让 RAG 能告诉用户"这个答案来自哪个文档":
pythondef format_docs_with_source(docs): """格式化文档时包含来源信息""" return "\n\n---\n\n".join([ f"[来源: {doc.metadata.get('source', '未知')}]\n{doc.page_content}" for doc in docs ]) prompt = ChatPromptTemplate.from_template(""" 基于以下上下文回答问题。回答时注明信息来源。 上下文: {context} 问题:{question} """) # 同时返回检索到的文档和最终答案 from langchain_core.runnables import RunnableParallel rag_chain_with_source = RunnableParallel( answer=( {"context": retriever | format_docs_with_source, "question": RunnablePassthrough()} | prompt | ChatOpenAI(model="gpt-4o-mini", temperature=0) | StrOutputParser() ), sources=(retriever), # 同时返回检索到的文档对象 ) result = rag_chain_with_source.invoke("这个项目的主要功能是什么?") print(result["answer"]) print("\n来源文件:") for doc in result["sources"]: print(f" - {doc.metadata.get('source')}")
#RAG 常见失败原因
用 RAG 不等于 AI 就不会说错话了。以下是最常见的失败场景:
检索质量差:找不到正确的内容,或者找到了一堆无关内容。这是 RAG 最常见的失败点,95% 的 RAG 问题都在检索环节,不在生成环节。排查方法:把 retriever.invoke(问题) 单独跑一遍,看返回的内容是否和问题相关。这一步我每次都做,不确认检索质量不往下走。
分块切坏了:一个答案被切到了两个块里,检索时只找到了一半。换分块策略,加大 chunk_overlap。
问题和文档语言不匹配:用户用中文问,文档是英文,Embedding 跨语言相似度会变差。要么在检索前把问题翻译成文档语言,要么用多语言 Embedding 模型(如 text-embedding-3-large)。
Prompt 没有"不知道就说不知道":LLM 在没有相关上下文时会编造。System Prompt 里必须明确写"如果上下文没有相关信息,直接说不知道,不要编造"。
#动手练习
把你自己的一份 PDF 或 Markdown 文档跑成一个问答系统:
python# TODO 1:加载你的文档(PDF 或 .txt 或 .md) # TODO 2:调整分块参数(从 chunk_size=500, chunk_overlap=50 开始) # TODO 3:创建向量数据库并存储 # TODO 4:测试检索效果(retriever.invoke("你的问题") 看返回什么) # TODO 5:组装完整的 RAG Chain 并运行 # 进阶:尝试把检索策略从 similarity 换成 mmr,对比效果差异
#小结
- RAG 的核心是"先检索后生成"——LLM 基于检索到的文档内容作答,而不是靠记忆。
- 分块质量决定 RAG 质量——90% 的 RAG 失败是检索问题,不是模型问题。先把
retriever.invoke()单独跑通,确认能找到正确内容。 - 开发用 Chroma(零配置),生产再评估是否需要 Pinecone 等托管服务。
- MMR 检索比默认相似度搜索更实用——避免返回一堆内容重叠的块。
- Prompt 里必须明确写"上下文没有相关内容时说不知道"——否则 LLM 会在检索失败时编造答案,RAG 反而让错误更严重。
下一步:Agents 代理系统 — 让 AI 不只是"查文档回答",而是自主调用工具完成任务
向量数据库参考:Chroma 文档↗ | FAISS↗ | Pinecone↗