大模型系列之-RAG技术深度解析:从基础架构到实战应用

4 阅读7分钟

前言

一开始接触大模型问答的时候,发现直接问它一些最新的、或者公司内部的知识,它就开始胡说八道了,要么说“我的知识截止到...”,要么就一本正经地编答案,干!!

后来发现有个叫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系统都能精准检索、聪明回答!有什么问题欢迎留言讨论~