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

一、朴素 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 |
835

被折叠的 条评论
为什么被折叠?



