前言
一开始接触大模型问答的时候,发现直接问它一些最新的、或者公司内部的知识,它就开始胡说八道了,要么说“我的知识截止到...”,要么就一本正经地编答案,干!!
后来发现有个叫RAG(检索增强生成) 的技术能解决这个问题,说白了就是“不会就查,查完再答”。我用下来之后发现,这玩意儿是真香,但坑也是真多。今天就把我折腾RAG的实战经验,从核心架构到优化技巧,一次性给你讲明白。
核心架构:拆开看看里面都有啥
一个完整的RAG系统,其实就像个聪明的图书管理员。你问问题,它先去资料库(向量数据库)里翻找最相关的几本书(文档片段),然后把这几页内容和你问题一起,交给大模型去组织答案。
整个流程拆开看,主要就这几块:
1. 文档处理流水线(Ingestion Pipeline)
这是最基础也最容易出问题的地方。你的PDF、Word、网页,都得先变成大模型能“吃”的格式。
# 一个简单的文档处理示例,用LangChainfrom langchain.document_loaders import PyPDFLoaderfrom langchain.text_splitter import RecursiveCharacterTextSplitter# 加载文档loader = PyPDFLoader("公司手册.pdf")documents = loader.load()# 切分文档 - 这里就有讲究了!text_splitter = RecursiveCharacterTextSplitter( chunk_size=500, # 每个片段500字符 chunk_overlap=50, # 重叠50字符,避免信息被切断 separators=["\n\n", "\n", "。", ",", " ", ""] # 按这些符号优先切分)chunks = text_splitter.split_documents(documents)
附赠小技巧:
chunk_size别瞎设!太大会导致检索不精准,太小又会让模型看不到完整上下文。我试下来,500-1000字符对于中文文档比较合适。
2. 向量化与检索(Embedding & Retrieval)
这是RAG的“检索”部分核心。简单说就是把文字变成一串数字(向量),然后计算相似度。
from langchain.embeddings import HuggingFaceEmbeddingsfrom langchain.vectorstores import Chroma# 使用开源的embedding模型embeddings = HuggingFaceEmbeddings( model_name="BAAI/bge-small-zh-v1.5", # 中文小模型,效果不错 model_kwargs={'device': 'cpu'}, # 没GPU也能跑 encode_kwargs={'normalize_embeddings': True} # 归一化,提高检索精度)# 创建向量数据库vectorstore = Chroma.from_documents( documents=chunks, embedding=embeddings, persist_directory="./chroma_db"# 保存到本地,下次不用重新生成)
也不卖关子,这里有几个关键选择:
- Embedding模型:中文千万别用OpenAI的text-embedding-ada-002(虽然它英文很强),对中文支持不好。推荐用
BAAI/bge系列或者m3e系列。 - 向量数据库:小项目用Chroma(轻量,本地运行),生产环境可以考虑Qdrant、Weaviate或者Pinecone(云服务)。
3. 增强生成(Augmented Generation)
这是最后一步,把检索到的文档和问题一起喂给大模型。
from langchain.chains import RetrievalQAfrom langchain.llms import ChatGLM# 初始化本地大模型llm = ChatGLM( endpoint="http://localhost:8000", # 本地部署的ChatGLM API max_tokens=1024, temperature=0.1# 温度调低,让答案更确定)# 创建RAG链qa_chain = RetrievalQA.from_chain_type( llm=llm, chain_type="stuff", # 最简单的方式:把所有检索到的文档拼起来一起问 retriever=vectorstore.as_retriever( search_kwargs={"k": 3} # 检索前3个最相关的片段 ), return_source_documents=True# 返回源文档,方便调试)# 提问!result = qa_chain("我们公司的年假政策是怎样的?")print(result["result"])print("参考来源:", result["source_documents"])
优化策略:从“能用”到“好用”
如果你按上面的步骤搭起来,会发现基本功能是有了,但效果可能不太理想。下面是我踩过坑后总结的优化技巧:
1. 检索优化:让找东西更准
- 问题重写:用户问“咋请假?”,但文档里写的是“年假申请流程”。需要在检索前把问题改写成“年假申请流程”。
# 用大模型先重写问题rewrite_prompt = """请将以下用户问题重写为更适合文档检索的查询语句。用户问题:{question}重写后的查询:"""# 先调用一次LLM重写问题,再用重写后的问题去检索
- 混合检索:光用向量检索(语义搜索)不够,再加个关键词检索(稀疏搜索)。比如“2024年Q2财报”,其中“2024”、“Q2”这些关键词很重要。
from langchain.retrievers import BM25Retriever, EnsembleRetriever# 创建两个检索器vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 3})bm25_retriever = BM25Retriever.from_documents(chunks)# 组合起来ensemble_retriever = EnsembleRetriever( retrievers=[bm25_retriever, vector_retriever], weights=[0.4, 0.6] # 调整权重,我试下来0.4/0.6效果不错)
2. 提示词优化:让模型答得更好
直接扔文档给模型,它可能抓不住重点。得在提示词里“教”它怎么用这些文档。
# 改进后的提示词模板RAG_PROMPT_TEMPLATE = """你是一个专业的问答助手,请根据以下提供的参考文档来回答问题。如果文档中没有相关信息,请直接说“根据提供的资料,我无法回答这个问题”,不要编造答案。参考文档:{context}用户问题:{question}请按照以下格式回答:1. 首先给出直接答案2. 然后说明这个答案的依据(引用文档中的具体内容)3. 如果有相关流程或注意事项,一并列出回答:"""
附赠小技巧:在提示词里加上“如果不知道就说不知道”,能减少80%的胡编乱造!贴不贴心吧?
3. 评估与迭代:怎么知道效果好不好
搭完RAG不能就完事了,得有个评估方法。
# 简单的评估脚本test_questions = [ "年假有多少天?", "请假需要谁审批?", "病假需要什么证明?"]for question in test_questions: result = qa_chain(question) print(f"问题:{question}") print(f"答案:{result['result'][:200]}...") # 只打印前200字符 print(f"检索到的文档数:{len(result['source_documents'])}") print("-" * 50) # 手动评估:1. 答案是否正确 2. 是否引用了相关文档 # 生产环境可以用GPT-4来自动评估,但那个要花钱...
部署实战:让RAG跑起来
理论说完了,来点实际的。假设我们要部署一个公司知识库问答系统。
项目结构:
company-rag/├── docs/ # 原始文档│ ├── 员工手册.pdf│ ├── 财务制度.docx│ └── 产品介绍.md├── ingest.py # 文档处理脚本├── query.py # 查询接口├── chroma_db/ # 向量数据库(自动生成)└── requirements.txt # 依赖包
requirements.txt:
langchain==0.1.0chromadb==0.4.22sentence-transformers==2.2.2pypdf==3.17.0python-docx==1.1.0fastapi==0.104.1uvicorn==0.24.0
ingest.py(文档处理):
import osfrom pathlib import Path# ... 上面的文档处理代码if __name__ == "__main__": # 处理docs目录下所有文档 docs_dir = Path("./docs") all_chunks = [] for file_path in docs_dir.glob("**/*"): if file_path.suffix == ".pdf": loader = PyPDFLoader(str(file_path)) elif file_path.suffix == ".docx": from langchain.document_loaders import Docx2txtLoader loader = Docx2txtLoader(str(file_path)) elif file_path.suffix == ".md": from langchain.document_loaders import TextLoader loader = TextLoader(str(file_path)) else: continue documents = loader.load() chunks = text_splitter.split_documents(documents) all_chunks.extend(chunks) print(f"已处理:{file_path.name},得到{len(chunks)}个片段") # 创建向量数据库 vectorstore = Chroma.from_documents( documents=all_chunks, embedding=embeddings, persist_directory="./chroma_db" ) print("向量数据库构建完成!")
query.py(Web接口):
from fastapi import FastAPIfrom pydantic import BaseModelapp = FastAPI()class QueryRequest(BaseModel): question: str# 启动时加载QA链(实际生产要加缓存)qa_chain = None# 这里初始化你的QA链@app.post("/query")asyncdef query(request: QueryRequest): result = qa_chain(request.question) return { "answer": result["result"], "sources": [doc.metadata.get("source", "") for doc in result["source_documents"]] }if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8000)
运行起来:
# 先处理文档python ingest.py# 启动服务python query.py# 访问 http://localhost:8000/docs 就能看到API文档了
后记
RAG这东西,说起来简单,就是“检索+生成”,但真想做好,每个环节都有讲究。文档怎么切、用什么embedding模型、怎么设计提示词、怎么评估效果...都是坑。
我用下来之后发现,最大的经验就是:别想一次性完美。先搭个最简单的版本跑起来,然后找一些真实问题去测试,看哪里出问题了就优化哪里。可能是检索不准,那就优化检索;可能是生成不好,那就优化提示词。
另外,RAG虽然解决了大模型的“知识更新”和“幻觉”问题,但它也不是万能的。对于需要复杂推理、数学计算或者创造性写作的任务,还是直接问大模型可能更好。
最后,祝各位的RAG系统都能精准检索、聪明回答!有什么问题欢迎留言讨论~