前言 大模型的知识截止于训练数据的时间点,而且不知道你公司内部的产品文档、技术手册、规章制度。
RAG(Retrieval-Augmented Generation,检索增强生成)解决了这个问题:先把你的文档”喂”给系统,用户提问时,系统先检索最相关的文档片段,再把片段和问题一起发给大模型。这样模型就能基于你的私有知识来回答了。
这篇文章用最小的代码量,跑通一个完整的 RAG 流程。
一、RAG 的核心流程 1 2 3 4 5 文档 → 切片 → Embedding → 向量数据库 ↓ (用户提问时) 用户提问 → 向量化 → 相似度检索 → Top-K 文档片段 ↓ 拼进 Prompt → 发给 LLM → 返回答案
四个关键步骤:
加载文档 :读入 PDF、TXT、Markdown、网页等
文本切片 :大文档切成小段落(chunk)
向量化存储 :把每个 chunk 转成向量,存入向量数据库
检索 + 生成 :查询时检索最相关的 chunk,拼进 prompt 发给 LLM
二、安装依赖 1 2 pip install langchain langchain-community langchain-openai pip install chromadb pypdf unstructured
三、加载文档 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 from langchain_community.document_loaders import ( TextLoader, PyPDFLoader, UnstructuredMarkdownLoader, CSVLoader, ) loader = TextLoader("docs/产品手册.txt" , encoding="utf-8" ) documents = loader.load() print (f"加载了 {len (documents)} 个文档" )from langchain_community.document_loaders import DirectoryLoaderloader = DirectoryLoader("docs/" , glob="**/*.md" , loader_cls=UnstructuredMarkdownLoader) documents = loader.load() print (f"共加载 {len (documents)} 个文档" )
四、文本切片 文档太长不能直接塞给 Embedding 模型,需要切成小段落(chunk)。
1 2 3 4 5 6 7 8 9 10 11 12 13 from langchain.text_splitter import RecursiveCharacterTextSplittertext_splitter = RecursiveCharacterTextSplitter( chunk_size=800 , chunk_overlap=150 , separators=["\n\n" , "\n" , "。" , "," , " " , "" ] ) chunks = text_splitter.split_documents(documents) print (f"切成了 {len (chunks)} 个文本块" )for i, chunk in enumerate (chunks[:3 ]): print (f"--- Chunk {i} ({len (chunk.page_content)} 字符) ---" ) print (chunk.page_content[:200 ])
切片参数调优
参数
太小
太大
推荐范围
chunk_size
缺少上下文,回答碎片化
检索精度下降,噪音多
500~1500
chunk_overlap
冗余多,存储和检索开销大
关键信息在边界被切断
chunk_size 的 15~25%
五、向量化与存储 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 from langchain_openai import OpenAIEmbeddingsfrom langchain_community.vectorstores import Chromaembeddings = OpenAIEmbeddings( model="text-embedding-3-small" , openai_api_key="sk-xxx" ) vectorstore = Chroma.from_documents( documents=chunks, embedding=embeddings, persist_directory="./chroma_db" ) print (f"向量数据库已创建,共 {len (chunks)} 条记录" )
ChromaDB 是一个轻量级的向量数据库,适合单机使用。数据存在 ./chroma_db/ 目录,下次启动可以直接加载:
1 2 3 4 vectorstore = Chroma( persist_directory="./chroma_db" , embedding_function=embeddings )
六、检索 + 回答 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 from langchain_openai import ChatOpenAIfrom langchain.chains import RetrievalQAllm = ChatOpenAI( model="gpt-4o-mini" , temperature=0.3 , openai_api_key="sk-xxx" ) qa_chain = RetrievalQA.from_chain_type( llm=llm, chain_type="stuff" , retriever=vectorstore.as_retriever(search_kwargs={"k" : 4 }), return_source_documents=True ) question = "我们的产品支持哪些支付方式?" result = qa_chain.invoke({"query" : question}) print ("回答:" , result["result" ])print ("\n引用来源:" )for doc in result["source_documents" ]: print (f" - {doc.page_content[:80 ]} ..." )
七、一个完整的 RAG 工具类 把上面的流程封装成一个可复用的类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 from langchain_community.document_loaders import DirectoryLoader, UnstructuredMarkdownLoaderfrom langchain.text_splitter import RecursiveCharacterTextSplitterfrom langchain_openai import OpenAIEmbeddings, ChatOpenAIfrom langchain_community.vectorstores import Chromafrom langchain.chains import RetrievalQAimport osclass RAG : def __init__ (self, docs_dir="./docs" , db_dir="./chroma_db" ): self .docs_dir = docs_dir self .db_dir = db_dir self .embeddings = OpenAIEmbeddings(model="text-embedding-3-small" ) self .llm = ChatOpenAI(model="gpt-4o-mini" , temperature=0.3 ) if os.path.exists(db_dir) and os.listdir(db_dir): self .vectorstore = Chroma( persist_directory=db_dir, embedding_function=self .embeddings ) else : self .vectorstore = None def build_index (self ): """构建/重建向量索引""" loader = DirectoryLoader(self .docs_dir, glob="**/*.md" , loader_cls=UnstructuredMarkdownLoader) documents = loader.load() text_splitter = RecursiveCharacterTextSplitter( chunk_size=800 , chunk_overlap=150 ) chunks = text_splitter.split_documents(documents) self .vectorstore = Chroma.from_documents( documents=chunks, embedding=self .embeddings, persist_directory=self .db_dir ) print (f"索引构建完成: {len (chunks)} 个文本块" ) def ask (self, question, k=4 ): """提问""" if self .vectorstore is None : raise ValueError("请先调用 build_index()" ) qa = RetrievalQA.from_chain_type( llm=self .llm, chain_type="stuff" , retriever=self .vectorstore.as_retriever( search_kwargs={"k" : k} ), return_source_documents=True ) result = qa.invoke({"query" : question}) return { "answer" : result["result" ], "sources" : [doc.page_content[:100 ] for doc in result["source_documents" ]] } rag = RAG(docs_dir="./my_knowledge_base" ) rag.build_index() print (rag.ask("公司年假政策是什么?" )["answer" ])
八、效果调优经验 检索效果不好?
调整 chunk_size 和 chunk_overlap
换更强的 Embedding 模型(如 bge-large-zh-v1.5 替换 bge-small)
尝试多种检索策略混合(关键词 BM25 + 向量检索)
对文档做预处理:去掉页眉页脚、表格转文字
回答效果不好?
调低 temperature(0.1~0.3),减少模型”自由发挥”
在 system prompt 里强调”如果文档中没有相关信息,就说不知道”
增加 k 值(返回更多 chunk,但同时 prompt 会变长)
考虑对检索到的 chunk 做重排序(reranker 模型)
效果不错但太慢?
换用更小的 LLM(GPT-4o-mini 替代 GPT-4o)
用 GPU 跑 Embedding 模型
减少每次检索的 chunk 数量
九、更高级的 RAG 这篇文章走通了最基础的 RAG 流程。生产环境中还会用到:
Reranker :对检索结果做二次排序,提高相关性
多轮对话 RAG :支持追问,需要处理对话历史和上下文
混合检索 :向量检索 + 关键词检索,互补不足
图谱 RAG :不是检索文本块,而是构建知识图谱后推理
以上跑通了一整个 RAG 管道。把 ./my_knowledge_base/ 里的文件换成你自己的文档,就能用了。