RAG(Retrieval-Augmented Generation,检索增强生成)是一种将外部知识库与大语言模型结合的技术架构。它解决了LLM的两大核心痛点:
| LLM原生问题 | RAG如何解决 |
|---|---|
| 知识截止日期(无最新信息) | 实时从知识库检索最新文档 |
| 幻觉(编造不存在的事实) | 答案有文档来源,可溯源验证 |
| 无法处理私有数据 | 将企业内部文档入库后即可问答 |
| 上下文窗口有限 | 只注入相关片段,不需要全文 |
| 模型更新成本高 | 只更新知识库,无需重训模型 |
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 企业知识库问答 | RAG | 数据频繁更新,无需重训 |
| 特定领域写作风格 | Fine-tuning | 风格/格式偏好,非知识问题 |
| 实时信息检索 | RAG | Fine-tuning无法更新知识 |
| 专业术语理解 | 两者结合 | 术语理解用FT,详情用RAG |
| 客服问答系统 | RAG | FAQ/政策文档随时更新 |
pip install langchain langchain-community langchain-anthropic \
langchain-chroma pypdf sentence-transformers
from langchain_community.document_loaders import PyPDFLoader, DirectoryLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_community.vectorstores import Chroma
from langchain_anthropic import ChatAnthropic
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
import os
# ════════════════════════════════════════════════
# 阶段1:文档加载
# ════════════════════════════════════════════════
# 加载单个PDF
loader = PyPDFLoader("company_handbook.pdf")
# 或加载整个文件夹
# loader = DirectoryLoader("./docs/", glob="**/*.pdf", loader_cls=PyPDFLoader)
documents = loader.load()
print(f"加载了 {len(documents)} 页文档")
# ════════════════════════════════════════════════
# 阶段2:文档切片(关键步骤)
# ════════════════════════════════════════════════
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000, # 每块约1000个字符
chunk_overlap=200, # 块间重叠200字符(保持上下文连贯)
separators=["\n\n", "\n", "。", "!", "?", " ", ""], # 中文优先级
length_function=len,
)
chunks = text_splitter.split_documents(documents)
print(f"切分为 {len(chunks)} 个文本块")
# ════════════════════════════════════════════════
# 阶段3+4:向量嵌入 + 存储(Chroma本地)
# ════════════════════════════════════════════════
# 使用开源嵌入模型(免费,支持中文)
embeddings = HuggingFaceEmbeddings(
model_name="BAAI/bge-m3", # 多语言,中英文都很好
# model_name="BAAI/bge-large-zh", # 中文专优版本
model_kwargs={"device": "cpu"},
encode_kwargs={"normalize_embeddings": True}
)
# 创建/加载向量数据库
PERSIST_DIR = "./chroma_db"
if os.path.exists(PERSIST_DIR):
# 复用已有数据库
vectorstore = Chroma(
persist_directory=PERSIST_DIR,
embedding_function=embeddings
)
else:
# 首次创建
vectorstore = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory=PERSIST_DIR
)
print("向量数据库创建完成")
retriever = vectorstore.as_retriever(
search_type="similarity",
search_kwargs={"k": 5} # 检索最相关的5块
)
# ════════════════════════════════════════════════
# 阶段5+6:检索 + 生成(RAG Chain)
# ════════════════════════════════════════════════
llm = ChatAnthropic(model="claude-3-7-sonnet-20250219", temperature=0)
# RAG提示词模板
rag_prompt = ChatPromptTemplate.from_messages([
("system", """你是一个专业的文档问答助手。请严格基于以下检索到的文档内容回答问题。
如果文档中没有相关信息,请明确说明"根据提供的文档,我无法找到相关信息",不要编造答案。
检索到的相关文档:
{context}"""),
("human", "{question}")
])
def format_docs(docs):
"""格式化检索到的文档"""
return "\n\n".join([
f"[文档{i+1}] 来源:{doc.metadata.get('source', '未知')}\n{doc.page_content}"
for i, doc in enumerate(docs)
])
# LCEL构建RAG链
rag_chain = (
{
"context": retriever | format_docs,
"question": RunnablePassthrough()
}
| rag_prompt
| llm
| StrOutputParser()
)
# ── 使用 ─────────────────────────────────────────
questions = [
"公司的年假政策是什么?",
"如何申请报销?需要哪些材料?",
"试用期是多久?"
]
for q in questions:
print(f"\n问:{q}")
answer = rag_chain.invoke(q)
print(f"答:{answer}")
print("-" * 60)
pip install llama-index llama-index-llms-anthropic llama-index-embeddings-huggingface
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader, Settings
from llama_index.llms.anthropic import Anthropic
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
# 配置全局设置
Settings.llm = Anthropic(model="claude-3-7-sonnet-20250219")
Settings.embed_model = HuggingFaceEmbedding(model_name="BAAI/bge-m3")
Settings.chunk_size = 1024
Settings.chunk_overlap = 128
# 加载文档(自动处理PDF/TXT/MD等格式)
documents = SimpleDirectoryReader("./docs").load_data()
# 一行创建索引
index = VectorStoreIndex.from_documents(documents)
# 保存索引(避免重复计算)
index.storage_context.persist(persist_dir="./llamaindex_storage")
# 创建查询引擎
query_engine = index.as_query_engine(
similarity_top_k=5,
streaming=True # 流式输出
)
# 查询
response = query_engine.query("公司的核心价值观是什么?")
response.print_response_stream() # 流式打印
# 带来源引用的查询
print("\n来源文档:")
for node in response.source_nodes:
print(f" - 相似度:{node.score:.3f} | {node.metadata['file_name']}")
最适合快速原型和中小规模项目。数据存本地,零配置,直接pip install就能用。缺点是不支持分布式,百万级向量后性能下降。
无需运维,高可用,毫秒级查询。有免费层(100万向量),付费从$70/月起。最适合不想管基础设施的团队。
比Chroma快5-10倍,支持有效载荷过滤(按metadata筛选后再搜索),Docker一键部署,推荐需要自托管且有高性能需求的团队。
Meta出品,业界标准。纯内存操作,GPU加速时亿级向量秒级查询。不支持服务化,适合离线批处理场景。
| 数据库 | 规模 | 部署 | 价格 | 推荐场景 |
|---|---|---|---|---|
| Chroma | 100万向量内 | 本地 | 免费 | 原型/学习/小项目 |
| FAISS | 亿级 | 本地 | 免费 | 批处理/研究 |
| Qdrant | 1000万+ | 自托管/云 | 开源免费 | 高性能自托管 |
| Pinecone | 亿级 | 全托管 | 免费层+$70/月 | 企业生产 |
| Weaviate | 亿级 | 自托管/云 | 开源+SaaS | 多模态+图谱 |
| 模型 | 维度 | 中文 | 价格 | 推荐指数 |
|---|---|---|---|---|
| BAAI/bge-m3 | 1024 | 极好 | 免费(本地) | 9.5/10 |
| text-embedding-3-large | 3072 | 好 | $0.13/1M tokens | 9.0/10 |
| text-embedding-3-small | 1536 | 好 | $0.02/1M tokens | 8.5/10 |
| BAAI/bge-large-zh | 1024 | 中文最优 | 免费(本地) | 8.8/10 |
| Voyage-3 | 1024 | 好 | $0.06/1M tokens | 8.7/10 |
先让LLM生成一个"假设性答案",用这个答案去检索而不是用原始问题。显著提升检索召回率。
召回20个文档后,用Cross-Encoder重新打分,选取真正最相关的5个。准确率提升15-25%。
关键词搜索(BM25)+ 语义搜索(向量)结合,用RRF算法融合结果,全面覆盖精确匹配和语义理解。
用LLM将一个问题扩展为3-5个同义问题,分别检索后合并去重,大幅提高召回覆盖率。
小块存储用于精准检索,大块上下文用于生成。解决chunk过小导致上下文不足的问题。
将文档关系存为图谱,检索时不仅返回相似文档,还返回关联实体。适合复杂关系推理。
from langchain_core.prompts import ChatPromptTemplate
from langchain_anthropic import ChatAnthropic
from langchain_core.output_parsers import StrOutputParser
llm = ChatAnthropic(model="claude-3-7-sonnet-20250219", temperature=0.2)
# HyDE:先生成假设答案再检索
hyde_prompt = ChatPromptTemplate.from_template(
"""请为以下问题生成一个详细的假设性答案(即使你不确定,也要给出合理的答案)。
这个答案将用于文档检索,而不是直接给用户看。
问题:{question}
假设性答案:"""
)
hyde_chain = hyde_prompt | llm | StrOutputParser()
def hyde_retriever(question: str, vectorstore, k: int = 5):
"""使用HyDE技术进行检索"""
# 生成假设答案
hypothetical_answer = hyde_chain.invoke({"question": question})
# 用假设答案去检索(而不是原始问题)
docs = vectorstore.similarity_search(hypothetical_answer, k=k)
return docs
# 对比普通检索
normal_docs = vectorstore.similarity_search("公司有什么福利?", k=5)
hyde_docs = hyde_retriever("公司有什么福利?", vectorstore, k=5)
# HyDE通常能检索到更相关的文档
from sentence_transformers import CrossEncoder
# 加载重排序模型(约500MB)
reranker = CrossEncoder("BAAI/bge-reranker-v2-m3") # 支持中英文
def rerank_documents(query: str, documents, top_k: int = 3):
"""对检索结果重新排序"""
# 构建查询-文档对
pairs = [(query, doc.page_content) for doc in documents]
# 批量打分
scores = reranker.predict(pairs)
# 按分数排序
ranked = sorted(zip(scores, documents), reverse=True)
# 返回top_k个
return [doc for _, doc in ranked[:top_k]]
# 使用
raw_docs = retriever.invoke("公司的培训机会有哪些?") # 召回10个
reranked = rerank_documents("公司的培训机会有哪些?", raw_docs, top_k=3)
# 重排后只取最相关的3个,质量更高
| 指标 | 含义 | 目标值 | 评估工具 |
|---|---|---|---|
| Faithfulness | 答案是否忠实于检索文档 | >0.8 | RAGAS |
| Answer Relevancy | 答案是否回答了问题 | >0.75 | RAGAS |
| Context Precision | 检索文档的精准率 | >0.7 | RAGAS |
| Context Recall | 相关文档的召回率 | >0.7 | RAGAS |
| Latency | 端到端响应时间 | <3秒 | LangSmith |
| Cost per Query | 每次查询的API费用 | 依预算 | 自定义 |
pip install ragas
from ragas import evaluate
from ragas.metrics import faithfulness, answer_relevancy, context_precision
from datasets import Dataset
# 准备评估数据集
eval_data = {
"question": ["公司年假政策?", "报销需要什么材料?"],
"answer": ["...", "..."], # RAG系统输出
"contexts": [["检索到的文档1..."], ["检索到的文档2..."]], # 检索的上下文
"ground_truth": ["正确答案1", "正确答案2"] # 人工标注的正确答案
}
dataset = Dataset.from_dict(eval_data)
# 一键评估
result = evaluate(
dataset,
metrics=[faithfulness, answer_relevancy, context_precision]
)
print(result)
# 输出类似:{'faithfulness': 0.87, 'answer_relevancy': 0.82, 'context_precision': 0.75}