Ollama: RAG管道

RAG 让 AI 有了记忆库——不是猜答案,而是从知识中检索真相。

💡 提示:RAG 的核心原理是"先检索,再生成"——模型只在检索到的文档片段基础上回答,而非凭记忆。这大幅减少"幻觉"(编造信息),但检索质量直接决定回答质量,分块策略和 Embedding 模型选择是关键。

📋 前置知识:需要先掌握以下内容

1. 你将学到


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) 端到端管道流程

100%
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 PDF 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(召回率)。

📖 小节


📝 作业

  1. 基础题(难度⭐):用 OllamaEmbeddings 将 5 条产品 FAQ 文本存入 Chroma,并执行一次相似性检索。
  2. 进阶题(难度⭐⭐):构建完整 RAG 管道——从文本文件加载、分块、嵌入、存储到检索生成,测试 3 个问题。
  3. 挑战题(难度⭐⭐⭐):为 SupportBot V3 实现知识库——准备 10+ 条产品文档,构建 RAG 系统,对比有无 RAG 时同一问题的回答质量差异。
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏