RAG 是什么,为什么需要它 大语言模型有一个根本性的局限:它只知道训练数据里的东西。你问它公司内部的产品文档、最新的技术规范、昨天刚更新的 API 文档,它一概不知道。
传统的解决思路是微调——把你的数据喂给模型重新训练。但微调成本高、周期长,而且数据更新后得重新来一遍。
RAG(Retrieval-Augmented Generation,检索增强生成)提供了一种更轻量的方案:不改模型,改输入 。每次用户提问时,先从你的知识库里检索出相关内容,把检索到的内容塞到 prompt 里一起发给模型,模型基于这些上下文来回答。
打个比方:微调相当于让学生背下整本教材,RAG 相当于让学生带着教材开卷考试。后者更灵活、更新更方便。
RAG 的完整流程 一个 RAG 系统有两个阶段:
索引阶段(离线做一次):
加载文档(PDF、Markdown、网页、数据库记录等)
文本切分(把长文档切成小段)
向量化(用嵌入模型把文本段落转成向量)
存入向量数据库
查询阶段(每次用户提问):
用户提问 -> 向量化
在向量数据库中检索相似段落
把检索结果拼入 prompt
用 Hermes 生成回答
下面按这个流程,一步步写代码。
环境准备 1 pip install langchain langchain-community chromadb sentence-transformers llama-cpp-python
我们用的技术栈:
LangChain — 做 pipeline 编排
ChromaDB — 本地向量数据库,轻量好用
sentence-transformers — 加载嵌入模型
llama-cpp-python — 加载 GGUF 格式的 Hermes 模型
如果你还没有 Hermes 模型文件,先看 Hermes 入门指南 了解怎么获取。
第一步:文档加载和切分 文档切分是 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 from langchain.text_splitter import RecursiveCharacterTextSplitterfrom langchain_community.document_loaders import ( DirectoryLoader, TextLoader, PyPDFLoader, UnstructuredMarkdownLoader ) def load_documents (docs_dir: str ) -> list : """加载目录下的所有文档""" documents = [] md_loader = DirectoryLoader( docs_dir, glob="**/*.md" , loader_cls=UnstructuredMarkdownLoader, show_progress=True ) documents.extend(md_loader.load()) pdf_loader = DirectoryLoader( docs_dir, glob="**/*.pdf" , loader_cls=PyPDFLoader, show_progress=True ) documents.extend(pdf_loader.load()) txt_loader = DirectoryLoader( docs_dir, glob="**/*.txt" , loader_cls=TextLoader, loader_kwargs={"encoding" : "utf-8" }, show_progress=True ) documents.extend(txt_loader.load()) print (f"共加载 {len (documents)} 个文档" ) return documents def split_documents (documents: list , chunk_size: int = 500 , chunk_overlap: int = 80 ) -> list : """切分文档""" splitter = RecursiveCharacterTextSplitter( chunk_size=chunk_size, chunk_overlap=chunk_overlap, separators=["\n\n" , "\n" , "。" , "!" , "?" , ";" , " " , "" ], length_function=len ) chunks = splitter.split_documents(documents) print (f"切分为 {len (chunks)} 个文本块" ) return chunks
几个关键参数的选择依据:
chunk_size=500 — 中文场景下 500 字符大约是 200-300 个汉字,一个完整的段落。英文场景通常设 1000
chunk_overlap=80 — 前后段落有 80 字符的重叠,避免关键信息恰好被切断
separators — 优先按段落分(\n\n),其次按句子分(句号等标点),最后才按字符分。中文要特别注意加上中文标点
第二步:向量化和存储 嵌入模型(Embedding Model)负责把文本转成固定维度的向量。选一个好的嵌入模型很重要——如果嵌入质量差,语义相近的文本在向量空间里不相近,检索就会拉胯。
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 from langchain_community.embeddings import HuggingFaceEmbeddingsfrom langchain_community.vectorstores import Chromadef create_vector_store (chunks: list , persist_dir: str = "./chroma_db" ) -> Chroma: """创建向量数据库""" embeddings = HuggingFaceEmbeddings( model_name="BAAI/bge-m3" , model_kwargs={"device" : "cuda" }, encode_kwargs={ "normalize_embeddings" : True , "batch_size" : 32 } ) vectorstore = Chroma.from_documents( documents=chunks, embedding=embeddings, persist_directory=persist_dir, collection_metadata={"hnsw:space" : "cosine" } ) print (f"向量数据库创建完成,保存在 {persist_dir} " ) return vectorstore
嵌入模型选择建议:
模型
维度
中文效果
模型大小
推荐场景
BAAI/bge-m3
1024
优秀
~2.2 GB
中英混合首选
BAAI/bge-large-zh-v1.5
1024
优秀
~1.3 GB
纯中文场景
intfloat/multilingual-e5-large
1024
良好
~1.1 GB
多语言场景
BAAI/bge-small-zh-v1.5
512
良好
~96 MB
资源受限
关于嵌入模型和语义搜索的更多细节,后续会在 语义搜索入门 里展开讲。
第三步:检索器配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 def create_retriever (vectorstore: Chroma, search_type: str = "mmr" , k: int = 4 ): """创建检索器""" if search_type == "mmr" : retriever = vectorstore.as_retriever( search_type="mmr" , search_kwargs={ "k" : k, "fetch_k" : 20 , "lambda_mult" : 0.6 } ) elif search_type == "similarity" : retriever = vectorstore.as_retriever( search_type="similarity" , search_kwargs={"k" : k} ) return retriever
推荐用 MMR 而不是简单的相似度检索。简单相似度检索容易返回几条内容高度相似的段落(可能来自同一段文字的不同切片),浪费了宝贵的 context 空间。MMR 会在保证相关性的前提下尽量让结果多样化。
第四步:用 Hermes 做生成 把检索和生成串起来:
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 from llama_cpp import Llamaclass HermesRAG : def __init__ (self, model_path: str , vectorstore_dir: str = "./chroma_db" ): self .llm = Llama( model_path=model_path, n_ctx=8192 , n_gpu_layers=-1 , chat_format="chatml" ) self .vectorstore = load_vector_store(vectorstore_dir) self .retriever = create_retriever(self .vectorstore, search_type="mmr" , k=4 ) def query (self, question: str , temperature: float = 0.3 ) -> dict : """RAG 查询""" docs = self .retriever.invoke(question) context = "\n\n---\n\n" .join([ f"[来源: {doc.metadata.get('source' , '未知' )} ]\n{doc.page_content} " for doc in docs ]) system_prompt = """你是一个专业的知识库助手。请基于下面提供的参考资料来回答用户的问题。 规则: 1. 只基于参考资料中的信息回答,不要编造 2. 如果参考资料中没有相关信息,明确告诉用户"根据现有资料无法回答" 3. 回答时尽量引用具体的信息来源 4. 用清晰简洁的中文回答""" user_prompt = f"参考资料:\n{context} \n\n问题:{question} " messages = [ {"role" : "system" , "content" : system_prompt}, {"role" : "user" , "content" : user_prompt} ] response = self .llm.create_chat_completion( messages=messages, max_tokens=1024 , temperature=temperature ) answer = response["choices" ][0 ]["message" ]["content" ] return { "question" : question, "answer" : answer, "source_documents" : [ {"content" : doc.page_content[:200 ], "source" : doc.metadata.get("source" , "未知" )} for doc in docs ] }
使用方式:
1 2 3 4 5 6 7 8 rag = HermesRAG( model_path="./Hermes-3-Llama-3.1-8B-Q5_K_M.gguf" , vectorstore_dir="./chroma_db" ) result = rag.query("我们的退款政策是什么?" ) print (result["answer" ])print ("参考来源:" , [d["source" ] for d in result["source_documents" ]])
提升 RAG 效果的实用技巧 基础 RAG 搭起来只是起步,想在实际场景里用好还需要不少优化。
1. 查询改写(Query Rewriting)
用户的原始提问可能太口语化或太模糊,直接拿去检索效果不好。可以用 Hermes 先改写一下查询:
1 2 3 4 5 6 7 8 9 10 def rewrite_query (self, original_query: str ) -> str : messages = [ { "role" : "system" , "content" : "你是一个搜索查询优化专家。把用户的口语化问题改写成更适合检索的形式。只输出改写后的查询,不要解释。" }, {"role" : "user" , "content" : original_query} ] response = self .llm.create_chat_completion(messages=messages, max_tokens=100 , temperature=0.1 ) return response["choices" ][0 ]["message" ]["content" ]
比如用户问”那个退款要多久啊”,改写后可能变成”退款处理时间周期政策”,更容易匹配到知识库里的相关段落。
2. 混合检索(Hybrid Search)
纯向量检索有时候会”过度语义化”——用户明确搜一个专有名词(如 “ERR_4012”),向量检索可能找不到。解决方案是混合使用向量检索和关键词检索:
1 2 3 4 5 6 7 8 9 10 11 12 13 from langchain.retrievers import EnsembleRetrieverfrom langchain_community.retrievers import BM25Retrieverdef create_hybrid_retriever (chunks, vectorstore, k=4 ): bm25_retriever = BM25Retriever.from_documents(chunks) bm25_retriever.k = k vector_retriever = vectorstore.as_retriever(search_type="mmr" , search_kwargs={"k" : k}) ensemble = EnsembleRetriever( retrievers=[bm25_retriever, vector_retriever], weights=[0.4 , 0.6 ] ) return ensemble
3. 重排序(Reranking)
检索到一批候选段落后,用一个 cross-encoder 模型重新排序,把最相关的排在前面:
1 2 3 4 5 6 7 8 9 10 11 from sentence_transformers import CrossEncoderclass Reranker : def __init__ (self, model_name="BAAI/bge-reranker-v2-m3" ): self .model = CrossEncoder(model_name, max_length=512 ) def rerank (self, query: str , documents: list , top_k: int = 3 ) -> list : pairs = [(query, doc.page_content) for doc in documents] scores = self .model.predict(pairs) scored_docs = sorted (zip (documents, scores), key=lambda x: x[1 ], reverse=True ) return [doc for doc, score in scored_docs[:top_k]]
重排序的效果非常显著——初次检索可能返回 10 个结果里有 3 个不太相关的,经过重排序后把最相关的 3-4 个挑出来,送给 Hermes 生成,回答质量明显提升。
4. 上下文压缩
检索到的段落可能包含很多与问题无关的信息。在送给 Hermes 之前,可以先做一轮压缩,只保留和问题相关的部分。这个方法会增加一轮 LLM 调用的延迟,但能显著减少塞入 prompt 的无关信息,让 Hermes 生成的回答更聚焦。
评估 RAG 系统质量 搭完不测等于没搭。评估 RAG 系统需要关注几个维度:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 def evaluate_rag (rag_system, test_cases: list ) -> dict : results = {"total" : len (test_cases), "retrieval_hit" : 0 , "answer_correct" : 0 } for case in test_cases: result = rag_system.query(case ["question" ]) sources = [d["source" ] for d in result["source_documents" ]] if case .get("expected_source" ) and any (case ["expected_source" ] in s for s in sources): results["retrieval_hit" ] += 1 if case .get("expected_keywords" ) and all (kw in result["answer" ] for kw in case ["expected_keywords" ]): results["answer_correct" ] += 1 for key in ["retrieval_hit" , "answer_correct" ]: results[f"{key} _rate" ] = results[key] / results["total" ] return results
准备一组测试用例(问题 + 期望答案中应包含的关键词 + 期望来源文档),定期跑一遍评估,确保系统迭代过程中质量不退化。
性能优化 索引阶段优化:
批量嵌入,别一条一条处理
GPU 做嵌入比 CPU 快 10 倍以上
如果文档量大(10万+),考虑用 Milvus 替代 ChromaDB
查询阶段优化:
嵌入模型做查询向量化通常只需 10-50ms
ChromaDB 检索在百万量级文档上也能做到 50ms 以内
主要瓶颈在 Hermes 生成这一步——具体怎么优化推理速度,看你选的 量化方案 和推理框架
RAG 不是一个搭完就不管的系统,它需要根据实际使用反馈持续调优——调整切分粒度、换嵌入模型、优化 prompt、改检索策略。cocoloop 社区里有不少同学在分享自己的 RAG 实践经验,遇到问题可以去交流。