企业级RAG系统架构与实战完全教程

教程简介

全面讲解企业级RAG系统的核心架构与实战开发,涵盖RAG架构演进、多模态文档解析、Chunking策略优化、向量数据库选型、混合检索、Reranking模型、查询改写、幻觉检测、RAG评估框架、大规模RAG运维等核心内容,帮助开发者构建生产级RAG系统。

企业级RAG系统架构与实战完全教程

教程简介

检索增强生成(Retrieval-Augmented Generation, RAG)已成为企业级AI应用的核心架构模式。本教程从RAG的基础原理出发,系统讲解企业级RAG系统的架构设计、核心组件、实战部署与运维优化,帮助开发者构建生产级别的智能检索系统。


第一章:RAG架构演进与设计原则

1.1 从朴素RAG到高级RAG

RAG的核心思想是将外部知识库与大语言模型(LLM)结合,通过检索相关文档来增强模型的生成能力。

朴素RAG(Naive RAG) 的基本流程:

用户查询 → 文档检索 → 拼接上下文 → LLM生成 → 返回结果

高级RAG(Advanced RAG) 在此基础上增加了查询优化、多路检索、重排序、幻觉检测等环节:

用户查询 → 查询理解/改写 → 多路检索 → 重排序 → 上下文压缩 → LLM生成 → 事实性验证 → 返回结果

模块化RAG(Modular RAG) 将系统拆分为可组合的独立模块:

class ModularRAG:
    """模块化RAG架构"""
    def __init__(self):
        self.query_processor = QueryProcessor()    # 查询处理
        self.retriever = HybridRetriever()         # 混合检索
        self.reranker = CrossEncoderReranker()     # 重排序
        self.context_manager = ContextManager()     # 上下文管理
        self.generator = LLMGenerator()            # 生成
        self.validator = HallucinationValidator()   # 幻觉验证
        self.memory = ConversationMemory()         # 对话记忆

1.2 企业级RAG的特殊挑战

企业场景与Demo级RAG有本质区别:

维度 Demo级 企业级
文档量 几十篇 百万级
文档类型 纯文本 PDF/表格/图片/音视频
查询复杂度 简单问答 多轮对话/复杂推理
准确性要求 大致正确 必须可溯源
延迟要求 几秒可接受 P99 < 3秒
安全性 无要求 权限控制/审计

第二章:多模态文档解析

2.1 PDF文档解析

企业文档大量以PDF格式存在,解析质量直接影响RAG效果。

# 使用 PyMuPDF4LLM 进行高质量PDF解析
import pymupdf4llm

def parse_pdf_with_pymupdf(pdf_path: str) -> str:
    """将PDF转换为Markdown格式,保留结构信息"""
    md_text = pymupdf4llm.to_markdown(
        pdf_path,
        page_chunks=True,       # 按页分块
        write_images=False,     # 不提取图片
        image_path="images/",
        dpi=150
    )
    return md_text


# 使用 Unstructured 进行复杂PDF解析
from unstructured.partition.pdf import partition_pdf

def parse_complex_pdf(pdf_path: str) -> list:
    """解析包含表格、图片的复杂PDF"""
    elements = partition_pdf(
        filename=pdf_path,
        strategy="hi_res",           # 高精度模式
        model_name="yolox",          # 版面分析模型
        extract_images_in_pdf=True,
        infer_table_structure=True,
        chunking_strategy="by_title",
        max_characters=1000,
        new_after_n_chars=800,
        overlap=200
    )
    return elements

2.2 表格数据处理

表格是企业文档中最常见也最难处理的结构。

import pandas as pd
from io import StringIO

class TableProcessor:
    """表格处理器:将表格转换为LLM可理解的文本"""
    
    def table_to_natural_language(self, table: pd.DataFrame, context: str = "") -> str:
        """将表格转换为自然语言描述"""
        descriptions = []
        
        # 表头信息
        headers = ", ".join(table.columns.tolist())
        descriptions.append(f"该表格包含以下列:{headers}")
        
        # 逐行描述
        for idx, row in table.iterrows():
            row_desc = []
            for col in table.columns:
                row_desc.append(f"{col}为{row[col]}")
            descriptions.append(f"第{idx+1}行:{','.join(row_desc)}")
        
        return "\n".join(descriptions)
    
    def table_to_markdown(self, table: pd.DataFrame) -> str:
        """将DataFrame转为Markdown表格"""
        return table.to_markdown(index=False)
    
    def smart_chunk_table(self, table: pd.DataFrame, max_rows: int = 20) -> list:
        """大表格智能分块"""
        chunks = []
        for i in range(0, len(table), max_rows):
            chunk = table.iloc[i:i+max_rows]
            chunks.append(self.table_to_markdown(chunk))
        return chunks

2.3 图片与多模态内容

import base64

class ImageProcessor:
    """图片处理器:提取图片中的文字和语义信息"""
    
    def __init__(self, vlm_client):
        self.vlm = vlm_client  # 视觉语言模型
    
    async def extract_from_image(self, image_path: str) -> dict:
        """使用VLM提取图片中的文字和语义"""
        with open(image_path, "rb") as f:
            img_base64 = base64.b64encode(f.read()).decode()
        
        prompt = """请分析这张图片,提取以下信息:
1. 图片中的所有文字内容(OCR)
2. 图片的类型(图表/流程图/截图/照片等)
3. 图片所表达的核心信息
4. 如果是图表,请描述数据趋势和关键数值
        
请以JSON格式返回结果。"""
        
        result = await self.vlm.chat(
            messages=[{
                "role": "user",
                "content": [
                    {"type": "text", "text": prompt},
                    {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{img_base64}"}}
                ]
            }]
        )
        return result

第三章:Chunking策略与优化

3.1 常见分块策略

Chunking是RAG系统中影响检索质量的关键环节。

from typing import List

class TextChunker:
    """文本分块器"""
    
    def fixed_size_chunk(self, text: str, chunk_size: int = 512, overlap: int = 50) -> List[str]:
        """固定大小分块"""
        chunks = []
        start = 0
        while start < len(text):
            end = start + chunk_size
            chunks.append(text[start:end])
            start = end - overlap
        return chunks
    
    def recursive_split(self, text: str, chunk_size: int = 1000, 
                        separators: List[str] = None) -> List[str]:
        """递归字符分割:按语义边界分块"""
        if separators is None:
            separators = ["\n\n", "\n", "。", "!", "?", ".", "!", "?", " "]
        
        chunks = []
        current_chunks = [text]
        
        for sep in separators:
            new_chunks = []
            for chunk in current_chunks:
                if len(chunk) <= chunk_size:
                    new_chunks.append(chunk)
                else:
                    splits = chunk.split(sep)
                    current = ""
                    for split in splits:
                        if len(current) + len(split) + len(sep) <= chunk_size:
                            current = current + sep + split if current else split
                        else:
                            if current:
                                new_chunks.append(current)
                            current = split
                    if current:
                        new_chunks.append(current)
            current_chunks = new_chunks
        
        return [c.strip() for c in current_chunks if c.strip()]
    
    def semantic_chunk(self, text: str, embedding_model, threshold: float = 0.5) -> List[str]:
        """语义分块:基于嵌入相似度的智能分块"""
        import numpy as np
        
        # 按句子分割
        sentences = self._split_sentences(text)
        
        # 计算每个句子的嵌入
        embeddings = embedding_model.encode(sentences)
        
        # 计算相邻句子的相似度
        similarities = []
        for i in range(len(embeddings) - 1):
            sim = np.dot(embeddings[i], embeddings[i+1]) / (
                np.linalg.norm(embeddings[i]) * np.linalg.norm(embeddings[i+1])
            )
            similarities.append(sim)
        
        # 在相似度低于阈值处切分
        chunks = []
        current_chunk = [sentences[0]]
        for i, sim in enumerate(similarities):
            if sim < threshold:
                chunks.append(" ".join(current_chunk))
                current_chunk = [sentences[i+1]]
            else:
                current_chunk.append(sentences[i+1])
        
        if current_chunk:
            chunks.append(" ".join(current_chunk))
        
        return chunks

3.2 高级分块:Parent-Child策略

class ParentChildChunker:
    """父子分块策略:检索小块,返回大块"""
    
    def __init__(self, child_size=256, parent_size=1024, overlap=64):
        self.child_size = child_size
        self.parent_size = parent_size
        self.overlap = overlap
    
    def chunk(self, text: str) -> dict:
        """生成父子块"""
        # 生成大块(父块)
        parent_chunks = self._split(text, self.parent_size, self.overlap)
        
        result = {"parents": [], "children": []}
        
        for parent_idx, parent in enumerate(parent_chunks):
            result["parents"].append({
                "id": f"parent_{parent_idx}",
                "text": parent,
                "metadata": {"parent_id": f"parent_{parent_idx}"}
            })
            
            # 将大块拆分为小块(子块)
            child_chunks = self._split(parent, self.child_size, self.overlap // 2)
            for child_idx, child in enumerate(child_chunks):
                result["children"].append({
                    "id": f"child_{parent_idx}_{child_idx}",
                    "text": child,
                    "parent_id": f"parent_{parent_idx}",
                    "metadata": {"parent_id": f"parent_{parent_idx}"}
                })
        
        return result
    
    def _split(self, text, size, overlap):
        """按句子边界分块"""
        sentences = text.replace("。", "。\n").replace("!", "!\n").replace("?", "?\n").split("\n")
        chunks = []
        current = ""
        for sent in sentences:
            if len(current) + len(sent) <= size:
                current += sent
            else:
                if current:
                    chunks.append(current.strip())
                current = sent
        if current:
            chunks.append(current.strip())
        return chunks

第四章:向量数据库选型与实战

4.1 主流向量数据库对比

特性 Milvus Qdrant Weaviate Chroma
语言 Go/C++ Rust Go Python
部署复杂度 高 中 中 低
性能 极高 高 高 中
标量过滤 ✅ ✅ ✅ ✅
多租户 ✅ ✅ ✅ ❌
适用规模 十亿级 千万级 千万级 百万级
云服务 Zilliz Cloud Qdrant Cloud WCS ❌

4.2 Milvus实战

from pymilvus import connections, Collection, FieldSchema, CollectionSchema, DataType, utility

class MilvusVectorStore:
    """Milvus向量数据库操作封装"""
    
    def __init__(self, host="localhost", port="19530"):
        connections.connect(host=host, port=port)
        self.collection = None
    
    def create_collection(self, name: str, dim: int = 1536):
        """创建集合"""
        fields = [
            FieldSchema(name="id", dtype=DataType.VARCHAR, is_primary=True, max_length=64),
            FieldSchema(name="text", dtype=DataType.VARCHAR, max_length=65535),
            FieldSchema(name="embedding", dtype=DataType.FLOAT_VECTOR, dim=dim),
            FieldSchema(name="metadata", dtype=DataType.JSON),
        ]
        schema = CollectionSchema(fields, description="RAG document chunks")
        self.collection = Collection(name, schema)
        
        # 创建向量索引
        index_params = {
            "metric_type": "COSINE",
            "index_type": "HNSW",
            "params": {"M": 16, "efConstruction": 256}
        }
        self.collection.create_index("embedding", index_params)
        return self.collection
    
    def insert(self, ids: list, texts: list, embeddings: list, metadata: list):
        """批量插入数据"""
        data = [ids, texts, embeddings, metadata]
        self.collection.insert(data)
        self.collection.flush()
    
    def search(self, query_embedding: list, top_k: int = 10, 
               filter_expr: str = None) -> list:
        """向量检索"""
        self.collection.load()
        
        search_params = {"metric_type": "COSINE", "params": {"ef": 128}}
        
        results = self.collection.search(
            data=[query_embedding],
            anns_field="embedding",
            param=search_params,
            limit=top_k,
            expr=filter_expr,
            output_fields=["text", "metadata"]
        )
        
        return [
            {
                "id": hit.id,
                "text": hit.entity.get("text"),
                "score": hit.score,
                "metadata": hit.entity.get("metadata")
            }
            for hit in results[0]
        ]

4.3 Qdrant实战

from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams, PointStruct, Filter, FieldCondition

class QdrantVectorStore:
    """Qdrant向量数据库操作封装"""
    
    def __init__(self, host="localhost", port=6333):
        self.client = QdrantClient(host=host, port=port)
    
    def create_collection(self, name: str, dim: int = 1536):
        """创建集合"""
        self.client.create_collection(
            collection_name=name,
            vectors_config=VectorParams(size=dim, distance=Distance.COSINE)
        )
    
    def upsert(self, collection: str, points: list):
        """插入或更新数据"""
        self.client.upsert(
            collection_name=collection,
            points=[
                PointStruct(
                    id=p["id"],
                    vector=p["embedding"],
                    payload={"text": p["text"], "metadata": p.get("metadata", {})}
                )
                for p in points
            ]
        )
    
    def search(self, collection: str, query_vector: list, 
               top_k: int = 10, filter_conditions: dict = None) -> list:
        """向量检索"""
        query_filter = None
        if filter_conditions:
            query_filter = Filter(
                must=[
                    FieldCondition(key=k, match={"value": v})
                    for k, v in filter_conditions.items()
                ]
            )
        
        results = self.client.search(
            collection_name=collection,
            query_vector=query_vector,
            limit=top_k,
            query_filter=query_filter
        )
        
        return [
            {
                "id": r.id,
                "text": r.payload.get("text"),
                "score": r.score,
                "metadata": r.payload.get("metadata", {})
            }
            for r in results
        ]

第五章:混合检索策略

5.1 稠密检索 + 稀疏检索

单一检索方式难以覆盖所有场景,混合检索能显著提升召回率。

from sentence_transformers import SentenceTransformer
from rank_bm25 import BM25Okapi
import numpy as np

class HybridRetriever:
    """混合检索器:稠密向量 + BM25稀疏检索"""
    
    def __init__(self, embedding_model_name: str = "BAAI/bge-large-zh-v1.5"):
        self.dense_model = SentenceTransformer(embedding_model_name)
        self.bm25 = None
        self.documents = []
        self.dense_index = None
    
    def build_index(self, documents: list):
        """构建索引"""
        self.documents = documents
        texts = [doc["text"] for doc in documents]
        
        # BM25索引
        tokenized = [list(text) for text in texts]  # 中文按字符分词
        self.bm25 = BM25Okapi(tokenized)
        
        # 稠密向量索引
        self.dense_index = self.dense_model.encode(texts, show_progress_bar=True)
    
    def search(self, query: str, top_k: int = 10, 
               dense_weight: float = 0.7, sparse_weight: float = 0.3) -> list:
        """混合检索"""
        # 稠密检索
        query_embedding = self.dense_model.encode([query])[0]
        dense_scores = np.dot(self.dense_index, query_embedding)
        
        # 稀疏检索
        query_tokens = list(query)
        sparse_scores = self.bm25.get_scores(query_tokens)
        
        # 归一化
        dense_scores = (dense_scores - dense_scores.min()) / (dense_scores.max() - dense_scores.min() + 1e-8)
        sparse_scores = (sparse_scores - sparse_scores.min()) / (sparse_scores.max() - sparse_scores.min() + 1e-8)
        
        # 加权融合
        combined_scores = dense_weight * dense_scores + sparse_weight * sparse_scores
        
        # 排序
        top_indices = np.argsort(combined_scores)[::-1][:top_k]
        
        results = []
        for idx in top_indices:
            results.append({
                "text": self.documents[idx]["text"],
                "score": float(combined_scores[idx]),
                "dense_score": float(dense_scores[idx]),
                "sparse_score": float(sparse_scores[idx]),
                "metadata": self.documents[idx].get("metadata", {})
            })
        
        return results

5.2 ColBERT晚期交互检索

# 使用 ColBERT 进行晚期交互检索
# pip install ragatouille

from ragatouille import RAGPretrainedModel

class ColBERTRetriever:
    """ColBERT晚期交互检索器"""
    
    def __init__(self, model_path: str = "colbert-ir/colbertv2.0"):
        self.model = RAGPretrainedModel.from_pretrained(model_path)
    
    def index(self, documents: list, collection_name: str = "my_index"):
        """构建ColBERT索引"""
        self.model.index(
            collection=[doc["text"] for doc in documents],
            index_name=collection_name,
            max_document_length=256,
            split_documents=True
        )
    
    def search(self, query: str, top_k: int = 10) -> list:
        """ColBERT检索"""
        results = self.model.search(query, k=top_k)
        return results

第六章:重排序(Reranking)

6.1 Cross-Encoder重排序

检索返回的候选文档需要经过精排来提升相关性。

from sentence_transformers import CrossEncoder

class Reranker:
    """Cross-Encoder重排序器"""
    
    def __init__(self, model_name: str = "BAAI/bge-reranker-v2-m3"):
        self.model = CrossEncoder(model_name, max_length=512)
    
    def rerank(self, query: str, documents: list, top_k: int = 5) -> list:
        """对检索结果重排序"""
        pairs = [[query, doc["text"]] for doc in documents]
        scores = self.model.predict(pairs)
        
        for i, doc in enumerate(documents):
            doc["rerank_score"] = float(scores[i])
        
        # 按重排序分数排序
        reranked = sorted(documents, key=lambda x: x["rerank_score"], reverse=True)
        return reranked[:top_k]


class LLMReranker:
    """使用LLM进行重排序(适合高质量场景)"""
    
    def __init__(self, llm_client):
        self.llm = llm_client
    
    async def rerank(self, query: str, documents: list, top_k: int = 5) -> list:
        """LLM重排序"""
        doc_list = "\n".join([
            f"[{i+1}] {doc['text'][:200]}..."
            for i, doc in enumerate(documents)
        ])
        
        prompt = f"""请根据与查询的相关性,对以下文档进行排序。
查询:{query}

文档列表:
{doc_list}

请返回排序后的文档编号(从最相关到最不相关),用逗号分隔。
例如:3,1,5,2,4

排序结果:"""
        
        result = await self.llm.chat(messages=[{"role": "user", "content": prompt}])
        
        # 解析排序结果
        try:
            order = [int(x.strip()) - 1 for x in result.strip().split(",")]
            reranked = [documents[i] for i in order if i < len(documents)]
            return reranked[:top_k]
        except:
            return documents[:top_k]

第七章:查询理解与改写

7.1 查询意图识别

class QueryProcessor:
    """查询处理器"""
    
    def __init__(self, llm_client):
        self.llm = llm_client
    
    async def classify_intent(self, query: str) -> str:
        """查询意图分类"""
        prompt = f"""请对以下查询进行意图分类:
查询:{query}

可选分类:
- factual:事实性问答
- comparison:对比分析
- how-to:操作指导
- summary:总结归纳
- analysis:深入分析

请只返回分类名称。"""
        
        result = await self.llm.chat(messages=[{"role": "user", "content": prompt}])
        return result.strip().lower()
    
    async def rewrite_query(self, query: str, history: list = None) -> str:
        """查询改写:优化检索效果"""
        history_text = ""
        if history:
            history_text = "\n对话历史:\n" + "\n".join([
                f"用户:{h['user']}\n助手:{h['assistant']}"
                for h in history[-3:]
            ])
        
        prompt = f"""请将以下查询改写为更适合检索的形式,保持核心意图不变。
{history_text}

原始查询:{query}

改写后的查询(只返回改写结果):"""
        
        result = await self.llm.chat(messages=[{"role": "user", "content": prompt}])
        return result.strip()
    
    async def generate_sub_queries(self, query: str) -> list:
        """查询分解:将复杂查询拆分为子查询"""
        prompt = f"""请将以下复杂查询分解为2-4个更具体的子查询:
原始查询:{query}

请以JSON数组格式返回子查询列表。
示例:["子查询1", "子查询2", "子查询3"]"""
        
        result = await self.llm.chat(messages=[{"role": "user", "content": prompt}])
        import json
        try:
            return json.loads(result.strip())
        except:
            return [query]

第八章:幻觉检测与事实性验证

8.1 基于检索的验证

class HallucinationValidator:
    """幻觉检测器"""
    
    def __init__(self, llm_client, embedding_model):
        self.llm = llm_client
        self.embedding_model = embedding_model
    
    async def validate(self, query: str, answer: str, 
                       source_documents: list) -> dict:
        """验证生成内容的事实性"""
        
        # 提取答案中的关键声明
        claims = await self._extract_claims(answer)
        
        # 对每个声明进行验证
        validations = []
        for claim in claims:
            # 在源文档中查找支持证据
            evidence = await self._find_evidence(claim, source_documents)
            
            # 判断是否有充分支持
            is_supported = await self._check_support(claim, evidence)
            
            validations.append({
                "claim": claim,
                "is_supported": is_supported,
                "evidence": evidence
            })
        
        # 计算事实性得分
        supported_count = sum(1 for v in validations if v["is_supported"])
        factuality_score = supported_count / len(validations) if validations else 1.0
        
        return {
            "factuality_score": factuality_score,
            "validations": validations,
            "is_faithful": factuality_score >= 0.8
        }
    
    async def _extract_claims(self, text: str) -> list:
        """提取文本中的关键声明"""
        prompt = f"""请从以下文本中提取所有事实性声明(可验证的陈述):
{text}

请以JSON数组格式返回,每个元素为一个声明。"""
        
        result = await self.llm.chat(messages=[{"role": "user", "content": prompt}])
        import json
        try:
            return json.loads(result.strip())
        except:
            return [text]
    
    async def _find_evidence(self, claim: str, documents: list) -> list:
        """在源文档中查找支持证据"""
        claim_embedding = self.embedding_model.encode([claim])[0]
        
        evidence = []
        for doc in documents:
            doc_embedding = self.embedding_model.encode([doc["text"]])[0]
            similarity = np.dot(claim_embedding, doc_embedding) / (
                np.linalg.norm(claim_embedding) * np.linalg.norm(doc_embedding)
            )
            if similarity > 0.5:
                evidence.append({"text": doc["text"], "score": float(similarity)})
        
        return sorted(evidence, key=lambda x: x["score"], reverse=True)[:3]
    
    async def _check_support(self, claim: str, evidence: list) -> bool:
        """判断声明是否有充分证据支持"""
        if not evidence:
            return False
        
        evidence_text = "\n".join([e["text"][:300] for e in evidence])
        
        prompt = f"""根据以下证据,判断声明是否得到支持。

声明:{claim}

证据:
{evidence_text}

请回答:SUPPORTED(有支持)或 NOT_SUPPORTED(无支持)
只返回判断结果。"""
        
        result = await self.llm.chat(messages=[{"role": "user", "content": prompt}])
        return "SUPPORTED" in result.upper()

第九章:RAG评估框架

9.1 RAGAS评估

# pip install ragas

from ragas import evaluate
from ragas.metrics import (
    faithfulness,       # 忠实度
    answer_relevancy,   # 答案相关性
    context_precision,  # 上下文精确度
    context_recall      # 上下文召回率
)
from datasets import Dataset

class RAGEvaluator:
    """RAG系统评估器"""
    
    def evaluate_with_ragas(self, eval_data: list) -> dict:
        """使用RAGAS框架评估"""
        dataset = Dataset.from_list(eval_data)
        
        result = evaluate(
            dataset=dataset,
            metrics=[
                faithfulness,
                answer_relevancy,
                context_precision,
                context_recall
            ]
        )
        
        return {
            "faithfulness": result["faithfulness"],
            "answer_relevancy": result["answer_relevancy"],
            "context_precision": result["context_precision"],
            "context_recall": result["context_recall"],
            "overall": result["faithfulness"] * 0.3 + result["answer_relevancy"] * 0.3 + 
                       result["context_precision"] * 0.2 + result["context_recall"] * 0.2
        }
    
    def evaluate_retrieval(self, queries: list, retrieved_docs: list, 
                           ground_truth: list) -> dict:
        """评估检索质量"""
        hits = 0
        mrr_sum = 0
        
        for i, (retrieved, relevant) in enumerate(zip(retrieved_docs, ground_truth)):
            retrieved_ids = [d["id"] for d in retrieved]
            
            # Hit@K
            if any(doc_id in relevant for doc_id in retrieved_ids[:5]):
                hits += 1
            
            # MRR
            for rank, doc_id in enumerate(retrieved_ids[:10]):
                if doc_id in relevant:
                    mrr_sum += 1 / (rank + 1)
                    break
        
        n = len(queries)
        return {
            "hit_at_5": hits / n,
            "mrr_at_10": mrr_sum / n
        }

第十章:完整RAG Pipeline实战

10.1 端到端RAG系统

class EnterpriseRAG:
    """企业级RAG系统完整实现"""
    
    def __init__(self, config: dict):
        self.config = config
        
        # 初始化各组件
        self.document_parser = DocumentParser()
        self.chunker = ParentChildChunker(
            child_size=config.get("child_size", 256),
            parent_size=config.get("parent_size", 1024)
        )
        self.embedding_model = SentenceTransformer(config.get("embedding_model", "BAAI/bge-large-zh-v1.5"))
        self.vector_store = MilvusVectorStore()
        self.reranker = Reranker()
        self.query_processor = QueryProcessor(llm_client=None)
        self.validator = HallucinationValidator(llm_client=None, embedding_model=self.embedding_model)
        self.memory = ConversationMemory(max_turns=5)
    
    async def ingest(self, file_path: str):
        """文档摄入流程"""
        # 1. 解析文档
        content = self.document_parser.parse(file_path)
        
        # 2. 分块
        chunks = self.chunker.chunk(content)
        
        # 3. 生成嵌入
        child_texts = [c["text"] for c in chunks["children"]]
        embeddings = self.embedding_model.encode(child_texts)
        
        # 4. 存入向量数据库
        self.vector_store.insert(
            ids=[c["id"] for c in chunks["children"]],
            texts=child_texts,
            embeddings=embeddings.tolist(),
            metadata=[c["metadata"] for c in chunks["children"]]
        )
        
        # 5. 存储父块映射
        self._store_parent_mapping(chunks["parents"])
        
        return len(chunks["children"])
    
    async def query(self, question: str, session_id: str = None) -> dict:
        """查询流程"""
        # 1. 获取对话历史
        history = self.memory.get_history(session_id)
        
        # 2. 查询处理
        rewritten = await self.query_processor.rewrite_query(question, history)
        
        # 3. 混合检索
        query_embedding = self.embedding_model.encode([rewritten])[0]
        raw_results = self.vector_store.search(
            query_embedding.tolist(), top_k=20
        )
        
        # 4. 重排序
        reranked = self.reranker.rerank(rewritten, raw_results, top_k=5)
        
        # 5. 获取父块上下文
        parent_ids = list(set(r["metadata"].get("parent_id") for r in reranked))
        context = self._get_parent_context(parent_ids)
        
        # 6. 生成回答
        answer = await self._generate(question, context, history)
        
        # 7. 幻觉验证
        validation = await self.validator.validate(question, answer, reranked)
        
        # 8. 更新对话记忆
        self.memory.add(session_id, question, answer)
        
        return {
            "answer": answer,
            "sources": [{"text": r["text"][:200], "score": r.get("rerank_score", 0)} for r in reranked],
            "factuality_score": validation["factuality_score"],
            "is_faithful": validation["is_faithful"]
        }
    
    async def _generate(self, question: str, context: str, history: list) -> str:
        """生成回答"""
        history_text = ""
        if history:
            history_text = "\n".join([
                f"用户:{h['user']}\n助手:{h['assistant']}"
                for h in history[-3:]
            ])
        
        prompt = f"""基于以下上下文信息回答用户的问题。如果上下文中没有相关信息,请明确说明无法回答。

上下文:
{context}

对话历史:
{history_text}

用户问题:{question}

请给出准确、详细的回答,并在回答中标注信息来源。"""
        
        return await self.llm.chat(messages=[{"role": "user", "content": prompt}])

第十一章:大规模RAG系统运维

11.1 索引更新策略

class IndexManager:
    """索引管理器"""
    
    def __init__(self, vector_store, embedding_model):
        self.vector_store = vector_store
        self.embedding_model = embedding_model
    
    async def incremental_update(self, new_documents: list):
        """增量更新索引"""
        for doc in new_documents:
            # 检查文档是否已存在
            existing = await self._check_exists(doc["id"])
            
            if existing:
                # 文档已更新,删除旧数据后重新索引
                await self._delete_document(doc["id"])
            
            # 索引新文档
            await self._index_document(doc)
    
    async def rebuild_index(self, all_documents: list):
        """全量重建索引"""
        # 创建临时集合
        temp_name = f"{self.collection_name}_temp"
        self.vector_store.create_collection(temp_name)
        
        # 批量索引
        batch_size = 1000
        for i in range(0, len(all_documents), batch_size):
            batch = all_documents[i:i+batch_size]
            embeddings = self.embedding_model.encode([d["text"] for d in batch])
            self.vector_store.insert(
                ids=[d["id"] for d in batch],
                texts=[d["text"] for d in batch],
                embeddings=embeddings.tolist(),
                metadata=[d.get("metadata", {}) for d in batch]
            )
        
        # 原子切换
        self._swap_collection(self.collection_name, temp_name)

11.2 性能优化建议

  1. 缓存策略:对高频查询结果进行缓存
  2. 异步处理:全文档摄入采用异步流水线
  3. 批量嵌入:批量调用embedding模型减少API调用次数
  4. 预加载:向量数据库索引预加载到内存
  5. 连接池:数据库和API连接复用

最佳实践总结

  1. 文档预处理是关键:投入足够精力优化文档解析和分块策略
  2. 混合检索优于单一检索:结合稠密和稀疏检索提升召回率
  3. 重排序必不可少:Cross-Encoder重排序能显著提升精确度
  4. 查询改写提升体验:LLM驱动的查询改写能理解用户真实意图
  5. 幻觉检测保障质量:生产环境必须有事实性验证机制
  6. 持续评估迭代:建立评估体系,定期评估和优化系统
  7. 权限控制不可忽视:企业场景必须考虑文档级权限控制

总结

本教程系统讲解了企业级RAG系统的完整技术栈,从文档解析、分块策略、向量数据库、混合检索、重排序、查询改写到幻觉检测和评估框架。RAG系统的核心在于将正确的上下文以正确的方式传递给LLM,每个环节都直接影响最终效果。

随着技术的发展,Agentic RAG、GraphRAG、多模态RAG等新范式不断涌现,RAG正在从简单的检索增强向智能化、自适应的方向演进。掌握本教程的核心技术,将为构建生产级AI应用打下坚实基础。

内容声明

本文内容为AI技术学习教程,仅供学习参考。如涉及技术问题,欢迎通过 xurj005@163.com 与我们交流。

目录