如果你正在构建一个基于大语言模型(LLM)的智能应用,比如一个能回答公司内部文档问题的客服机器人,或者一个能总结长篇技术报告的工具,你很可能已经遇到了一个核心难题: 大模型无法记住它没“见过”的信息,更无法保证回答的准确性。

直接向模型提问,它可能会基于训练数据“一本正经地胡说八道”(幻觉问题)。而简单地将文档内容全部塞进提示词(Prompt),又会迅速耗尽模型的上下文窗口,导致成本飙升且效果不佳。

RAG(检索增强生成) 正是解决这一痛点的关键技术范式。它让大模型能够“外接”一个专属知识库,在回答前先进行精准检索,从而生成 有据可依、准确可靠 的答案。

然而,从“知道RAG概念”到“搭建一个稳定、高效、可用的RAG系统”,中间隔着巨大的工程鸿沟。网上教程往往只讲单一环节,比如如何调用一个Embedding API,或者如何连接向量数据库。但一个生产级的RAG系统是一个复杂的工程拼图,涉及数据预处理、向量化、检索、重排、生成以及流程编排等多个环节,任何一个环节的短板都会导致最终效果大打折扣。

本文将从零开始,为你拆解一个 完整、可落地的RAG系统全流程 。我们将不仅介绍每个核心组件(Embedding模型、向量数据库、检索优化策略),更会使用 LangGraph 这一新兴的AI应用编排框架,将整个流程串联成一个具备状态管理和复杂决策能力的智能体(Agent)。最后,我们还会探讨如何通过 LoRA微调 来低成本地定制专属的Embedding模型,进一步提升检索精度。

读完本文,你将能清晰地回答:

  1. RAG系统的核心流程与关键组件是什么?
  2. 如何根据场景选择合适的Embedding模型和向量数据库?
  3. 超越简单相似度检索,有哪些高级优化策略?
  4. 如何用LangGraph构建一个具备自我修正和路由能力的智能RAG流程?
  5. 何时以及如何对Embedding模型进行LoRA微调?

1. RAG系统全景图:不止是“检索+生成”

在深入代码之前,我们必须建立一个正确的认知:RAG不是一个简单的“向量搜索+提示词拼接”的公式。一个健壮的RAG系统是一个精心设计的管道(Pipeline)。

1.1 核心流程拆解

一个典型的RAG流程包含以下关键阶段:

  1. 文档加载与预处理 :从PDF、Word、网页、数据库等来源获取原始文本。
  2. 文本分割(Chunking) :将长文档切割成适合模型处理的小片段。这是影响检索精度的 首要因素 。分割策略(按字符、按句子、按语义、重叠窗口)需要根据文档类型精心设计。
  3. 向量化(Embedding) :使用Embedding模型将文本块转换为高维空间中的向量(一组数字)。这个向量的质量直接决定了后续检索的准确性。
  4. 向量存储与索引 :将向量及其对应的原始文本(元数据)存入向量数据库,并建立高效的索引(如HNSW, IVF)以加速检索。
  5. 查询转换与检索 :将用户问题也转换为向量,在向量数据库中执行相似度搜索(如余弦相似度),找出最相关的K个文本块。
  6. 后处理与重排(Rerank) :初步检索的结果可能包含相关性不高但向量相似的噪音。使用一个更精细的 重排模型 对Top K结果进行二次排序,筛选出最相关的几个片段。
  7. 提示工程与生成 :将筛选后的文本片段作为“上下文”,与用户问题一起构造成最终的提示词,提交给大语言模型(LLM)生成最终答案。
  8. 评估与迭代 :通过人工评估或自动化指标(如检索命中率、答案忠实度)来衡量系统效果,并持续优化上述各个环节。

1.2 为什么需要LangGraph?

传统的RAG实现(如使用LangChain)通常是线性的、静态的管道。而 LangGraph 引入了“图”的概念,允许你定义具有状态(State)和循环(Cycle)的复杂工作流。

  • 状态管理 :可以记住多轮对话的历史、中间结果和决策路径。
  • 条件路由 :可以根据检索结果的质量(如相似度分数过低)决定下一步动作,例如触发一个“联网搜索”节点,或直接告知用户“知识库中未找到相关信息”。
  • 自我修正 :可以设计一个“验证”节点,检查LLM生成的答案是否与提供的上下文一致,如果不一致,则重新检索或生成。
  • 多智能体协作 :可以轻松编排多个“专家”智能体(如一个负责检索,一个负责总结,一个负责校验)协同工作。

将RAG构建在LangGraph之上,意味着你的系统从一个“问答机”进化成了一个具备基本推理和决策能力的“智能体”。

2. 环境准备与工具选型

在开始实战前,我们需要搭建开发环境并选择合适的技术栈。本文将以Python为核心,选择目前主流且易用的开源工具。

2.1 基础环境

  • Python 3.10+ :确保你的Python版本在3.10或以上。
  • 包管理工具 :使用 pip conda
  • IDE :VSCode、PyCharm等均可。

2.2 核心库安装

我们将使用以下库,请通过pip安装:

# 核心AI应用框架与向量数据库客户端
pip install langgraph langchain langchain-community

# 文本分割与加载
pip install "unstructured[all-docs]" tiktoken pypdf

# Embedding模型(以BGE为例)和重排模型
pip install sentence-transformers FlagEmbedding

# 向量数据库(以Chroma为例,轻量易用)
pip install chromadb

# LLM调用(以Ollama本地运行Llama3为例,也可替换为OpenAI等)
pip install ollama

# 可选:用于LoRA微调
pip install transformers datasets peft accelerate torch

2.3 关键组件选型建议

  • Embedding模型
    • 通用场景 BAAI/bge-large-zh-v1.5 (中文优)、 BAAI/bge-base-en-v1.5 (英文优)、 thenlper/gte-large 。本文选用 BAAI/bge-base-en-v1.5
    • 追求最新 :可关注 BAAI/bge-m3 ,它支持多语言、多粒度,但资源消耗更大。
  • 向量数据库
    • 轻量开发/原型 ChromaDB ,无需服务,API简单。
    • 生产级、高并发 Qdrant Milvus Weaviate 。它们支持分布式、持久化、更丰富的过滤条件。
  • LLM
    • 本地部署(隐私、成本) :通过 Ollama 运行 llama3 qwen2.5 mistral 等模型。
    • 云API(便捷、强大) :OpenAI GPT-4o、Anthropic Claude、DeepSeek等。
  • 编排框架 LangGraph ,用于构建有状态的智能工作流。

3. 实战第一步:构建基础RAG管道

让我们先构建一个最基础的、线性的RAG管道,理解每个环节。

3.1 文档加载与分割

假设我们有一个 sample.pdf 的技术文档。

# file_path: data_loader.py
from langchain_community.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter

def load_and_split_pdf(pdf_path):
    """加载PDF并分割文本"""
    loader = PyPDFLoader(pdf_path)
    documents = loader.load() # 加载出Document对象列表

    # 创建文本分割器
    # chunk_size: 每个文本块的最大字符数
    # chunk_overlap: 块之间的重叠字符数,防止语义被切断
    # separators: 分割符优先级列表
    text_splitter = RecursiveCharacterTextSplitter(
        chunk_size=500,
        chunk_overlap=50,
        separators=["\n\n", "\n", "。", "!", "?", ";", ",", " ", ""]
    )
    # 执行分割
    chunks = text_splitter.split_documents(documents)
    print(f"原始文档页数: {len(documents)}")
    print(f"分割后文本块数量: {len(chunks)}")
    print(f"示例块内容 (前200字符): {chunks[0].page_content[:200]}...")
    return chunks

if __name__ == "__main__":
    chunks = load_and_split_pdf("./docs/sample.pdf")

关键点 chunk_size 需要根据Embedding模型的最大输入长度和文档特点调整。重叠( overlap )对于保持上下文连贯性至关重要。

3.2 向量化与存储

接下来,我们将文本块转换为向量并存入ChromaDB。

# file_path: vector_store.py
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_community.vectorstores import Chroma
import os

def create_vector_store(chunks, persist_directory="./chroma_db"):
    """创建并持久化向量数据库"""
    # 1. 初始化Embedding模型
    # 这里使用开源的BGE模型,无需API密钥
    model_name = "BAAI/bge-base-en-v1.5"
    model_kwargs = {'device': 'cpu'} # 或 'cuda'
    encode_kwargs = {'normalize_embeddings': True} # 归一化,便于计算余弦相似度
    embeddings = HuggingFaceEmbeddings(
        model_name=model_name,
        model_kwargs=model_kwargs,
        encode_kwargs=encode_kwargs
    )

    # 2. 创建向量数据库,并持久化到本地目录
    vectorstore = Chroma.from_documents(
        documents=chunks,
        embedding=embeddings,
        persist_directory=persist_directory
    )
    # 显式持久化
    vectorstore.persist()
    print(f"向量数据库已创建并保存至: {persist_directory}")
    return vectorstore

def load_vector_store(persist_directory="./chroma_db"):
    """加载已存在的向量数据库"""
    embeddings = HuggingFaceEmbeddings(model_name="BAAI/bge-base-en-v1.5")
    vectorstore = Chroma(
        persist_directory=persist_directory,
        embedding_function=embeddings
    )
    print("向量数据库加载成功。")
    return vectorstore

if __name__ == "__main__":
    # 假设已有chunks
    # vectorstore = create_vector_store(chunks)
    vectorstore = load_vector_store()

关键点 normalize_embeddings=True 意味着向量会被归一化为单位长度,此时向量点积就等于余弦相似度,是推荐做法。

3.3 检索与生成

现在,我们可以进行检索并调用LLM生成答案了。

# file_path: basic_rag.py
from langchain.chains import RetrievalQA
from langchain_community.llms import Ollama
from vector_store import load_vector_store # 导入上一步的函数

def setup_basic_rag_chain():
    """设置基础的RAG问答链"""
    # 1. 加载向量数据库
    vectorstore = load_vector_store()

    # 2. 创建检索器,可以配置搜索参数
    retriever = vectorstore.as_retriever(
        search_type="similarity", # 相似度搜索
        search_kwargs={"k": 4} # 返回最相关的4个片段
    )

    # 3. 初始化LLM (使用本地Ollama)
    # 确保已通过 `ollama pull llama3` 下载了模型
    llm = Ollama(model="llama3", temperature=0.1) # temperature调低,答案更确定

    # 4. 创建RetrievalQA链
    qa_chain = RetrievalQA.from_chain_type(
        llm=llm,
        chain_type="stuff", # 最简单的方式,将所有上下文塞进prompt
        retriever=retriever,
        return_source_documents=True, # 返回源文档,便于调试
        verbose=True # 打印详细日志
    )
    return qa_chain

if __name__ == "__main__":
    qa_chain = setup_basic_rag_chain()
    query = "RAG系统的主要优势是什么?"
    result = qa_chain.invoke({"query": query})
    print("问题:", query)
    print("答案:", result["result"])
    print("\n--- 参考来源 ---")
    for i, doc in enumerate(result["source_documents"]):
        print(f"[{i+1}] {doc.page_content[:150]}...")

运行这个脚本,你将得到一个基于本地知识库的答案。这就是一个最基础的RAG系统。

4. 进阶优化:让检索更精准

基础RAG的检索可能不够精准。我们引入两个关键优化: 重排(Rerank) 元数据过滤

4.1 集成重排模型

重排模型是一个更精细的文本匹配模型,它会对初步检索出的Top K个结果进行二次打分和排序。

# file_path: advanced_retriever.py
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import CrossEncoderReranker
from langchain_community.cross_encoders import HuggingFaceCrossEncoder
from vector_store import load_vector_store

def create_advanced_retriever():
    """创建带重排功能的高级检索器"""
    vectorstore = load_vector_store()
    base_retriever = vectorstore.as_retriever(search_kwargs={"k": 10}) # 第一步多检索一些

    # 初始化交叉编码器重排模型(例如BGE的Reranker)
    # 注意:rerank模型通常与embedding模型不同,专用于对(query, doc)对进行精细打分
    model_name = "BAAI/bge-reranker-large"
    cross_encoder = HuggingFaceCrossEncoder(model_name=model_name)
    compressor = CrossEncoderReranker(model=cross_encoder, top_n=4) # 重排后保留Top 4

    # 组合成上下文压缩检索器
    compression_retriever = ContextualCompressionRetriever(
        base_compressor=compressor,
        base_retriever=base_retriever
    )
    return compression_retriever

if __name__ == "__main__":
    retriever = create_advanced_retriever()
    query = "如何优化RAG中的文本分割?"
    docs = retriever.invoke(query)
    print(f"检索到 {len(docs)} 个相关文档片段:")
    for i, doc in enumerate(docs):
        print(f"[{i+1}] Score (if any): {doc.metadata.get('score', 'N/A')} | Content: {doc.page_content[:100]}...")

关键点 :重排模型计算量大,通常只在初步检索( k=10~20 )后进行,筛选出最相关的少量( top_n=3~5 )片段送入LLM,在精度和延迟间取得平衡。

4.2 利用元数据过滤

在存入向量数据库时,我们可以为每个文本块添加元数据(如来源文件、章节、页码)。检索时可以利用这些元数据进行过滤。

# file_path: metadata_filter.py
from langchain.vectorstores import Chroma
from langchain.embeddings import HuggingFaceEmbeddings

def search_with_metadata_filter():
    """使用元数据过滤进行检索"""
    embeddings = HuggingFaceEmbeddings(model_name="BAAI/bge-base-en-v1.5")
    vectorstore = Chroma(persist_directory="./chroma_db", embedding_function=embeddings)

    # 假设我们在存储时,为每个文档添加了 `source` 和 `page` 元数据
    # 检索时,我们可以添加过滤器
    results = vectorstore.similarity_search(
        query="神经网络结构",
        k=5,
        filter={"source": "deep_learning_book.pdf"} # 只从这本书里找
        # 更复杂的过滤: filter={"$or": [{"source": "book1"}, {"page": {"$gte": 10}}]}
    )
    for doc in results:
        print(f"Source: {doc.metadata.get('source')}, Page: {doc.metadata.get('page')}")
        print(f"Content: {doc.page_content[:150]}\n")

关键点 :合理的元数据设计(如文档类型、日期、作者、重要性标签)能极大提升检索的针对性和准确性。

5. 用LangGraph构建智能RAG工作流

现在,我们将使用LangGraph把基础RAG和优化策略组合成一个更智能的、有状态的工作流。这个工作流将具备 条件路由 能力:如果检索结果置信度低,则转向其他处理方式。

5.1 定义状态与节点

首先,定义整个图的状态(State)和各个节点(Node)的功能。

# file_path: langgraph_rag.py
from typing import TypedDict, List, Annotated
from langgraph.graph import StateGraph, END
from langchain_core.documents import Document
from langchain_community.llms import Ollama
from langchain.prompts import ChatPromptTemplate
from langchain.schema.output_parser import StrOutputParser
import operator
from advanced_retriever import create_advanced_retriever # 导入我们之前写的高级检索器

# 1. 定义状态结构
class GraphState(TypedDict):
    """
    图的状态,在节点间传递。
    """
    question: str # 用户原始问题
    retrieved_docs: List[Document] # 检索到的文档
    generation: str # LLM生成的最终答案
    decision: str # 路由决策,例如 "proceed", "web_search", "clarify"

# 2. 初始化组件
llm = Ollama(model="llama3")
retriever = create_advanced_retriever() # 使用带重排的检索器

# 3. 定义各个节点函数
def retrieve(state: GraphState):
    """检索节点:从知识库中获取相关文档"""
    print(f"---执行检索: {state['question'][:50]}...")
    docs = retriever.invoke(state["question"])
    # 简单计算平均相关性分数(假设文档有'score'元数据)
    avg_score = sum([d.metadata.get('score', 0) for d in docs]) / len(docs) if docs else 0
    print(f"检索到 {len(docs)} 个文档,平均相关性分数: {avg_score:.3f}")
    return {"retrieved_docs": docs, "decision": "proceed" if avg_score > 0.5 else "low_confidence"}

def generate_answer(state: GraphState):
    """生成节点:基于检索到的文档生成答案"""
    print("---执行生成---")
    context = "\n\n".join([doc.page_content for doc in state["retrieved_docs"]])
    prompt = ChatPromptTemplate.from_messages([
        ("system", "你是一个专业的助手,请严格根据以下上下文回答问题。如果上下文不包含答案,请明确说'根据已知信息无法回答'。"),
        ("human", "上下文:\n{context}\n\n问题:{question}")
    ])
    chain = prompt | llm | StrOutputParser()
    answer = chain.invoke({"context": context, "question": state["question"]})
    return {"generation": answer}

def decide_route(state: GraphState):
    """决策节点:根据检索结果的质量决定下一步"""
    print("---执行路由决策---")
    # 这里可以根据 retrieved_docs 的长度、分数、内容等做更复杂的判断
    if state["decision"] == "low_confidence":
        # 例如,可以路由到一个“联网搜索”节点,或者直接返回一个提示
        # 这里我们简单返回一个提示
        return {"generation": "抱歉,我的知识库中关于这个问题的信息不足,无法给出准确答案。"}
    else:
        # 质量合格,继续到生成节点
        return {"decision": "to_generate"}

def web_search_fallback(state: GraphState):
    """(示例)联网搜索回退节点"""
    # 这里可以集成 Tavily、SerpAPI 等搜索工具
    print("---执行联网搜索---")
    # simulated_search_result = "这是模拟的联网搜索到的信息..."
    # return {"retrieved_docs": [Document(page_content=simulated_search_result)], "decision": "proceed"}
    return {"generation": "(此示例未实现真实搜索)建议您尝试使用搜索引擎获取最新信息。"}

5.2 构建并运行图

将节点连接起来,并定义路由逻辑。

# file_path: langgraph_rag.py (续)
# 4. 构建图
workflow = StateGraph(GraphState)

# 添加节点
workflow.add_node("retrieve", retrieve)
workflow.add_node("generate", generate_answer)
workflow.add_node("decide", decide_route)
workflow.add_node("web_search", web_search_fallback)

# 设置入口点
workflow.set_entry_point("retrieve")

# 定义边(连接)
workflow.add_edge("retrieve", "decide")
# 条件路由:根据 `decision` 字段的值决定下一步
workflow.add_conditional_edges(
    "decide",
    lambda state: state.get("decision"),
    {
        "to_generate": "generate", # 去生成答案
        "low_confidence": "web_search", # 去联网搜索
    }
)
workflow.add_edge("generate", END)
workflow.add_edge("web_search", END)

# 编译图
app = workflow.compile()

# 5. 运行图
if __name__ == "__main__":
    # 初始化状态
    initial_state = GraphState(question="LangGraph在RAG中起什么作用?")
    # 运行
    final_state = app.invoke(initial_state)
    print("\n" + "="*50)
    print(f"最终答案:\n{final_state['generation']}")
    print("="*50)

这个工作流实现了:检索 -> 判断置信度 -> 高则生成答案,低则转向备用方案(如联网搜索)。你可以通过扩展 decide_route 函数和添加更多节点(如答案验证、多步查询改写)来构建更复杂的智能体。

6. 终极定制:使用LoRA微调Embedding模型

当通用Embedding模型在你的专属领域(如医疗病历、法律条文、金融报告)上表现不佳时,微调是提升效果的关键手段。 LoRA(Low-Rank Adaptation) 是一种参数高效的微调方法,只需训练极少量参数,就能让大模型适配新任务。

6.1 LoRA微调Embedding模型原理

我们不是微调整个庞大的模型(可能数亿参数),而是为模型中的线性层注入可训练的“低秩适配器”。训练时,原模型权重冻结,只更新适配器参数,从而大幅降低计算和存储成本。

6.2 准备训练数据

你需要一个由 (query, positive_doc, negative_doc) 组成的三元组数据集。 positive_doc 是与 query 相关的正例, negative_doc 是不相关或相关性弱的负例。

# file_path: prepare_lora_data.py
from datasets import Dataset
import json

# 示例:构造一个简单的训练数据
train_data = [
    {
        "query": "什么是Transformer架构?",
        "positive": "Transformer是一种基于自注意力机制的深度学习模型架构,广泛应用于NLP领域...",
        "negative": "卷积神经网络(CNN)主要用于计算机视觉任务,如图像分类..."
    },
    {
        "query": "如何配置ChromaDB的持久化路径?",
        "positive": "在创建Chroma客户端时,通过`persist_directory`参数指定本地目录即可实现持久化。",
        "negative": "MySQL数据库的连接字符串通常包含主机、端口、用户名和密码。"
    }
]

# 保存为JSON文件
with open("./data/train_data.jsonl", "w") as f:
    for item in train_data:
        f.write(json.dumps(item, ensure_ascii=False) + "\n")

# 使用datasets库加载
dataset = Dataset.from_json("./data/train_data.jsonl", split="train")
print(dataset)

6.3 使用PEFT进行LoRA微调

以下是一个简化的微调脚本框架,展示了核心步骤。

# file_path: train_lora_embedding.py
from transformers import AutoModel, AutoTokenizer, TrainingArguments
from peft import LoraConfig, get_peft_model, TaskType
from trl import SFTTrainer
from datasets import load_dataset
import torch

# 1. 加载模型和分词器
model_name = "BAAI/bge-base-en-v1.5"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModel.from_pretrained(model_name)

# 2. 配置LoRA
lora_config = LoraConfig(
    task_type=TaskType.FEATURE_EXTRACTION, # 特征提取任务
    r=8, # LoRA的秩,越小参数量越少
    lora_alpha=32,
    lora_dropout=0.1,
    target_modules=["query", "key", "value"] # 针对Transformer的QKV矩阵
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters() # 查看可训练参数占比,通常<1%

# 3. 定义数据预处理函数
def preprocess_function(examples):
    """将三元组数据转换为模型输入"""
    queries = examples["query"]
    positives = examples["positive"]
    negatives = examples["negative"]
    # 这里需要根据你选择的损失函数(如对比损失、三元组损失)来构造批次
    # 以下是一个简化示例,实际训练需要更复杂的数据整理和损失计算
    batch = tokenizer(queries, padding="max_length", truncation=True, max_length=512)
    return batch

dataset = load_dataset("json", data_files="./data/train_data.jsonl", split="train")
tokenized_dataset = dataset.map(preprocess_function, batched=True)

# 4. 定义训练参数
training_args = TrainingArguments(
    output_dir="./bge-lora-finetuned",
    per_device_train_batch_size=4,
    num_train_epochs=3,
    logging_dir="./logs",
    save_strategy="epoch",
    evaluation_strategy="no", # 示例中未准备验证集
)

# 5. 创建Trainer (此处使用SFTTrainer,实际需自定义损失函数)
# 注意:Embedding模型的微调通常需要自定义Trainer和损失函数(如CosineEmbeddingLoss, TripletLoss)
# 这里仅为流程展示,直接运行可能不工作。
trainer = SFTTrainer(
    model=model,
    args=training_args,
    train_dataset=tokenized_dataset,
    tokenizer=tokenizer,
)
print("开始训练...(此示例需要自定义损失函数才能有效训练Embedding模型)")
# trainer.train()

关键点 :微调Embedding模型的核心在于 损失函数 的设计(如对比损失、三元组损失),这需要你根据数据格式自定义训练循环。上述代码主要展示了使用PEFT库注入LoRA适配器的流程。

6.4 使用微调后的模型

训练完成后,你可以像使用原模型一样加载并使用它。

from peft import PeftModel
from transformers import AutoModel

base_model = AutoModel.from_pretrained("BAAI/bge-base-en-v1.5")
fine_tuned_model = PeftModel.from_pretrained(base_model, "./bge-lora-finetuned/checkpoint-xxx")
# 合并LoRA权重到基础模型,便于部署
merged_model = fine_tuned_model.merge_and_unload()
merged_model.save_pretrained("./bge-finetuned-merged")

然后,在LangChain中指定新的模型路径即可。

embeddings = HuggingFaceEmbeddings(model_name="./bge-finetuned-merged")

7. 常见问题与排查思路

在构建RAG系统的每个阶段,你都可能遇到以下典型问题:

问题现象 可能原因 排查方式 解决方案
检索结果完全不相关 1. Embedding模型与领域不匹配。
2. 文本分割不合理(chunk过大或过小)。
3. 查询与文档语言不一致。
1. 检查Embedding模型名称。
2. 打印出文本块内容,看分割是否破坏了语义。
3. 计算查询与几个已知相关文档的相似度。
1. 更换或微调Embedding模型。
2. 调整 chunk_size chunk_overlap ,尝试按段落或句子分割。
3. 确保查询与文档语言一致。
LLM回答“根据上下文无法回答”,但明明有相关文档 1. 检索到的文档未有效送入Prompt。
2. Prompt指令不清晰。
3. 上下文过长,超出模型窗口。
1. 检查 source_documents ,确认检索是否成功。
2. 查看构建的完整Prompt。
3. 检查上下文总长度。
1. 修复检索器连接。
2. 优化System Prompt,使用更明确的指令。
3. 减少 k 值,或使用 map_reduce 等链式类型处理长上下文。
系统响应速度慢 1. Embedding模型在CPU上运行。
2. 向量数据库索引未优化。
3. 重排模型计算开销大。
1. 监控各阶段耗时。
2. 检查向量数据库索引类型(如HNSW)。
3. 检查重排模型的 top_n 设置。
1. 使用GPU运行Embedding和重排模型。
2. 为向量数据库创建合适的索引。
3. 调整初步检索的 k 和重排的 top_n ,或在特定场景下关闭重排。
Ollama LLM服务连接失败 1. Ollama服务未启动。
2. 模型未下载。
1. 命令行运行 ollama serve 检查服务状态。
2. 运行 ollama list 检查模型是否存在。
1. 确保Ollama服务在运行。
2. 使用 ollama pull llama3 下载指定模型。
ChromaDB持久化后加载失败 1. 保存和加载时使用的Embedding函数不一致。
2. 持久化目录被破坏。
1. 确认两次使用的 model_name 完全相同。
2. 检查目录下是否有 chroma.sqlite3 等文件。
1. 统一Embedding模型配置。
2. 尝试重新生成向量数据库。

8. 最佳实践与工程建议

要将一个实验性的RAG管道升级为生产系统,请关注以下方面:

  1. 数据预处理是根基

    • 清洗 :去除无关字符、乱码、页眉页脚。
    • 标准化 :统一日期、单位、缩写格式。
    • 高质量分割 :尝试不同的分割策略(语义分割、固定长度、递归字符),并通过评估选择最佳方案。
  2. 检索策略组合拳

    • 混合检索 :结合 密集向量检索 (语义相似)和 稀疏检索 (如BM25,关键词匹配),取长补短。
    • 多路召回 :使用不同的Embedding模型或检索参数进行多次检索,然后合并去重。
    • 查询改写/扩展 :在检索前,使用LLM对原始查询进行改写或扩展,以提高召回率。
  3. 提示工程优化

    • 清晰的系统指令 :明确要求模型“基于给定上下文回答”,并定义无法回答时的行为。
    • 上下文结构化 :在Prompt中清晰分隔不同来源的上下文,可附加来源信息。
    • 少样本示例(Few-Shot) :在Prompt中提供一两个问答示例,引导模型输出格式和风格。
  4. 评估与监控

    • 定义评估指标 :至少包括 检索命中率 (检索到的文档是否相关)和 答案忠实度 (答案是否基于上下文)。
    • 构建测试集 :收集一批真实用户问题及标准答案,用于定期回归测试。
    • 记录日志 :记录每次问答的查询、检索到的文档ID、生成的答案、耗时和置信度分数,便于分析和优化。
  5. 安全与成本

    • 输入过滤 :对用户查询进行敏感词过滤和恶意指令检测。
    • 输出审查 :对模型生成的内容进行安全性审查。
    • 成本控制 :监控LLM API调用次数和Token消耗,设置用量告警。对于内部应用,优先考虑本地模型。

从理解RAG的核心价值,到动手搭建一个包含Embedding、向量数据库、检索优化和LangGraph智能编排的完整流程,再到探索通过LoRA微调进行深度定制,你已经走完了构建一个现代RAG应用的关键路径。记住,RAG不是一个一劳永逸的解决方案,而是一个需要持续迭代优化的系统工程。成功的核心在于对业务数据的深刻理解,以及对每个技术环节的精心调优。建议你从一个小而具体的场景开始,构建最小可行产品(MVP),然后依据评估数据,逐个环节地进行优化和升级。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐