Ollama: RAG管道
RAG 让 AI 有了记忆库——不是猜答案,而是从知识中检索真相。
💡 提示:RAG 的核心原理是"先检索,再生成"——模型只在检索到的文档片段基础上回答,而非凭记忆。这大幅减少"幻觉"(编造信息),但检索质量直接决定回答质量,分块策略和 Embedding 模型选择是关键。
📋 前置知识:需要先掌握以下内容
- 第13课:LangChain集成
1. 你将学到
- RAG 架构:文档加载 → 分块 → Embedding → 向量存储 → 检索 → 生成
- Ollama Embeddings 模型使用
- 向量数据库选型:Chroma / FAISS / Qdrant
- 文档分块策略与参数调优
- Alice 的 SupportBot V3:产品知识库 RAG
2. 一个 SaaS 创业者的真实故事
⚠️ 警告: RAG 不能消除幻觉,只能大幅降低其概率。如果检索到的文档片段不相关或不足,模型仍可能"编造"答案。关键策略:限定模型只在检索结果范围内回答,并在 System Prompt 中加入"如果文档中没有相关信息,请回答'我无法确认'"。
(1) 痛点:模型不知道产品细节
Alice 的 SupportBot 能做通用客服,但不知道具体产品的退货政策、保修条款、价格细节。模型经常"编造"信息,客户投诉不断。
(2) 解法:RAG 让模型基于真实文档回答
将产品手册、FAQ、政策文档导入向量数据库,检索相关片段后让模型基于事实生成回答:
PYTHON
# RAG: Retrieve relevant docs, then generate answer
docs = retriever.invoke("What is the warranty for headphones?")
answer = llm.invoke(f"Based on: {docs}\nAnswer: What is the warranty?")
3. RAG 架构详解
💡 提示: RAG 的核心原理是"先检索,再生成"——模型只在检索到的文档片段基础上回答,而非凭记忆。这大幅减少"幻觉"(编造信息),但检索质量直接决定回答质量,分块策略和 Embedding 模型选择是关键。
(1) 端到端管道流程
flowchart LR
A[Documents] --> B[Loader]
B --> C[Splitter]
C --> D[Embedding Model<br/>nomic-embed-text]
D --> E[Vector Store<br/>Chroma/FAISS]
F[User Query] --> G[Query Embedding]
G --> E
E --> H[Top-K Chunks]
H --> I[LLM Generation<br/>qwen2.5]
I --> J[Answer]
(2) 各环节详解
| 阶段 | 工具 | 输入 | 输出 |
|---|---|---|---|
| 加载 | PyPDFLoader / TextLoader | PDF/MD/TXT | Document 列表 |
| 分块 | RecursiveCharacterTextSplitter | Document | Chunk 列表 |
| 嵌入 | OllamaEmbeddings | Chunk 文本 | 向量 (768d) |
| 存储 | Chroma / FAISS | 向量 + 文本 | 索引 |
| 检索 | Similarity Search | Query 向量 | Top-K Chunks |
| 生成 | ChatOllama | Query + Chunks | Answer |
4. Ollama Embeddings
⚠️ 注意:Embedding 模型选择直接影响检索质量——nomic-embed-text(768维)英文表现优秀但中文一般,mxbai-embed-large(1024维)精度更高但体积翻倍。中文场景建议测试多模型后选最优,不要想当然地认为"更大就更好"。
(1) 嵌入模型对比
| 模型 | 维度 | 大小 | 速度 | 适用 |
|---|---|---|---|---|
| nomic-embed-text | 768 | 274 MB | 快 | 通用英文 |
| mxbai-embed-large | 1024 | 670 MB | 中 | 高精度 |
| all-minilm | 384 | 46 MB | 极快 | 轻量级 |
(2) 嵌入模型选型决策
| 场景 | 推荐模型 | 理由 |
|---|---|---|
| 通用 RAG | nomic-embed-text | 性价比最优 |
| 高精度检索 | mxbai-embed-large | 维度更高,区分度好 |
| 资源受限 | all-minilm | 最小最快 |
| 中文为主 | nomic-embed-text | 中文表现尚可 |
▶ 示例 1: Ollama Embeddings 基础
PYTHON
from langchain_ollama import OllamaEmbeddings
# Initialize embeddings model
embeddings = OllamaEmbeddings(model="nomic-embed-text")
# Embed a single text
vector = embeddings.embed_query("What is the return policy?")
print(f"Dimension: {len(vector)}") # 768
# Embed multiple texts
vectors = embeddings.embed_documents([
"Free shipping on orders over $50",
"30-day return policy for all products",
"Warranty covers manufacturing defects for 1 year"
])
print(f"Embedded {len(vectors)} documents")
输出:
TEXT
📖 仅展示
# 执行成功
5. 文档加载与分块
(1) 文档加载器
| Loader | 格式 | 安装 |
|---|---|---|
| PyPDFLoader | pip install pypdf | |
| TextLoader | TXT | 内置 |
| UnstructuredMarkdownLoader | Markdown | pip install unstructured |
| CSVLoader | CSV | 内置 |
| UnstructuredHTMLLoader | HTML/URL | pip install unstructured |
(2) 分块策略对比
| 策略 | 参数 | 适用文档 | 优缺点 |
|---|---|---|---|
| 固定大小 | chunk_size=500, overlap=50 | 通用 | 简单但可能切断语义 |
| 递归字符 | chunk_size=500, overlap=50, separators | 结构化文档 | 保留段落边界 |
| 语义分块 | similarity_threshold | 长文 | 最佳但计算量大 |
💡 提示: RecursiveCharacterTextSplitter 是首选。chunk_size=500-1000、overlap=50-100 是经验最优值。
▶ 示例 2: 文档加载与分块
PYTHON
from langchain_community.document_loaders import TextLoader, PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
# Load text document
loader = TextLoader("product_faq.txt")
docs = loader.load()
print(f"Loaded {len(docs)} document(s)")
# Split into chunks
splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=50,
separators=["\n\n", "\n", ". ", " ", ""]
)
chunks = splitter.split_documents(docs)
print(f"Split into {len(chunks)} chunks")
print(f"First chunk preview: {chunks[0].page_content[:100]}...")
输出:
TEXT
📖 仅展示
# 执行成功
6. 向量数据库选型
(1) 三大向量库对比
| 维度 | Chroma | FAISS | Qdrant |
|---|---|---|---|
| 类型 | 嵌入式 | 内存库 | 客户端/服务器 |
| 持久化 | ✅ 本地文件 | ❌ 内存 | ✅ 磁盘/云 |
| 安装 | pip install | pip install + faiss-cpu | Docker / pip |
| 适合 | 开发/小规模 | 高速检索 | 生产环境 |
| 过滤 | ✅ 元数据过滤 | ❌ | ✅ 高级过滤 |
| 分布式 | ❌ | ❌ | ✅ |
▶ 示例 3: Chroma 向量存储与检索
PYTHON
from langchain_ollama import OllamaEmbeddings
from langchain_community.vectorstores import Chroma
from langchain_text_splitters import RecursiveCharacterTextSplitter
# Prepare documents
documents = [
"Return policy: 30 days from purchase, items must be in original condition.",
"Shipping: Free for orders over $50. Standard delivery takes 3-5 business days.",
"Warranty: All electronics have a 1-year manufacturer warranty.",
"International shipping: Available to 50+ countries. Import duties may apply.",
"Refund process: Full refund within 5-7 business days after we receive the return."
]
# Split documents
splitter = RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=20)
chunks = splitter.create_documents(documents)
# Create vector store
embeddings = OllamaEmbeddings(model="nomic-embed-text")
vectorstore = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory="./chroma_db"
)
# Similarity search
results = vectorstore.similarity_search("How do I get a refund?", k=2)
for doc in results:
print(f"- {doc.page_content}")
输出:
TEXT
📖 仅展示
# 执行成功
▶ 示例 4: FAISS 向量存储
PYTHON
from langchain_ollama import OllamaEmbeddings
from langchain_community.vectorstores import FAISS
embeddings = OllamaEmbeddings(model="nomic-embed-text")
# Create FAISS index
texts = [
"Product A: Wireless headphones, $89.99, Bluetooth 5.3",
"Product B: Laptop stand, $34.99, Adjustable height",
"Product C: USB-C cable, $12.99, 2-meter length"
]
vectorstore = FAISS.from_texts(texts, embeddings)
# Save and load
vectorstore.save_local("./faiss_index")
loaded_vs = FAISS.load_local("./faiss_index", embeddings,
allow_dangerous_deserialization=True)
# Search
results = loaded_vs.similarity_search("headphones", k=2)
for doc in results:
print(doc.page_content)
输出:
TEXT
📖 仅展示
# 执行成功
7. 完整 RAG 管道与 SupportBot V3
▶ 示例 5: 完整 RAG 管道
PYTHON
from langchain_ollama import ChatOllama, OllamaEmbeddings
from langchain_community.vectorstores import Chroma
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
# Build retriever
embeddings = OllamaEmbeddings(model="nomic-embed-text")
vectorstore = Chroma(persist_directory="./chroma_db", embedding_function=embeddings)
retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
# Build RAG chain
llm = ChatOllama(model="qwen2.5", temperature=0.3)
prompt = ChatPromptTemplate.from_messages([
("system", "You are SupportBot. Answer based ONLY on the following context. "
"If the answer is not in the context, say 'I don't have that information. "
"Let me connect you with a human agent.'\n\nContext:\n{context}"),
("human", "{question}")
])
def format_docs(docs):
return "\n\n".join(doc.page_content for doc in docs)
rag_chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
# Ask questions
answer = rag_chain.invoke("What is the return policy?")
print(answer)
输出:
TEXT
📖 仅展示
# 函数定义成功
8. 综合示例:SupportBot V3 RAG 客服系统
PYTHON
# ============================================
# Comprehensive: SupportBot V3 with RAG
# Product knowledge base powered customer service
# ============================================
from langchain_ollama import ChatOllama, OllamaEmbeddings
from langchain_community.document_loaders import TextLoader
from langchain_community.vectorstores import Chroma
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
from pathlib import Path
class SupportBotV3:
def __init__(self, kb_path: str = "./knowledge_base",
model: str = "qwen2.5",
embed_model: str = "nomic-embed-text"):
self.llm = ChatOllama(model=model, temperature=0.3)
self.embeddings = OllamaEmbeddings(model=embed_model)
self.kb_path = kb_path
self.vectorstore = None
self.rag_chain = None
def build_knowledge_base(self, doc_dir: str):
"""Load documents and build vector store."""
docs = []
for f in Path(doc_dir).glob("*.txt"):
loader = TextLoader(str(f))
docs.extend(loader.load())
for f in Path(doc_dir).glob("*.md"):
loader = TextLoader(str(f))
docs.extend(loader.load())
splitter = RecursiveCharacterTextSplitter(
chunk_size=500, chunk_overlap=50
)
chunks = splitter.split_documents(docs)
self.vectorstore = Chroma.from_documents(
documents=chunks,
embedding=self.embeddings,
persist_directory=self.kb_path
)
print(f"Indexed {len(chunks)} chunks from {len(docs)} documents")
def setup_rag_chain(self):
"""Build the RAG chain."""
if not self.vectorstore:
self.vectorstore = Chroma(
persist_directory=self.kb_path,
embedding_function=self.embeddings
)
retriever = self.vectorstore.as_retriever(search_kwargs={"k": 3})
prompt = ChatPromptTemplate.from_messages([
("system", (
"You are SupportBot for GlobalShop e-commerce. "
"Answer based ONLY on the provided context. "
"If not in context, say: 'Let me connect you with a human agent.' "
"Be concise (2-3 sentences).\n\nContext:\n{context}"
)),
("human", "{question}")
])
def format_docs(docs):
return "\n\n".join(d.page_content for d in docs)
self.rag_chain = (
{"context": retriever | format_docs,
"question": RunnablePassthrough()}
| prompt | self.llm | StrOutputParser()
)
def ask(self, question: str) -> str:
"""Ask a question using RAG."""
if not self.rag_chain:
self.setup_rag_chain()
return self.rag_chain.invoke(question)
# Usage
if __name__ == "__main__":
bot = SupportBotV3()
# First time: build knowledge base
# bot.build_knowledge_base("./docs")
# Ask questions
questions = [
"What is the return policy for electronics?",
"How long does international shipping take?",
"Do you offer free returns?",
]
for q in questions:
answer = bot.ask(q)
print(f"Q: {q}")
print(f"A: {answer}\n")
❓ 常见问题
Q RAG 和微调(Fine-tuning)该选哪个?
A RAG 适合知识频繁更新的场景(产品FAQ、政策文档),无需重新训练。微调适合改变模型行为模式。90% 企业场景用 RAG 更经济。
Q chunk_size 怎么选?
A 500-1000 字符是通用起点。短文档(FAQ)用 200-500;长文档(手册)用 500-1000。overlap 设为 chunk_size 的 10-20%。
Q 检索结果不准确怎么办?
A 尝试:1) 换更大维度的嵌入模型(mxbai-embed-large);2) 增大 k 值(3→5);3) 用 MMR 搜索增加多样性;4) 改善文档分块质量。
Q Chroma 和 FAISS 怎么选?
A 开发阶段用 Chroma(自动持久化、支持过滤)。需要极致速度且数据全在内存用 FAISS。生产环境用 Qdrant(分布式、高级过滤)。
Q RAG 的 token 消耗怎么估算?
A 每次查询消耗 = query tokens + top-k chunks tokens + generated answer tokens。k=3、chunk_size=500 时,输入约 1500-2000 tokens。本地推理无 token 费用。
Q 如何评估 RAG 质量?
A 用 RAGAS 框架评估 4 个指标:Faithfulness(答案是否忠于上下文)、Answer Relevancy(答案是否切题)、Context Precision(检索精确度)、Context Recall(召回率)。
📖 小节
- RAG 六步管道:加载 → 分块 → 嵌入 → 存储 → 检索 → 生成
- nomic-embed-text 是 Ollama 默认嵌入模型,768 维,性价比最优
- RecursiveCharacterTextSplitter 是首选分块工具,chunk_size=500 是起点
- Chroma 适合开发,FAISS 适合高速检索,Qdrant 适合生产
- RAG chain 用 LCEL 组合:retriever | format | prompt | llm | parser
- SupportBot V3 基于 RAG 让回答有据可查,消除"编造"问题
📝 作业
- 基础题(难度⭐):用 OllamaEmbeddings 将 5 条产品 FAQ 文本存入 Chroma,并执行一次相似性检索。
- 进阶题(难度⭐⭐):构建完整 RAG 管道——从文本文件加载、分块、嵌入、存储到检索生成,测试 3 个问题。
- 挑战题(难度⭐⭐⭐):为 SupportBot V3 实现知识库——准备 10+ 条产品文档,构建 RAG 系统,对比有无 RAG 时同一问题的回答质量差异。