AI Agent开发实战⑳|RAG系统整体优化:从Pipeline到端到端评估

单模块优化完了,召回率从70%提到85%,但上线后用户反馈"答案还是不准"。问题出在哪?可能是模块之间没有协同优化,也可能是评估指标选错了。本文讲透RAG系统的端到端优化。

一、RAG系统的典型架构

用户Query
    ↓
┌─────────────────────────────────────┐
│  检索阶段                            │
│  ┌──────────┐   ┌──────────┐       │
│  │ Query改写 │ → │ 向量检索  │       │
│  └──────────┘   └──────────┘       │
│        ↓              ↓             │
│  ┌──────────┐   ┌──────────┐       │
│  │ 关键词检索│ → │ 混合检索  │       │
│  └──────────┘   └──────────┘       │
└─────────────────────────────────────┘
                ↓
┌─────────────────────────────────────┐
│  后处理阶段                          │
│  ┌──────────┐   ┌──────────┐       │
│  │ Rerank   │ → │ 上下文选择│       │
│  └──────────┘   └──────────┘       │
│                       ↓             │
│                ┌──────────┐        │
│                │ 上下文压缩│        │
│                └──────────┘        │
└─────────────────────────────────────┘
                ↓
┌─────────────────────────────────────┐
│  生成阶段                            │
│  ┌──────────┐   ┌──────────┐       │
│  │ Prompt构建│ → │ LLM生成   │       │
│  └──────────┘   └──────────┘       │
│                       ↓             │
│                ┌──────────┐        │
│                │ 自我检验  │        │
│                └──────────┘        │
└─────────────────────────────────────┘
                ↓
            最终答案

二、端到端评估指标

2.1 检索质量指标

class RetrievalMetrics:
    """检索质量评估"""
    
    @staticmethod
    def recall_at_k(retrieved: list, relevant: set, k: int) -> float:
        """Recall@K:前K个结果中有多少相关文档"""
        retrieved_k = set(retrieved[:k])
        return len(retrieved_k & relevant) / len(relevant) if relevant else 0
    
    @staticmethod
    def precision_at_k(retrieved: list, relevant: set, k: int) -> float:
        """Precision@K:前K个结果中有多少是相关的"""
        retrieved_k = set(retrieved[:k])
        return len(retrieved_k & relevant) / k if k > 0 else 0
    
    @staticmethod
    def mrr(retrieved: list, relevant: set) -> float:
        """MRR:第一个相关文档的排名倒数"""
        for i, doc in enumerate(retrieved, 1):
            if doc in relevant:
                return 1 / i
        return 0
    
    @staticmethod
    def ndcg_at_k(retrieved: list, relevance_scores: dict, k: int) -> float:
        """NDCG@K:考虑排序位置的准确率"""
        dcg = sum(
            relevance_scores.get(doc, 0) / np.log2(i + 1)
            for i, doc in enumerate(retrieved[:k], 1)
        )
        
        ideal_scores = sorted(relevance_scores.values(), reverse=True)[:k]
        idcg = sum(
            score / np.log2(i + 1)
            for i, score in enumerate(ideal_scores, 1)
        )
        
        return dcg / idcg if idcg > 0 else 0

2.2 生成质量指标

class GenerationMetrics:
    """生成质量评估"""
    
    @staticmethod
    def faithfulness(answer: str, context: str, llm) -> float:
        """忠实度:答案是否基于上下文"""
        
        prompt = f"""
        上下文:{context}
        答案:{answer}
        
        请判断答案中的每句话是否都能从上下文中推导出来。
        输出JSON:{{"faithful_sentences": 数量, "total_sentences": 数量}}
        """
        
        response = llm.invoke(prompt)
        result = json.loads(extract_json(response.content))
        
        return result["faithful_sentences"] / result["total_sentences"]
    
    @staticmethod
    def answer_relevance(answer: str, question: str, llm) -> float:
        """答案相关性:答案是否回答了问题"""
        
        prompt = f"""
        问题:{question}
        答案:{answer}
        
        请判断答案与问题的相关程度(0-10分)。
        输出JSON:{{"score": 分数, "reason": "原因"}}
        """
        
        response = llm.invoke(prompt)
        result = json.loads(extract_json(response.content))
        
        return result["score"] / 10
    
    @staticmethod
    def completeness(answer: str, expected_points: list, llm) -> float:
        """完整性:答案是否覆盖了所有要点"""
        
        prompt = f"""
        答案:{answer}
        期望覆盖的要点:{expected_points}
        
        请判断答案覆盖了哪些要点。
        输出JSON:{{"covered": ["要点1", "要点2"]}}
        """
        
        response = llm.invoke(prompt)
        result = json.loads(extract_json(response.content))
        
        return len(result["covered"]) / len(expected_points)

2.3 端到端指标

class E2EMetrics:
    """端到端指标"""
    
    @staticmethod
    def answer_accuracy(answer: str, ground_truth: str, llm) -> float:
        """答案准确性(与标准答案对比)"""
        
        prompt = f"""
        标准答案:{ground_truth}
        实际答案:{answer}
        
        请判断实际答案与标准答案的相似度(0-10分)。
        主要关注:关键信息是否一致、是否有错误信息、是否有遗漏。
        输出JSON:{{"score": 分数, "issues": ["问题列表"]}}
        """
        
        response = llm.invoke(prompt)
        result = json.loads(extract_json(response.content))
        
        return result["score"] / 10
    
    @staticmethod
    def latency(pipeline_func, query: str) -> float:
        """延迟(毫秒)"""
        
        import time
        start = time.time()
        pipeline_func(query)
        return (time.time() - start) * 1000

三、优化策略矩阵

3.1 问题诊断矩阵

症状可能原因优化方向
召回率低检索策略不对Query改写、混合检索
精确率低检索噪音多Rerank、过滤
答案跑题Prompt设计问题明确约束、结构化指令
答案幻觉上下文选择不当相关性过滤、自我检验
信息遗漏上下文压缩过度减少压缩、多样性选择
延迟高流程冗余并行化、缓存

3.2 优化优先级

第一步:诊断瓶颈
    │
    ├── 召回率<70%
    │   → 优先优化检索(Query改写、混合检索)
    │
    ├── 召回率>70%,但答案准确率<60%
    │   → 优先优化Rerank和上下文选择
    │
    ├── 准确率>60%,但答案质量差
    │   → 优化Prompt和生成
    │
    └── 质量达标但延迟高
        → 优化流程并行化、缓存

四、系统优化实战

4.1 检索优化Pipeline

class OptimizedRetrievalPipeline:
    """优化的检索Pipeline"""
    
    def __init__(self, vector_store, bm25, reranker, embedder):
        self.vector_store = vector_store
        self.bm25 = bm25
        self.reranker = reranker
        self.embedder = embedder
    
    def retrieve(self, query: str, k: int = 10) -> list[dict]:
        """多阶段检索"""
        
        # 第一步:Query改写(可选)
        expanded_queries = self._expand_query(query)
        
        # 第二步:混合检索
        all_docs = []
        for q in expanded_queries:
            # 向量检索
            vec_results = self.vector_store.search(q, k=20)
            # BM25检索
            bm25_results = self.bm25.search(q, k=20)
            # 合并
            all_docs.extend(vec_results)
            all_docs.extend(bm25_results)
        
        # 第三步:去重
        unique_docs = self._deduplicate(all_docs)
        
        # 第四步:Rerank
        reranked = self.reranker.rerank(
            query=query,
            documents=[d["content"] for d in unique_docs],
            top_k=k
        )
        
        return reranked
    
    def _expand_query(self, query: str) -> list[str]:
        """Query扩展(简化版)"""
        return [query]  # 不扩展,直接返回原查询
    
    def _deduplicate(self, docs: list[dict]) -> list[dict]:
        """去重"""
        seen = set()
        unique = []
        for doc in docs:
            if doc["id"] not in seen:
                seen.add(doc["id"])
                unique.append(doc)
        return unique

4.2 完整RAG Pipeline

class ProductionRAG:
    """生产级RAG系统"""
    
    def __init__(self, config: dict):
        self.config = config
        
        # 初始化各模块
        self.retriever = OptimizedRetrievalPipeline(
            vector_store=config["vector_store"],
            bm25=config["bm25"],
            reranker=config["reranker"],
            embedder=config["embedder"]
        )
        
        self.context_manager = ContextManager(
            llm=config["llm"],
            embedder=config["embedder"],
            max_tokens=config.get("max_context_tokens", 3000)
        )
        
        self.generator = RAGGenerator(llm=config["llm"])
    
    def query(self, question: str) -> dict:
        """查询入口"""
        
        # 检索
        docs = self.retriever.retrieve(question, k=10)
        
        # 上下文处理
        context = self.context_manager.process(docs, question)
        
        # 生成
        result = self.generator.generate(question, context)
        
        return {
            "answer": result["answer"],
            "sources": docs[:3],
            "validation": result.get("validation"),
            "latency_ms": result.get("latency_ms")
        }
    
    def batch_query(self, questions: list[str]) -> list[dict]:
        """批量查询"""
        
        return [self.query(q) for q in questions]

五、性能优化

5.1 缓存策略

from functools import lru_cache
import hashlib

class CachedRAG:
    """带缓存的RAG"""
    
    def __init__(self, rag: ProductionRAG, cache_size: int = 1000):
        self.rag = rag
        self.cache = {}
        self.cache_size = cache_size
    
    def query(self, question: str) -> dict:
        """带缓存的查询"""
        
        # 缓存Key
        cache_key = hashlib.md5(question.encode()).hexdigest()
        
        # 命中缓存
        if cache_key in self.cache:
            return {**self.cache[cache_key], "from_cache": True}
        
        # 未命中,执行查询
        result = self.rag.query(question)
        result["from_cache"] = False
        
        # 写入缓存
        if len(self.cache) >= self.cache_size:
            # LRU淘汰
            oldest_key = next(iter(self.cache))
            del self.cache[oldest_key]
        
        self.cache[cache_key] = result
        return result

5.2 并行化

import asyncio
from concurrent.futures import ThreadPoolExecutor

class ParallelRAG:
    """并行化RAG"""
    
    def __init__(self, rag: ProductionRAG, max_workers: int = 4):
        self.rag = rag
        self.executor = ThreadPoolExecutor(max_workers=max_workers)
    
    async def batch_query_async(self, questions: list[str]) -> list[dict]:
        """异步批量查询"""
        
        loop = asyncio.get_event_loop()
        tasks = [
            loop.run_in_executor(self.executor, self.rag.query, q)
            for q in questions
        ]
        
        return await asyncio.gather(*tasks)
    
    def batch_query(self, questions: list[str]) -> list[dict]:
        """同步批量查询"""
        
        return list(self.executor.map(self.rag.query, questions))

六、监控与告警

class RAGMonitor:
    """RAG系统监控"""
    
    def __init__(self):
        self.metrics = {
            "total_queries": 0,
            "avg_latency_ms": 0,
            "avg_recall": 0,
            "avg_accuracy": 0,
            "cache_hit_rate": 0,
            "error_rate": 0
        }
        
        self.alerts = []
    
    def record(self, query_result: dict):
        """记录查询结果"""
        
        self.metrics["total_queries"] += 1
        
        # 更新平均值
        n = self.metrics["total_queries"]
        
        latency = query_result.get("latency_ms", 0)
        self.metrics["avg_latency_ms"] = (
            self.metrics["avg_latency_ms"] * (n - 1) + latency
        ) / n
        
        # 检查告警条件
        if latency > 5000:  # 超过5秒
            self.alerts.append({
                "type": "high_latency",
                "value": latency,
                "threshold": 5000,
                "timestamp": datetime.now()
            })
    
    def get_report(self) -> dict:
        """获取监控报告"""
        
        return {
            "metrics": self.metrics,
            "alerts": self.alerts[-10:],  # 最近10条告警
            "health": self._check_health()
        }
    
    def _check_health(self) -> str:
        """健康检查"""
        
        if self.metrics["error_rate"] > 0.1:
            return "unhealthy"
        elif self.metrics["avg_latency_ms"] > 3000:
            return "degraded"
        else:
            return "healthy"

七、总结

优化方向效果实现难度优先级
Query改写+5-10%召回
混合检索+8-12%召回
Rerank+15-20%准确率
Prompt优化+10-15%质量
缓存-50%延迟
并行化-30%延迟

RAG系统优化是持续迭代的过程,建议从检索→后处理→生成逐步优化。

下篇预告:「LangGraph深度实战:用状态图构建复杂Agent工作流」——从简单RAG到多步推理Agent的架构升级。


需要完整RAG Pipeline代码的同学,可以看我主页的付费资源专栏。

有问题欢迎评论区留言,大家一起讨论!

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐