为什么需要RAG?
大语言模型(LLM)虽然强大,但它有一个致命短板——知识截止日期。ChatGPT不知道你公司上周发布的内部文档,Claude不了解你项目特有的业务逻辑。直接让LLM回答私有领域的问题,轻则答非所问,重则一本正经地胡说八道。
RAG(Retrieval-Augmented Generation,检索增强生成) 就是解决这个问题的利器。它的核心思路非常简单:
- 检索:根据用户问题,从你的文档库中找到相关片段
- 增强:把这些片段作为上下文,注入到Prompt中
- 生成:让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系统效果的关键瓶颈。如果检索不准,生成再好的模型也白搭。