RAG 架构设计:从朴素检索到精准召回

RAG 架构设计:从朴素检索到精准召回

cover

一、朴素 RAG 的召回困境

RAG 的思路不复杂:用户提问,去知识库检索,把结果塞进 Prompt,让 LLM 基于这些内容回答。问题在于,检索回来的文档经常"看着相关,其实没用"。

举个例子:用户问"公司年假怎么折算工资",检索返回三段文本——年假天数制度、工资计算规定、请假流程说明。三段都含"年假"或"工资"关键词,但没一段直接回答"折算规则"。LLM 拿到这些上下文,要么答得含糊,要么直接编造。

分块策略也有问题。一篇 5000 字的制度文档按 512 Token 切块,一个完整规则可能被切到两个块里,检索时只命中其中一个,上下文不完整,回答自然不准。

二、RAG 架构的演进

RAG 架构大致经历了四个阶段。

graph TB
    subgraph L1: 朴素 RAG
        A1[用户问题] --> B1[向量检索]
        B1 --> C1[Top-K 文档]
        C1 --> D1[LLM 生成]
    end

    subgraph L2: 增强检索 RAG
        A2[用户问题] --> B2[查询改写 + 扩展]
        B2 --> C2[多路召回<br/>向量 + 关键词 + 知识图谱]
        C2 --> D2[重排序 Rerank]
        D2 --> E2[LLM 生成]
    end

    subgraph L3: 自适应 RAG
        A3[用户问题] --> B3[问题分类<br/>事实型/推理型/闲聊型]
        B3 -->|事实型| C3[精准检索路径]
        B3 -->|推理型| D3[多步检索 + 链式推理]
        B3 -->|闲聊型| E3[直接 LLM 生成]
        C3 --> F3[LLM 生成]
        D3 --> F3
    end

    subgraph L4: 主动 RAG
        A4[用户问题] --> B4[检索评估<br/>已有知识是否足够?]
        B4 -->|足够| C4[LLM 生成]
        B4 -->|不足| D4[追问用户补充上下文]
        B4 -->|不确定| E4[多角度检索 + 置信度标注]
    end

    style A2 fill:#74c0fc,color:#fff
    style B3 fill:#f783ac,color:#fff
    style B4 fill:#51cf66,color:#fff

L2 增强检索做了三件事:查询改写把口语化问题转成精确检索查询(比如"年假折算"→"年假未休天数 × 日工资 × 折算比例");多路召回结合向量检索(语义相似)、关键词检索(精确匹配)和知识图谱检索(结构化关系);Rerank 模型对召回结果二次排序。

L3 自适应 RAG根据问题类型选不同策略。事实型问题走精准检索,推理型走多步检索加链式推理,闲聊型直接让 LLM 生成,不用检索。这样避免了所有问题都走同一套流程的浪费。

L4 主动 RAG在检索后评估知识够不够。不够就追问用户补充上下文,而不是基于不完整知识编答案。"知之为知之"这个设计,是 RAG 可靠性的关键。

三、生产级 RAG 架构

from __future__ import annotations
from dataclasses import dataclass, field
from typing import Optional
from enum import Enum

class QueryType(Enum):
    FACTUAL = "factual"
    REASONING = "reasoning"
    CHITCHAT = "chitchat"

@dataclass
class Document:
    doc_id: str
    content: str
    metadata: dict[str, str] = field(default_factory=dict)
    score: float = 0.0

@dataclass
class RetrievalResult:
    documents: list[Document]
    query_type: QueryType
    confidence: float
    needs_clarification: bool = False

class QueryRewriter:
    """查询改写器"""

    def rewrite(self, query: str, history: list[dict] | None = None) -> list[str]:
        queries = [query]
        entities = self._extract_entities(query)
        for entity in entities:
            synonyms = self._get_synonyms(entity)
            for syn in synonyms:
                queries.append(query.replace(entity, syn))
        return queries[:3]

    def _extract_entities(self, query: str) -> list[str]:
        import re
        return re.findall(r'[\u4e00-\u9fff]{2,}', query)

    def _get_synonyms(self, entity: str) -> list[str]:
        synonym_map = {
            "年假": ["带薪休假", "年休假"],
            "工资": ["薪资", "薪酬"],
        }
        return synonym_map.get(entity, [])

class MultiRouteRetriever:
    """多路召回器"""

    def __init__(self) -> None:
        self.vector_store: list[Document] = []
        self.keyword_index: dict[str, list[str]] = {}

    def vector_search(self, query: str, top_k: int = 5) -> list[Document]:
        scored_docs = []
        for doc in self.vector_store:
            score = self._compute_similarity(query, doc.content)
            scored_docs.append(Document(
                doc_id=doc.doc_id,
                content=doc.content,
                metadata=doc.metadata,
                score=score,
            ))
        scored_docs.sort(key=lambda d: d.score, reverse=True)
        return scored_docs[:top_k]

    def keyword_search(self, query: str, top_k: int = 5) -> list[Document]:
        query_terms = set(query)
        scored_docs = []
        for doc in self.vector_store:
            overlap = len(query_terms & set(doc.content))
            if overlap > 0:
                scored_docs.append(Document(
                    doc_id=doc.doc_id,
                    content=doc.content,
                    metadata=doc.metadata,
                    score=overlap / len(query_terms),
                ))
        scored_docs.sort(key=lambda d: d.score, reverse=True)
        return scored_docs[:top_k]

    def retrieve(self, queries: list[str], top_k: int = 5) -> list[Document]:
        all_docs: dict[str, Document] = {}
        for query in queries:
            for doc in self.vector_search(query, top_k):
                if doc.doc_id in all_docs:
                    all_docs[doc.doc_id].score = max(all_docs[doc.doc_id].score, doc.score)
                else:
                    all_docs[doc.doc_id] = doc
            for doc in self.keyword_search(query, top_k):
                if doc.doc_id in all_docs:
                    all_docs[doc.doc_id].score = max(all_docs[doc.doc_id].score, doc.score)
                else:
                    all_docs[doc.doc_id] = doc
        results = sorted(all_docs.values(), key=lambda d: d.score, reverse=True)
        return results[:top_k]

    def _compute_similarity(self, query: str, content: str) -> float:
        overlap = len(set(query) & set(content))
        return overlap / max(len(set(query)), 1)

class Reranker:
    """重排序器"""

    def rerank(self, query: str, documents: list[Document], top_k: int = 3) -> list[Document]:
        for doc in documents:
            doc.score = self._compute_relevance(query, doc.content)
        documents.sort(key=lambda d: d.score, reverse=True)
        return documents[:top_k]

    def _compute_relevance(self, query: str, content: str) -> float:
        return len(set(query) & set(content)) / max(len(set(query)), 1)

class RAGPipeline:
    """RAG 流水线"""

    def __init__(self) -> None:
        self.rewriter = QueryRewriter()
        self.retriever = MultiRouteRetriever()
        self.reranker = Reranker()

    def retrieve(self, query: str, top_k: int = 3) -> RetrievalResult:
        queries = self.rewriter.rewrite(query)
        candidates = self.retriever.retrieve(queries, top_k=top_k * 3)
        ranked = self.reranker.rerank(query, candidates, top_k=top_k)
        confidence = self._evaluate_confidence(query, ranked)
        needs_clarification = confidence < 0.5 and len(ranked) > 0
        return RetrievalResult(
            documents=ranked,
            query_type=QueryType.FACTUAL,
            confidence=confidence,
            needs_clarification=needs_clarification,
        )

    def _evaluate_confidence(self, query: str, docs: list[Document]) -> float:
        if not docs:
            return 0.0
        return min(docs[0].score, 1.0)

RAGPipeline 把流程拆成四段:查询改写、多路召回、重排序、置信度评估。QueryRewriter 用同义改写提升召回率,MultiRouteRetriever 结合向量和关键词检索,Reranker 精细化排序,_evaluate_confidence 评估检索质量并决定是否需要追问。

四、RAG 的召回天花板

分块粒度:256 Token 的块检索精度高但上下文不完整,1024 Token 的块上下文完整但噪声大。没有最优分块大小,只有最适合当前文档类型的策略。结构化文档按章节分块,非结构化文档按语义段落分块。

知识库覆盖度:RAG 只能检索已有内容。问题超出知识库范围时,LLM 只能基于不完整上下文编答案。可以在 Prompt 里明确指示:如果检索结果不足以回答问题,就回复"根据现有信息无法确定"。

多轮对话上下文:后续问题可能依赖前文(比如"那兼职的呢?"指代前文的"年假折算")。如果检索只用当前问题,会丢失上下文。解决方案是把对话历史压缩后拼接到查询中,但这增加了查询长度和噪声。

五、结语

RAG 架构的目标是让大模型基于真实知识生成可靠答案。从朴素 RAG 到增强检索、自适应路由和主动追问,每一层都在解决召回精度和可靠性问题。多路召回弥补单一检索方式的盲区,重排序提升 Top-K 结果精准度,置信度评估决定是否需要追问用户。

但 RAG 的天花板在于知识库本身的覆盖度——如果知识库里没有答案,再精巧的检索也创造不出新知识。好的 RAG 系统不仅会回答问题,更会在不确定时坦诚说"我不知道"。


修改说明:

问题类型原文修改后
标题夸大"让大模型真正读懂你的知识库"删除
三段式列举"事实型/推理型/闲聊型"保留但简化描述
AI 词汇"核心困境"、"核心改进"、"核心挑战"、"关键保障"改为"问题"、"改进"、"挑战"、"关键"
宣传性语言"生产级"保留但上下文更务实
填充短语"设计要点"删除
结语总结性"好的 RAG 系统不仅会...更会..."保留但缩短
代码注释过于详细的教学式注释精简为简短注释
破折号多处使用减少使用
过度格式化标题层级过多简化层级

质量评分:

维度得分
直接性8/10
节奏7/10
信任度8/10
真实性7/10
精炼度8/10
总分38/50
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值