Contents

用Python+LangChain从零搭建RAG系统:让大模型读懂你的私有文档

为什么需要RAG?

大语言模型(LLM)虽然强大,但它有一个致命短板——知识截止日期。ChatGPT不知道你公司上周发布的内部文档,Claude不了解你项目特有的业务逻辑。直接让LLM回答私有领域的问题,轻则答非所问,重则一本正经地胡说八道。

RAG(Retrieval-Augmented Generation,检索增强生成) 就是解决这个问题的利器。它的核心思路非常简单:

  1. 检索:根据用户问题,从你的文档库中找到相关片段
  2. 增强:把这些片段作为上下文,注入到Prompt中
  3. 生成:让LLM基于这些上下文生成准确回答

这种方式不需要微调模型,成本低、见效快,是目前企业级AI应用最主流的架构方案。

技术栈选型

我们用到的技术栈:

  • LangChain:RAG框架,负责文档加载、切分、检索、链式调用
  • ChromaDB:轻量级向量数据库,本地运行,无需额外部署
  • OpenAI Embeddings:将文本转为向量表示
  • OpenAI GPT:生成回答的大模型

完整代码实现

1. 环境准备

1
pip install langchain langchain-openai langchain-community chromadb tiktoken

确保设置了OpenAI API Key:

1
export OPENAI_API_KEY="sk-your-api-key-here"

2. 加载文档

RAG的第一步是加载你的文档。LangChain支持多种数据源,这里我们以本地Markdown文件为例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from langchain_community.document_loaders import DirectoryLoader, TextLoader

# 加载指定目录下的所有.md文件
loader = DirectoryLoader(
    "./docs",
    glob="**/*.md",
    loader_cls=TextLoader,
    loader_kwargs={"encoding": "utf-8"}
)
documents = loader.load()
print(f"共加载 {len(documents)} 个文档片段")

如果你需要加载PDF或Word文档,只需替换对应的Loader:

1
2
3
4
5
6
7
# PDF文档
from langchain_community.document_loaders import PyPDFLoader
loader = PyPDFLoader("./docs/manual.pdf")

# Word文档
from langchain_community.document_loaders import Docx2txtLoader
loader = Docx2txtLoader("./docs/guide.docx")

3. 文档切分

原始文档通常很长,直接塞进上下文既浪费token又影响检索精度。我们需要把文档切成合适大小的片段:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from langchain.text_splitter import RecursiveCharacterTextSplitter

text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,       # 每个片段最大500个字符
    chunk_overlap=50,     # 相邻片段重叠50个字符,避免语义断裂
    separators=["\n##", "\n###", "\n\n", "\n", "。", "!", "?", " "],
    length_function=len,
)

chunks = text_splitter.split_documents(documents)
print(f"切分后共 {len(chunks)} 个片段")

关键参数说明

  • chunk_size:太大会浪费token,太小会丢失上下文,500-1000是常用区间
  • chunk_overlap:保证跨片段的语义连贯性
  • separators:按优先级拆分,先按标题、再按段落、最后按句子

4. 向量化并存入数据库

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import Chroma

# 初始化Embedding模型
embedding_model = OpenAIEmbeddings(model="text-embedding-3-small")

# 向量化并存入ChromaDB
vectorstore = Chroma.from_documents(
    documents=chunks,
    embedding=embedding_model,
    persist_directory="./chroma_db",   # 持久化到本地
    collection_name="my_docs"
)
print("向量数据库构建完成!")

5. 构建检索器

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 创建检索器,返回最相关的3个文档片段
retriever = vectorstore.as_retriever(
    search_type="similarity",
    search_kwargs={"k": 3}
)

# 测试检索效果
results = retriever.invoke("如何配置数据库连接?")
for i, doc in enumerate(results):
    print(f"\n--- 结果 {i+1} ---")
    print(f"来源: {doc.metadata.get('source', '未知')}")
    print(f"内容: {doc.page_content[:200]}")

6. 组装RAG Chain

这是核心部分,我们将检索和生成串联起来:

 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
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough

# 初始化LLM
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

# 定义Prompt模板
prompt = ChatPromptTemplate.from_template("""
你是一个专业的技术文档助手。请根据以下参考资料回答用户的问题。
如果参考资料中没有相关信息,请明确说明"根据现有文档无法回答",不要编造答案。

参考资料:
{context}

用户问题:{question}

请用简洁专业的中文回答:
""")

# 辅助函数:将检索到的文档片段格式化
def format_docs(docs):
    return "\n\n---\n\n".join([doc.page_content for doc in docs])

# 构建RAG Chain
rag_chain = (
    {
        "context": retriever | format_docs,
        "question": RunnablePassthrough()
    }
    | prompt
    | llm
    | StrOutputParser()
)

# 开始问答!
answer = rag_chain.invoke("这个项目的数据源是怎么配置的?")
print(answer)

7. 添加对话历史(可选)

支持多轮对话的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
from langchain_core.messages import HumanMessage, AIMessage
from langchain.chains import create_history_aware_retriever, create_retrieval_chain
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain_core.prompts import MessagesPlaceholder

# 上下文化检索器:能理解对话上下文来检索
contextualize_prompt = ChatPromptTemplate.from_messages([
    ("system", "根据聊天历史和最新的用户问题,生成一个独立的检索查询。"),
    MessagesPlaceholder("chat_history"),
    ("human", "{input}"),
])

history_aware_retriever = create_history_aware_retriever(
    llm, retriever, contextualize_prompt
)

# 对话QA链
qa_prompt = ChatPromptTemplate.from_messages([
    ("system", "你是技术文档助手。请根据参考资料回答问题。\n\n{context}"),
    MessagesPlaceholder("chat_history"),
    ("human", "{input}"),
])

question_answer_chain = create_stuff_documents_chain(llm, qa_prompt)
rag_chain = create_retrieval_chain(history_aware_retriever, question_answer_chain)

# 使用示例
chat_history = []

# 第一轮对话
response1 = rag_chain.invoke({"input": "项目的架构是什么?", "chat_history": chat_history})
chat_history.extend([
    HumanMessage(content="项目的架构是什么?"),
    AIMessage(content=response1["answer"])
])

# 第二轮对话(能理解上下文)
response2 = rag_chain.invoke({"input": "那数据库用的是什么?", "chat_history": chat_history})
print(response2["answer"])

常见问题与优化技巧

1. 检索质量不高怎么办?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# 使用混合检索:向量检索 + 关键词检索
from langchain.retrievers import EnsembleRetriever
from langchain_community.retrievers import BM25Retriever

# BM25关键词检索器
bm25_retriever = BM25Retriever.from_documents(chunks)
bm25_retriever.k = 3

# 向量检索器
vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 3})

# 混合检索,各占50%权重
ensemble_retriever = EnsembleRetriever(
    retrievers=[bm25_retriever, vector_retriever],
    weights=[0.5, 0.5]
)

2. 响应太慢?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# 方案一:使用更快的Embedding模型
embedding_model = OpenAIEmbeddings(model="text-embedding-3-small")

# 方案二:缓存检索结果
from langchain_community.vectorstores import Chroma
vectorstore = Chroma.from_documents(
    documents=chunks,
    embedding=embedding_model,
    persist_directory="./chroma_db"
)
# ChromaDB默认会缓存到本地磁盘,第二次查询直接读缓存

# 方案三:使用流式输出
for chunk in rag_chain.stream("你的问题"):
    print(chunk, end="", flush=True)

3. 回答出现幻觉?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 添加引用来源,让回答可追溯
rag_prompt_with_citation = ChatPromptTemplate.from_template("""
你是一个技术文档助手。请根据参考资料回答用户问题。

要求:
1. 回答时必须标注引用来源(文件名)
2. 如果参考资料不足,明确说明
3. 不要编造任何信息

参考资料:
{context}

用户问题:{question}
""")

完整项目结构

1
2
3
4
5
6
7
8
my-rag-app/
├── docs/                    # 你的文档目录
│   ├── guide.md
│   └── faq.md
├── chroma_db/               # 向量数据库(自动生成)
├── main.py                  # 主程序
├── requirements.txt         # 依赖
└── README.md

requirements.txt:

1
2
3
4
5
langchain>=0.3.0
langchain-openai>=0.2.0
langchain-community>=0.3.0
chromadb>=0.5.0
tiktoken>=0.7.0

总结

RAG的核心流程就是三步:加载切分 → 向量化存储 → 检索生成。相比微调模型,RAG有明显的优势:

  • 成本低:不需要GPU训练,API调用按量付费
  • 更新快:新增文档只需重新索引,无需重训模型
  • 可解释:能标注引用来源,回答有据可查
  • 灵活性强:可以随时调整切分策略、检索方式、Prompt模板

实际项目中,建议先用小规模文档验证效果,再逐步扩展。同时关注检索质量的优化,这是决定RAG系统效果的关键瓶颈。如果检索不准,生成再好的模型也白搭。