第27章 框架实操:用 LangChain/LlamaIndex 搭建完整 RAG 系统
大白话一句话: 前面六章学了RAG的每个零件,这章把它们组装成一台能跑的"整车"------用LangChain和LlamaIndex两个框架,从零搭建一个生产级RAG系统。
27.1 为什么需要框架
27.1.1 不用框架的痛
前面几章的代码都是"手写"的------自己写文档加载、自己写切分、自己写检索、自己写生成。好处是每一步都看得懂,坏处是:
| 痛点 | 具体表现 |
|---|---|
| 胶水代码太多 | 光把各环节串起来就要写几百行 |
| 换组件成本高 | 想从Chroma换成Milvus,检索代码全要改 |
| 缺少生产特性 | 流式输出、重试、缓存、链路追踪都得自己写 |
| 维护困难 | 代码散落各处,改一个地方怕影响全局 |
27.1.2 框架解决什么
LangChain 和 LlamaIndex 是当前最主流的两个RAG框架,它们的核心价值是:
框架 = 标准化接口 + 预置组件 + 可组合管道 + 生产级特性
标准化接口:所有向量数据库用同一套API,换底层只需改一行配置
预置组件:文档加载器、切分器、Embedding、检索器、生成器全部内置
可组合管道:像搭积木一样组装Pipeline,支持链式调用
生产级特性:流式输出、缓存、异步、链路追踪、重试机制开箱即用
27.1.3 LangChain vs LlamaIndex 怎么选
| 维度 | LangChain | LlamaIndex |
|---|---|---|
| 定位 | 通用LLM应用框架 | 专注数据/文档的RAG框架 |
| 优势 | 生态大、组件多、Agent能力强 | RAG做得更精细、文档处理更强 |
| 劣势 | 偏重、抽象层多、版本变化快 | Agent能力相对弱 |
| 适合场景 | Agent + RAG混合应用 | 纯RAG/文档问答 |
| 学习曲线 | 中等偏陡 | 相对平缓 |
| 社区活跃度 | 最高 | 高 |
大白话建议: 纯RAG项目选LlamaIndex,Agent+RAG混合选LangChain。两个都值得了解,本本章两个框架的完整实战都会讲。先学LlamaIndex上手快,再学LangChain扩展性强。
27.2 LlamaIndex 实战:30行代码跑通RAG
27.2.1 环境安装
bash
# 基础安装
pip install llama-index llama-index-embeddings-huggingface
pip install llama-index-llms-openai-like # 兼容OpenAI格式的API
pip install llama-index-vector-stores-chroma chromadb
# 如果用本地模型
pip install llama-index-llms-ollama
27.2.2 最简RAG:30行代码
python
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader, Settings
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
from llama_index.llms.openai_like import OpenAILike
# 全局配置
Settings.embed_model = HuggingFaceEmbedding(
model_name="BAAI/bge-large-zh-v1.5"
)
Settings.llm = OpenAILike(
model="qwen2.5:7b",
api_base="http://localhost:11434/v1",
api_key="not-needed",
temperature=0.1
)
# 1. 加载文档
documents = SimpleDirectoryReader("./docs").load_data()
print(f"加载了 {len(documents)} 个文档")
# 2. 构建索引(自动切分 + 向量化 + 存储)
index = VectorStoreIndex.from_documents(documents)
# 3. 创建查询引擎
query_engine = index.as_query_engine(
similarity_top_k=5, # 检索Top-5
response_mode="compact", # 回答模式:紧凑
)
# 4. 提问
response = query_engine.query("Qwen2.5支持多少种语言?")
print(response)
# 5. 查看检索到的来源
for i, node in enumerate(response.source_nodes, 1):
print(f"[{i}] score={node.score:.3f} | {node.text[:80]}...")
就这30行代码,跑通了一个完整的RAG系统:文档加载→切分→向量化→存储→检索→生成。
27.2.3 LlamaIndex的抽象层次
LlamaIndex提供三个层次的API,从简到繁:
高层次(High-Level):30行代码跑通
└── VectorStoreIndex.from_documents() → query()
中层次(Mid-Level):可定制每个环节
└── ServiceContext + Retriever + ResponseSynthesizer
低层次(Low-Level):完全控制
└── Document + Node + Embedding + VectorStore + Retriever + LLM
生产项目通常用中层次API------既不至于太黑盒,又不用写太多胶水代码。
27.3 LlamaIndex 进阶:定制每个环节
27.3.1 定制文档处理
python
from llama_index.core import Document
from llama_index.core.node_parser import (
SentenceSplitter,
SemanticSplitterNodeParser,
MarkdownNodeParser,
)
from llama_index.core.schema import MetadataMode
# ===== 1. 文档加载 =====
def load_documents(directory: str) -> list:
"""加载目录下所有文档"""
from llama_index.core import SimpleDirectoryReader
reader = SimpleDirectoryReader(
input_dir=directory,
required_exts=[".pdf", ".docx", ".md", ".txt", ".html"],
recursive=True, # 递归子目录
)
documents = reader.load_data()
# 添加全局元数据
for doc in documents:
doc.metadata.update({
"loaded_at": "2024-01-01",
"project": "AI开发指南",
})
return documents
# ===== 2. 文档切分 =====
def split_documents(documents: list, strategy: str = "recursive"):
"""
切分策略:
- recursive: 递归切分(推荐通用场景)
- semantic: 语义切分(高质量需求)
- markdown: Markdown结构切分(有标题的文档)
"""
if strategy == "recursive":
splitter = SentenceSplitter(
chunk_size=400,
chunk_overlap=60,
separator="\n\n",
secondary_chunking_regex="[^,.;。!?]+[,.;。!?]?",
)
elif strategy == "semantic":
splitter = SemanticSplitterNodeParser(
buffer_size=1,
breakpoint_percentile_threshold=95,
embed_model=Settings.embed_model,
)
elif strategy == "markdown":
splitter = MarkdownNodeParser()
else:
raise ValueError(f"未知策略: {strategy}")
nodes = splitter.get_nodes_from_documents(documents)
print(f"切分完成:{len(documents)}个文档 → {len(nodes)}个片段")
return nodes
# ===== 3. 数据清洗 =====
def clean_nodes(nodes: list) -> list:
"""清洗切分后的节点"""
import re
cleaned = []
for node in nodes:
text = node.text
# 去除多余空白
text = re.sub(r'\n{3,}', '\n\n', text)
text = re.sub(r' {2,}', ' ', text)
# 去除URL和邮箱
text = re.sub(r'http[s]?://\S+', '', text)
text = re.sub(r'\S+@\S+', '', text)
# 过滤太短的片段
if len(text.strip()) < 50:
continue
node.text = text.strip()
cleaned.append(node)
print(f"清洗完成:{len(nodes)} → {len(cleaned)} 个有效片段")
return cleaned
# 完整文档处理Pipeline
documents = load_documents("./docs")
nodes = split_documents(documents, strategy="recursive")
nodes = clean_nodes(nodes)
27.3.2 定制向量存储
python
from llama_index.vector_stores.chroma import ChromaVectorStore
from llama_index.core import VectorStoreIndex, StorageContext
import chromadb
# ===== 方案一:Chroma(开发推荐) =====
def create_chroma_index(nodes):
"""使用Chroma向量数据库"""
db = chromadb.PersistentClient(path="./chroma_db")
chroma_store = ChromaVectorStore(
collection_name="knowledge_base"
)
storage_context = StorageContext.from_defaults(
vector_store=chroma_store
)
index = VectorStoreIndex(
nodes=nodes,
storage_context=storage_context,
)
return index
# ===== 方案二:PostgreSQL + pgvector(生产推荐) =====
def create_pgvector_index(nodes):
"""使用PgVector向量数据库"""
from llama_index.vector_stores.postgres import PGVectorStore
vector_store = PGVectorStore.from_params(
database="rag_db",
host="localhost",
port="5432",
user="postgres",
password="password",
table_name="knowledge_base",
embed_dim=1024, # BGE-large维度
)
storage_context = StorageContext.from_defaults(
vector_store=vector_store
)
index = VectorStoreIndex(
nodes=nodes,
storage_context=storage_context,
)
return index
# ===== 方案三:Milvus(大规模推荐) =====
def create_milvus_index(nodes):
"""使用Milvus向量数据库"""
from llama_index.vector_stores.milvus import MilvusVectorStore
vector_store = MilvusVectorStore(
uri="http://localhost:19530",
collection_name="knowledge_base",
dim=1024,
overwrite=False, # 不覆盖已有数据
)
storage_context = StorageContext.from_defaults(
vector_store=vector_store
)
index = VectorStoreIndex(
nodes=nodes,
storage_context=storage_context,
)
return index
27.3.3 定制检索器
python
from llama_index.core.retrievers import (
VectorIndexRetriever,
QueryFusionRetriever,
)
from llama_index.retrievers.bm25 import BM25Retriever
from llama_index.core.postprocessor import (
SimilarityPostprocessor,
KeywordNodePostprocessor,
)
# ===== 基础向量检索器 =====
def create_vector_retriever(index, top_k: int = 10):
"""向量检索器"""
return VectorIndexRetriever(
index=index,
similarity_top_k=top_k,
)
# ===== 混合检索器(向量 + BM25) =====
def create_hybrid_retriever(index, nodes, top_k: int = 10):
"""混合检索:向量 + BM25,用RRF融合"""
vector_retriever = VectorIndexRetriever(
index=index,
similarity_top_k=top_k,
)
bm25_retriever = BM25Retriever.from_defaults(
nodes=nodes,
similarity_top_k=top_k,
)
# RRF融合
hybrid_retriever = QueryFusionRetriever(
retrievers=[vector_retriever, bm25_retriever],
similarity_top_k=top_k,
num_queries=1, # 不做查询扩展
mode="reciprocal_rerank", # RRF
)
return hybrid_retriever
# ===== 后处理器:过滤 + 重排序 =====
def create_postprocessors():
"""后处理Pipeline"""
return [
# 1. 过滤低分结果
SimilarityPostprocessor(similarity_cutoff=0.5),
# 2. 关键词过滤(排除/要求包含)
# KeywordNodePostprocessor(
# exclude_keywords=["废弃", "已删除"],
# require_keywords=[],
# ),
]
# ===== 重排序器 =====
def create_reranker():
"""BGE Reranker重排序"""
from llama_index.core.postprocessor import SentenceTransformerRerank
return SentenceTransformerRerank(
model="BAAI/bge-reranker-v2-m3",
top_n=5, # 重排后取Top-5
)
27.3.4 完整定制Pipeline
python
from llama_index.core import get_response_synthesizer
from llama_index.core.query_engine import RetrieverQueryEngine
from llama_index.core.prompts import PromptTemplate
# 定制回答Prompt
QA_PROMPT = PromptTemplate("""
你是一个严谨的知识库助手。请严格基于下方【参考资料】回答问题。
## 回答规则
1. 只基于参考资料回答,不要使用自身知识
2. 如果资料中没有相关信息,回答"根据现有资料,我无法回答该问题"
3. 不要编造或推断资料中不存在的信息
4. 在关键信息后标注来源[资料X]
5. 回答控制在200-500字
## 参考资料
{context_str}
## 用户问题
{query_str}
## 回答
""")
def build_rag_pipeline(index, nodes):
"""构建完整的RAG Pipeline"""
# 1. 检索器(混合检索)
retriever = create_hybrid_retriever(index, nodes, top_k=10)
# 2. 后处理器(过滤 + 重排序)
postprocessors = create_postprocessors()
postprocessors.append(create_reranker()) # 最后一步重排序
# 3. 回答合成器
response_synthesizer = get_response_synthesizer(
response_mode="compact",
text_qa_template=QA_PROMPT,
)
# 4. 组装查询引擎
query_engine = RetrieverQueryEngine(
retriever=retriever,
response_synthesizer=response_synthesizer,
node_postprocessors=postprocessors,
)
return query_engine
# 完整使用
print("=" * 60)
print("构建RAG Pipeline...")
print("=" * 60)
# Step 1: 文档处理
documents = load_documents("./docs")
nodes = split_documents(documents, strategy="recursive")
nodes = clean_nodes(nodes)
# Step 2: 构建索引
index = create_chroma_index(nodes)
# Step 3: 构建Pipeline
query_engine = build_rag_pipeline(index, nodes)
# Step 4: 提问
questions = [
"Qwen2.5支持多少种语言?",
"LoRA和QLoRA有什么区别?",
"如何减少大模型的幻觉?",
]
for q in questions:
print(f"\n{'='*60}")
print(f"问题:{q}")
print(f"{'='*60}")
response = query_engine.query(q)
print(f"\n回答:{response}")
print(f"\n来源:")
for i, node in enumerate(response.source_nodes, 1):
score = node.score or 0
source = node.metadata.get("file_name", "未知")
print(f" [{i}] score={score:.3f} | {source} | {node.text[:60]}...")
27.4 LangChain 实战:模块化RAG
27.4.1 环境安装
bash
pip install langchain langchain-community
pip install langchain-huggingface langchain-openai
pip install langchain-chroma langchain-postgres
pip install rank-bm25 sentence-transformers
27.4.2 LangChain RAG 架构
LangChain的设计哲学是"组合"------每个环节都是独立组件,通过Chain串联:
文档加载器 → 文本切分器 → Embedding模型 → 向量数据库
↓
用户问题 → 查询改写 → 检索器 → 重排序器 → 上下文组装 → LLM → 回答
27.4.3 完整LangChain RAG实现
python
from langchain_community.document_loaders import (
DirectoryLoader,
TextLoader,
PyPDFLoader,
Docx2txtLoader,
UnstructuredMarkdownLoader,
)
from langchain_text_splitters import (
RecursiveCharacterTextSplitter,
MarkdownHeaderTextSplitter,
)
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_chroma import Chroma
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough, RunnableParallel
from langchain_core.output_parsers import StrOutputParser
from langchain_community.retrievers import BM25Retriever
from langchain.retrievers import (
ContextualCompressionRetriever,
EnsembleRetriever,
)
from langchain.retrievers.document_compressors import (
DocumentCompressorPipeline,
EmbeddingsFilter,
)
import os
# ========================================
# 1. 文档加载
# ========================================
class DocumentLoader:
"""多格式文档加载器"""
LOADERS = {
".txt": TextLoader,
".pdf": PyPDFLoader,
".docx": Docx2txtLoader,
".md": UnstructuredMarkdownLoader,
}
def __init__(self, directory: str):
self.directory = directory
def load(self) -> list:
"""加载目录下所有支持的文档"""
from pathlib import Path
documents = []
for file_path in Path(self.directory).rglob("*"):
if file_path.suffix.lower() in self.LOADERS:
loader = self.LOADERS[file_path.suffix.lower()](
str(file_path)
)
docs = loader.load()
# 添加文件名作为元数据
for doc in docs:
doc.metadata["source"] = file_path.name
doc.metadata["file_type"] = file_path.suffix
documents.extend(docs)
print(f"加载完成:{len(documents)} 个文档")
return documents
# ========================================
# 2. 文档切分
# ========================================
class DocumentSplitter:
"""文档切分器"""
@staticmethod
def split(documents: list, chunk_size: int = 400, overlap: int = 60):
"""递归切分"""
splitter = RecursiveCharacterTextSplitter(
chunk_size=chunk_size,
chunk_overlap=overlap,
separators=["\n\n", "\n", "。", "!", "?", ".", "!", "?", " ", ""],
)
chunks = splitter.split_documents(documents)
print(f"切分完成:{len(documents)} 文档 → {len(chunks)} 片段")
return chunks
@staticmethod
def split_markdown(documents: list, chunk_size: int = 400):
"""Markdown结构切分"""
headers_to_split_on = [
("#", "Header 1"),
("##", "Header 2"),
("###", "Header 3"),
]
md_splitter = MarkdownHeaderTextSplitter(
headers_to_split_on=headers_to_split_on
)
# 先按Markdown结构切
md_chunks = []
for doc in documents:
md_chunks.extend(md_splitter.split_text(doc.page_content))
# 再按长度切
return DocumentSplitter.split(md_chunks, chunk_size=chunk_size)
# ========================================
# 3. 向量化 + 存储
# ========================================
class VectorStoreManager:
"""向量数据库管理"""
def __init__(self, embedding_model: str = "BAAI/bge-large-zh-v1.5"):
self.embeddings = HuggingFaceEmbeddings(
model_name=embedding_model,
model_kwargs={"device": "cpu"},
encode_kwargs={"normalize_embeddings": True},
)
self.vectorstore = None
def create_from_documents(self, documents: list, persist_dir: str = "./chroma_db"):
"""从文档创建向量数据库"""
self.vectorstore = Chroma.from_documents(
documents=documents,
embedding=self.embeddings,
persist_directory=persist_dir,
collection_name="knowledge_base",
)
print(f"向量数据库创建完成:{len(documents)} 条记录")
return self.vectorstore
def load_existing(self, persist_dir: str = "./chroma_db"):
"""加载已有向量数据库"""
self.vectorstore = Chroma(
persist_directory=persist_dir,
embedding_function=self.embeddings,
collection_name="knowledge_base",
)
return self.vectorstore
# ========================================
# 4. 检索器
# ========================================
class HybridRetriever:
"""混合检索器:向量 + BM25"""
def __init__(self, vectorstore, documents, top_k: int = 10):
self.vector_retriever = vectorstore.as_retriever(
search_type="similarity_score_threshold",
search_kwargs={"k": top_k, "score_threshold": 0.3},
)
self.bm25_retriever = BM25Retriever.from_documents(documents)
self.bm25_retriever.k = top_k
# 集成检索器(自动融合)
self.ensemble_retriever = EnsembleRetriever(
retrievers=[self.vector_retriever, self.bm25_retriever],
weights=[0.7, 0.3], # 向量权重70%,BM25权重30%
)
def retrieve(self, query: str) -> list:
"""检索"""
return self.ensemble_retriever.invoke(query)
# ========================================
# 5. 重排序器
# ========================================
class Reranker:
"""重排序器"""
def __init__(self, model_name: str = "BAAI/bge-reranker-v2-m3"):
from sentence_transformers import CrossEncoder
self.model = CrossEncoder(model_name)
def rerank(self, query: str, documents: list, top_n: int = 5) -> list:
"""重排序"""
pairs = [(query, doc.page_content) for doc in documents]
scores = self.model.predict(pairs)
# 按分数排序
scored_docs = list(zip(documents, scores))
scored_docs.sort(key=lambda x: x[1], reverse=True)
# 返回Top-N
result = []
for doc, score in scored_docs[:top_n]:
doc.metadata["rerank_score"] = float(score)
result.append(doc)
return result
# ========================================
# 6. 上下文组装
# ========================================
def format_context(documents: list) -> str:
"""格式化检索结果为上下文文本"""
parts = []
for i, doc in enumerate(documents, 1):
source = doc.metadata.get("source", "未知")
content = doc.page_content.strip()
parts.append(f"【资料{i}】来源:{source}\n{content}")
return "\n\n---\n\n".join(parts)
# ========================================
# 7. RAG Chain
# ========================================
def build_rag_chain(retriever, reranker, llm):
"""构建RAG Chain"""
# Prompt模板
prompt = ChatPromptTemplate.from_template("""
你是一个严谨的知识库助手。请严格基于下方【参考资料】回答问题。
## 回答规则
1. 只基于参考资料回答,不要使用自身训练数据中的知识
2. 如果资料中没有相关信息,回答"根据现有资料,我无法回答该问题"
3. 不要编造或推断资料中不存在的信息
4. 在关键信息后标注来源[资料X]
5. 回答控制在200-500字
## 参考资料
{context}
## 用户问题
{question}
## 回答
""")
# 定义Chain
def retrieve_and_rerank(question):
"""检索 + 重排序"""
docs = retriever.retrieve(question)
if reranker:
docs = reranker.rerank(question, docs, top_n=5)
return docs
# LCEL Chain(LangChain Expression Language)
rag_chain = (
{
"context": retrieve_and_rerank | format_context,
"question": RunnablePassthrough(),
}
| prompt
| llm
| StrOutputParser()
)
return rag_chain
# ========================================
# 8. 流式输出Chain
# ========================================
def build_streaming_chain(retriever, reranker, llm):
"""流式输出RAG Chain"""
prompt = ChatPromptTemplate.from_template(RAG_PROMPT_TEXT)
def retrieve_and_rerank(question):
docs = retriever.retrieve(question)
if reranker:
docs = reranker.rerank(question, docs, top_n=5)
return docs
rag_chain = (
{
"context": retrieve_and_rerank | format_context,
"question": RunnablePassthrough(),
}
| prompt
| llm
| StrOutputParser()
)
return rag_chain
RAG_PROMPT_TEXT = """
你是一个严谨的知识库助手。请严格基于下方【参考资料】回答问题。
## 回答规则
1. 只基于参考资料回答,不要使用自身知识
2. 如果资料中没有相关信息,回答"根据现有资料,我无法回答该问题"
3. 不要编造或推断资料中不存在的信息
4. 在关键信息后标注来源[资料X]
5. 回答控制在200-500字
## 参考资料
{context}
## 用户问题
{question}
## 回答
"""
# ========================================
# 9. 完整使用
# ========================================
def main():
"""完整RAG系统使用示例"""
# 配置LLM
llm = ChatOpenAI(
model="qwen2.5:7b",
base_url="http://localhost:11434/v1",
api_key="not-needed",
temperature=0.1,
max_tokens=1000,
)
# Step 1: 加载文档
loader = DocumentLoader("./docs")
documents = loader.load()
# Step 2: 切分
chunks = DocumentSplitter.split(documents, chunk_size=400, overlap=60)
# Step 3: 向量化 + 存储
vs_manager = VectorStoreManager("BAAI/bge-large-zh-v1.5")
vectorstore = vs_manager.create_from_documents(chunks)
# Step 4: 检索器
retriever = HybridRetriever(vectorstore, chunks, top_k=10)
# Step 5: 重排序器
reranker = Reranker("BAAI/bge-reranker-v2-m3")
# Step 6: 构建Chain
rag_chain = build_rag_chain(retriever, reranker, llm)
# Step 7: 提问
question = "Qwen2.5支持多少种语言?"
print(f"问题:{question}")
print(f"回答:{rag_chain.invoke(question)}")
# 流式输出
print("\n--- 流式输出 ---")
question2 = "LoRA和QLoRA有什么区别?"
print(f"问题:{question2}")
for chunk in rag_chain.stream(question2):
print(chunk, end="", flush=True)
print()
if __name__ == "__main__":
main()
27.5 生产级RAG系统架构
27.5.1 系统架构图
┌─────────────────────────────────────────────────────────┐
│ 用户层(前端) │
│ Web界面 / API接口 / 流式输出 / 反馈收集 │
└──────────────────────────┬──────────────────────────────┘
│
┌──────────────────────────▼──────────────────────────────┐
│ API网关层(FastAPI) │
│ 请求路由 / 鉴权 / 限流 / 日志 / 链路追踪 │
└──────────────────────────┬──────────────────────────────┘
│
┌──────────────────────────▼──────────────────────────────┐
│ RAG服务层(核心) │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────┐ │
│ │ 查询改写 │→│ 混合检索 │→│ 重排序 │→│ 生成优化 │ │
│ │ (LLM) │ │(向量+BM25)│ │(BGE) │ │(Prompt) │ │
│ └──────────┘ └──────────┘ └──────────┘ └─────────┘ │
│ │ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 幻觉检测 │ │ 引用标注 │ │ 缓存层 │ │
│ │ (LLM自检) │ │ (后处理) │ │(Redis) │ │
│ └──────────┘ └──────────┘ └──────────┘ │
└──────────┬───────────────┬───────────────┬────────────┘
│ │ │
┌──────────▼─────┐ ┌──────▼───────┐ ┌────▼──────────┐
│ 向量数据库 │ │ 关键词索引 │ │ LLM服务 │
│ (Milvus/PG) │ │ (Elasticsearch)│ (vLLM/Ollama) │
└────────────────┘ └──────────────┘ └───────────────┘
│
┌──────────▼─────────────────────────────────────────────┐
│ 数据处理层(离线) │
│ 文档加载 → 清洗 → 切分 → 向量化 → 入库(定时/手动触发) │
└─────────────────────────────────────────────────────────┘
27.5.2 FastAPI 服务封装
python
from fastapi import FastAPI, HTTPException
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from typing import Optional
import uuid
import time
app = FastAPI(title="RAG知识库API", version="1.0")
# ===== 数据模型 =====
class Question(BaseModel):
question: str
top_k: Optional[int] = 5
use_rerank: Optional[bool] = True
stream: Optional[bool] = False
class Answer(BaseModel):
answer: str
sources: list
latency_ms: int
request_id: str
# ===== 全局RAG引擎 =====
class RAGEngine:
"""RAG引擎单例"""
def __init__(self):
self.query_engine = None
self.retriever = None
self.reranker = None
self.llm = None
self._initialized = False
def initialize(self):
"""初始化(加载模型、连接数据库)"""
print("初始化RAG引擎...")
# 加载LLM
self.llm = ChatOpenAI(
model="qwen2.5:7b",
base_url="http://localhost:11434/v1",
api_key="not-needed",
temperature=0.1,
)
# 加载Embedding
self.embeddings = HuggingFaceEmbeddings(
model_name="BAAI/bge-large-zh-v1.5",
model_kwargs={"device": "cpu"},
)
# 加载向量数据库
self.vectorstore = Chroma(
persist_directory="./chroma_db",
embedding_function=self.embeddings,
collection_name="knowledge_base",
)
# 检索器
self.retriever = HybridRetriever(
self.vectorstore, [], top_k=10
)
# 重排序器
self.reranker = Reranker("BAAI/bge-reranker-v2-m3")
# 构建Chain
self.rag_chain = build_rag_chain(
self.retriever, self.reranker, self.llm
)
self._initialized = True
print("RAG引擎初始化完成")
def query(self, question: str, top_k: int = 5) -> dict:
"""查询"""
if not self._initialized:
raise RuntimeError("RAG引擎未初始化")
start_time = time.time()
request_id = str(uuid.uuid4())[:8]
# 检索
docs = self.retriever.retrieve(question)
if self.reranker:
docs = self.reranker.rerank(question, docs, top_n=top_k)
# 生成
context = format_context(docs)
answer = self.rag_chain.invoke(question)
latency = int((time.time() - start_time) * 1000)
return {
"answer": answer,
"sources": [
{
"source": d.metadata.get("source", "未知"),
"score": d.metadata.get("rerank_score", 0),
"preview": d.page_content[:100],
}
for d in docs
],
"latency_ms": latency,
"request_id": request_id,
}
def query_stream(self, question: str, top_k: int = 5):
"""流式查询"""
docs = self.retriever.retrieve(question)
if self.reranker:
docs = self.reranker.rerank(question, docs, top_n=top_k)
for chunk in self.rag_chain.stream(question):
yield chunk
# 全局实例
rag_engine = RAGEngine()
@app.on_event("startup")
async def startup():
"""启动时初始化"""
rag_engine.initialize()
@app.post("/api/v1/ask", response_model=Answer)
async def ask(q: Question):
"""问答接口"""
try:
result = rag_engine.query(q.question, q.top_k)
return Answer(**result)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/v1/ask/stream")
async def ask_stream(q: Question):
"""流式问答接口"""
async def generate():
for chunk in rag_engine.query_stream(q.question, q.top_k):
yield f"data: {chunk}\n\n"
return StreamingResponse(
generate(),
media_type="text/event-stream"
)
@app.get("/api/v1/health")
async def health():
"""健康检查"""
return {
"status": "healthy" if rag_engine._initialized else "initializing",
"model": "qwen2.5:7b",
"embedding": "bge-large-zh-v1.5",
}
# 启动命令:uvicorn api:app --host 0.0.0.0 --port 8000
27.5.3 缓存层设计
python
import hashlib
import json
import redis
class ResponseCache:
"""响应缓存 - 相同问题直接返回缓存结果"""
def __init__(self, redis_url: str = "redis://localhost:6379"):
self.redis = redis.from_url(redis_url)
self.ttl = 3600 # 缓存1小时
def _make_key(self, question: str, top_k: int) -> str:
"""生成缓存Key"""
content = f"{question}:{top_k}"
return f"rag:answer:{hashlib.md5(content.encode()).hexdigest()}"
def get(self, question: str, top_k: int = 5) -> Optional[dict]:
"""获取缓存"""
key = self._make_key(question, top_k)
cached = self.redis.get(key)
if cached:
result = json.loads(cached)
result["from_cache"] = True
return result
return None
def set(self, question: str, top_k: int, result: dict):
"""设置缓存"""
key = self._make_key(question, top_k)
# 不缓存流式响应
self.redis.setex(key, self.ttl, json.dumps(result, ensure_ascii=False))
# 在RAGEngine中加入缓存
class CachedRAGEngine(RAGEngine):
def __init__(self):
super().__init__()
self.cache = ResponseCache()
def query(self, question: str, top_k: int = 5) -> dict:
# 先查缓存
cached = self.cache.get(question, top_k)
if cached:
print(f"缓存命中: {question}")
return cached
# 缓存未命中,正常查询
result = super().query(question, top_k)
# 写入缓存
self.cache.set(question, top_k, result)
return result
27.6 文档管理后台
27.6.1 文档增删改查
python
class DocumentManager:
"""文档管理器 - 支持增量更新"""
def __init__(self, vectorstore, embeddings):
self.vectorstore = vectorstore
self.embeddings = embeddings
def add_document(self, file_path: str, metadata: dict = None):
"""添加单个文档"""
loader = DocumentLoader("")
ext = os.path.splitext(file_path)[1].lower()
if ext not in loader.LOADERS:
raise ValueError(f"不支持的格式: {ext}")
loader_cls = loader.LOADERS[ext]
docs = loader_cls(file_path).load()
if metadata:
for doc in docs:
doc.metadata.update(metadata)
# 切分
chunks = DocumentSplitter.split(docs, chunk_size=400, overlap=60)
# 入库
ids = self.vectorstore.add_documents(chunks)
print(f"添加成功:{file_path} → {len(ids)} 个片段")
return ids
def delete_document(self, file_name: str):
"""删除文档(按文件名)"""
# Chroma支持按元数据过滤删除
self.vectorstore.delete(
where={"source": file_name}
)
print(f"删除成功:{file_name}")
def update_document(self, file_path: str):
"""更新文档(先删后加)"""
file_name = os.path.basename(file_path)
self.delete_document(file_name)
self.add_document(file_path)
print(f"更新成功:{file_path}")
def list_documents(self) -> list:
"""列出所有文档"""
# 从Chroma获取所有文档的元数据
results = self.vectorstore.get()
# 按文件名去重
sources = set()
for metadata in results["metadatas"]:
sources.add(metadata.get("source", "未知"))
return sorted(sources)
def get_stats(self) -> dict:
"""获取统计信息"""
results = self.vectorstore.get()
return {
"total_chunks": len(results["ids"]),
"total_documents": len(set(
m.get("source", "未知") for m in results["metadatas"]
)),
"collection_name": "knowledge_base",
}
27.6.2 文档管理API
python
from fastapi import UploadFile, File
@app.post("/api/v1/documents/upload")
async def upload_document(file: UploadFile = File(...)):
"""上传文档"""
# 保存文件
file_path = f"./docs/{file.filename}"
with open(file_path, "wb") as f:
f.write(await file.read())
# 添加到向量数据库
rag_engine.doc_manager.add_document(file_path)
return {"status": "success", "filename": file.filename}
@app.delete("/api/v1/documents/{filename}")
async def delete_document(filename: str):
"""删除文档"""
rag_engine.doc_manager.delete_document(filename)
return {"status": "success", "deleted": filename}
@app.get("/api/v1/documents")
async def list_documents():
"""列出所有文档"""
docs = rag_engine.doc_manager.list_documents()
stats = rag_engine.doc_manager.get_stats()
return {"documents": docs, "stats": stats}
@app.post("/api/v1/documents/rebuild")
async def rebuild_index():
"""重建索引"""
# 清空现有索引
# 重新加载所有文档
# 重新切分和向量化
return {"status": "rebuilding", "message": "索引重建中..."}
27.7 Spring AI 集成(Java版)
对于后端团队来说,可能更习惯用Java。Spring AI提供了与LangChain类似的抽象:
27.7.1 Maven依赖
xml
<dependencies>
<!-- Spring AI核心 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
<!-- 向量数据库 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pgvector-store-spring-boot-starter</artifactId>
</dependency>
<!-- 文档加载 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-tika-document-reader</artifactId>
</dependency>
</dependencies>
27.7.2 配置
yaml
# application.yml
spring:
ai:
openai:
base-url: http://localhost:11434
api-key: not-needed
chat:
options:
model: qwen2.5:7b
temperature: 0.1
embedding:
options:
model: bge-large-zh-v1.5
vectorstore:
pgvector:
dimensions: 1024
distance-type: cosine_distance
schema-name: public
table-name: knowledge_base
27.7.3 RAG服务实现
java
@Service
public class RagService {
@Autowired
private ChatClient chatClient;
@Autowired
private VectorStore vectorStore;
private static final String RAG_PROMPT = """
你是一个严谨的知识库助手。请严格基于下方【参考资料】回答问题。
## 回答规则
1. 只基于参考资料回答,不要使用自身知识
2. 如果资料中没有相关信息,回答"根据现有资料,我无法回答该问题"
3. 不要编造或推断资料中不存在的信息
4. 在关键信息后标注来源[资料X]
5. 回答控制在200-500字
## 参考资料
{context}
## 用户问题
{question}
## 回答
""";
/**
* RAG问答
*/
public String ask(String question) {
// 1. 检索相关文档
List<Document> documents = vectorStore.similaritySearch(
SearchRequest.query(question)
.withTopK(5)
.withSimilarityThreshold(0.5)
);
// 2. 组装上下文
String context = formatContext(documents);
// 3. 生成回答
return chatClient.prompt()
.user(u -> u.text(RAG_PROMPT)
.param("context", context)
.param("question", question))
.call()
.content();
}
/**
* 流式问答
*/
public Flux<String> askStream(String question) {
List<Document> documents = vectorStore.similaritySearch(
SearchRequest.query(question).withTopK(5)
);
String context = formatContext(documents);
return chatClient.prompt()
.user(u -> u.text(RAG_PROMPT)
.param("context", context)
.param("question", question))
.stream()
.content();
}
/**
* 添加文档到知识库
*/
public void addDocument(Resource resource, String source) {
TikaDocumentReader reader = new TikaDocumentReader(resource);
List<Document> documents = reader.get();
// 切分
TokenTextSplitter splitter = new TokenTextSplitter();
List<Document> chunks = splitter.apply(documents);
// 添加元数据
chunks.forEach(doc -> {
doc.getMetadata().put("source", source);
});
// 入库
vectorStore.add(chunks);
}
private String formatContext(List<Document> documents) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < documents.size(); i++) {
Document doc = documents.get(i);
String source = (String) doc.getMetadata().getOrDefault("source", "未知");
sb.append(String.format("【资料%d】来源:%s\n%s\n\n---\n\n",
i + 1, source, doc.getText()));
}
return sb.toString();
}
}
27.7.4 Controller
java
@RestController
@RequestMapping("/api/v1")
public class RagController {
@Autowired
private RagService ragService;
@PostMapping("/ask")
public ResponseEntity<Map<String, Object>> ask(@RequestBody Map<String, String> body) {
String question = body.get("question");
String answer = ragService.ask(question);
return ResponseEntity.ok(Map.of(
"answer", answer,
"status", "success"
));
}
@PostMapping(value = "/ask/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> askStream(@RequestBody Map<String, String> body) {
String question = body.get("question");
return ragService.askStream(question);
}
@PostMapping("/documents/upload")
public ResponseEntity<?> upload(@RequestParam("file") MultipartFile file) {
try {
Resource resource = file.getResource();
ragService.addDocument(resource, file.getOriginalFilename());
return ResponseEntity.ok(Map.of("status", "success"));
} catch (Exception e) {
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
}
}
}
27.8 RAG系统评估与调优
27.8.1 构建评估Pipeline
python
class RAGEvaluator:
"""RAG系统评估器"""
def __init__(self, rag_chain, retriever, reranker):
self.rag_chain = rag_chain
self.retriever = retriever
self.reranker = reranker
def evaluate(self, test_cases: list) -> dict:
"""
test_cases: [
{
"question": "Qwen2.5支持多少种语言?",
"ground_truth": "Qwen2.5支持35种语言",
"expected_source": "qwen_tech.md"
},
...
]
"""
results = []
for case in test_cases:
question = case["question"]
ground_truth = case["ground_truth"]
# 检索评估
docs = self.retriever.retrieve(question)
if self.reranker:
docs = self.reranker.rerank(question, docs, top_n=5)
retrieval_hit = any(
case.get("expected_source", "") in d.metadata.get("source", "")
for d in docs
)
# 生成评估
answer = self.rag_chain.invoke(question)
# 忠实度评估
faithfulness = self._eval_faithfulness(answer, docs)
# 回答正确性
correctness = self._eval_correctness(answer, ground_truth)
results.append({
"question": question,
"answer": answer,
"ground_truth": ground_truth,
"retrieval_hit": retrieval_hit,
"faithfulness": faithfulness,
"correctness": correctness,
})
# 汇总
n = len(results)
return {
"results": results,
"summary": {
"retrieval_hit_rate": sum(r["retrieval_hit"] for r in results) / n,
"avg_faithfulness": sum(r["faithfulness"] for r in results) / n,
"avg_correctness": sum(r["correctness"] for r in results) / n,
"total_cases": n,
}
}
def _eval_faithfulness(self, answer: str, docs: list) -> float:
"""评估忠实度"""
context = format_context(docs)
# 简化版:检查回答中的数字是否在上下文中
import re
numbers = re.findall(r'\d+\.?\d*', answer)
if not numbers:
return 1.0
covered = sum(1 for n in numbers if n in context)
return covered / len(numbers)
def _eval_correctness(self, answer: str, ground_truth: str) -> float:
"""评估正确性(与标准答案的相似度)"""
from difflib import SequenceMatcher
return SequenceMatcher(None, answer, ground_truth).ratio()
# 使用
evaluator = RAGEvaluator(rag_chain, retriever, reranker)
test_cases = [
{
"question": "Qwen2.5支持多少种语言?",
"ground_truth": "Qwen2.5支持35种语言",
"expected_source": "qwen_tech.md"
},
{
"question": "LoRA和全量微调有什么区别?",
"ground_truth": "LoRA只训练少量参数,全量微调更新全部参数",
"expected_source": "finetune.md"
},
]
report = evaluator.evaluate(test_cases)
print(f"检索命中率: {report['summary']['retrieval_hit_rate']:.2%}")
print(f"平均忠实度: {report['summary']['avg_faithfulness']:.2%}")
print(f"平均正确性: {report['summary']['avg_correctness']:.2%}")
27.8.2 调优决策表
| 评估指标低 | 可能原因 | 调优方向 |
|---|---|---|
| 检索命中率低 | 切分太粗/Embedding不好/没有混合检索 | 调chunk_size、换Embedding模型、加BM25 |
| 忠实度低 | 温度太高/Prompt约束不够/上下文太多 | 降温度、加强Prompt、减少top_k |
| 正确性低 | 检索到但不相关/Prompt不好 | 加重排序、优化Prompt |
| 引用准确率低 | 模型标注能力弱 | 后处理修正引用 |
| 延迟高 | 检索慢/重排序慢/生成慢 | 加缓存、用更小模型、异步处理 |
27.9 从Demo到生产:关键差异
| 维度 | Demo | 生产 |
|---|---|---|
| 文档量 | 10-100个 | 1万-100万 |
| 并发 | 单用户 | 100-1000 QPS |
| 可用性 | 跑通就行 | 99.9% SLA |
| 监控 | 没有 | 全链路追踪 |
| 文档更新 | 手动 | 自动Pipeline |
| 缓存 | 没有 | Redis多级缓存 |
| 降级 | 没有 | 模型降级+兜底回复 |
| 安全 | 没有 | Prompt注入防护+鉴权 |
| 评估 | 肉眼看 | 自动化评估Pipeline |
| 成本 | 不关心 | Token监控+模型分级路由 |
生产级优化清单
python
# ===== 1. 异步处理 =====
@app.post("/api/v1/ask/async")
async def ask_async(q: Question, background_tasks: BackgroundTasks):
"""异步问答 - 适合长查询"""
task_id = str(uuid.uuid4())
# 放入任务队列,异步处理
background_tasks.add_task(process_async, task_id, q.question)
return {"task_id": task_id, "status": "processing"}
# ===== 2. 模型降级 =====
class ModelRouter:
"""模型分级路由"""
def __init__(self):
self.models = {
"simple": "qwen2.5:1.5b", # 简单问题用小模型
"standard": "qwen2.5:7b", # 标准问题用中模型
"complex": "qwen2.5:14b", # 复杂问题用大模型
}
def route(self, question: str) -> str:
"""根据问题复杂度选择模型"""
if len(question) < 20:
return self.models["simple"]
elif len(question) < 100:
return self.models["standard"]
else:
return self.models["complex"]
# ===== 3. 限流 =====
from slowapi import Limiter
from slowapi.util import get_remote_address
limiter = Limiter(key_func=get_remote_address)
@app.post("/api/v1/ask")
@limiter.limit("10/minute") # 每分钟10次
async def ask(request: Request, q: Question):
result = rag_engine.query(q.question)
return result
# ===== 4. 链路追踪 =====
from loguru import logger
@app.middleware("http")
async def log_requests(request: Request, call_next):
"""请求日志中间件"""
request_id = str(uuid.uuid4())[:8]
start = time.time()
logger.info(f"[{request_id}] {request.method} {request.url.path}")
response = await call_next(request)
duration = time.time() - start
logger.info(f"[{request_id}] completed in {duration:.3f}s")
response.headers["X-Request-ID"] = request_id
return response
27.10 常见问题与最佳实践
27.10.1 常见问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 内存溢出 | 文档太多一次性加载 | 分批处理、流式加载 |
| 检索慢 | 向量数据库索引没建 | 建HNSW索引、增加ef_construction |
| 回答重复 | 上下文有重复片段 | 加强去重、提高dedup阈值 |
| 中文切分差 | 按英文逻辑切分 | 用中文分隔符、加句号断句 |
| 模型不引用 | Prompt约束不够 | 强化Prompt、加引用后处理 |
| 多轮对话跑偏 | 没做查询改写 | 加查询改写模块 |
27.10.2 最佳实践清单
数据处理:
- 多格式文档统一加载
- 数据清洗Pipeline(去噪/去重/规范化)
- 递归切分 chunk_size=400, overlap=60 起步
- 元数据自动标注(source/date/category)
- 增量更新支持
检索:
- 混合检索(向量+BM25)
- 重排序(BGE Reranker)
- 检索结果去重
- 元数据过滤
生成:
- 低温度(0.1-0.3)
- Prompt明确约束(只基于资料)
- 引用标注
- 流式输出
- 幻觉检测
工程化:
- FastAPI服务封装
- Redis缓存
- 模型分级路由
- 限流+鉴权
- 链路追踪
- 文档管理后台
- 评估Pipeline
- 监控告警
27.11 本章小结
核心知识点回顾
RAG系统完整架构:
离线Pipeline:文档加载 → 清洗 → 切分 → 向量化 → 入库
在线Pipeline:查询改写 → 混合检索 → 重排序 → 上下文组装 → Prompt → 生成 → 引用标注 → 幻觉检测
框架选择:
- LlamaIndex:纯RAG项目首选,上手快,文档处理好
- LangChain:Agent+RAG混合项目,生态大,扩展强
- Spring AI:Java后端团队首选,企业级集成
生产化关键:
- 缓存(Redis)、降级(模型路由)、监控(链路追踪)
- 评估Pipeline(检索命中率+忠实度+正确性)
- 文档管理后台(增删改查+增量更新)
关键结论
- 框架是工具不是目的 --- 先理解RAG原理(第21-26章),再用框架提效。不懂原理用框架只是"黑盒魔法"
- LlamaIndex上手最快 --- 30行代码跑通RAG,适合快速验证
- LangChain扩展性最强 --- LCEL链式组合灵活,适合复杂应用
- Spring AI是Java团队的最佳选择 --- 与Spring生态无缝集成
- 从Demo到生产差距很大 --- 缓存、降级、监控、评估、文档管理,这些才是生产级系统的核心
- 评估是持续过程 --- 没有评估就没有优化,建立测试集 + 自动化评估Pipeline
第5阶段「RAG检索增强生成」完整回顾
| 章节 | 核心知识 |
|---|---|
| 第21章 RAG原理 | 检索→增强→生成、RAG vs 微调、三代演进 |
| 第22章 数据处理 | 文档加载、清洗、五种切分策略、元数据管理 |
| 第23章 向量化 | Embedding原理、BGE/M3E选型、维度选择 |
| 第24章 向量数据库 | HNSW/IVF/PQ索引、Milvus/PgVector/Chroma选型 |
| 第25章 检索策略 | 查询改写、混合检索(RRF)、重排序(BGE Reranker) |
| 第26章 生成优化 | 上下文组装、Prompt设计、引用标注、幻觉检测 |
| 第27章 框架实操 | LlamaIndex/LangChain/Spring AI完整实战 |
总进度
| 阶段 | 章节 | 状态 |
|---|---|---|
| 第1阶段 认知启蒙 | 第1-4章 | ✅ 全部完成 |
| 第2阶段 机器学习基础 | 第5-9章 | ✅ 全部完成 |
| 第3阶段 深度学习 | 第10-14章 | ✅ 全部完成 |
| 第4阶段 大模型(LLM) | 第15-20章 | ✅ 全部完成 |
| 第5阶段 RAG检索增强生成 | 第21-27章 | ✅ 全部完成 |
| 第6阶段 Agent智能体 | 第28章起 | 待写 |
大白话总结: 至此第5阶段「RAG检索增强生成」7章全部完成。RAG是目前AI应用开发中落地最多、ROI最高的场景------不需要训练模型,只需要"检索+生成"就能让大模型用上你的私有数据。掌握这7章的内容,你就能搭建一个生产级RAG系统了。
接下来进入第6阶段「Agent智能体」,第28章「Agent概念」会讲清楚AI能不能自己决定"先做什么后做什么"。需要继续写就告诉我。