知识库的边界问题
前面几篇优化了检索质量:更好的分块、更精准的排序、更聪明的问法。但有一个根本性的问题一直被回避:
如果知识库本来就没有这个问题的答案呢?
用户问的问题超出了知识库的覆盖范围,向量检索依然会返回"最相似"的几篇文档——但这些文档和问题的真实答案毫无关系。LLM 拿到这些文档,要么生成一个基于无关内容的幻觉答案,要么说"根据参考资料无法回答"——两种结果都不理想。
这是传统 RAG 的盲点:它从不质疑检索结果的质量,只是无条件地把文档塞给 LLM。
2024 年提出的 CRAG(Corrective RAG) 在这里加了一个"纠偏"步骤:先评估检索结果的质量,不合格时主动触发网络搜索作为补充或替代,而不是将就着用低质量的文档生成答案。
CRAG 的核心流程
用户问题
↓
向量检索(知识库)
↓
相关性评分:对每篇文档打 0~1 的相关性分数
↓
综合判断(三种 verdict)
├─ CORRECT(≥0.7) → 直接使用知识库文档
├─ INCORRECT(≤0.3)→ 丢弃知识库,触发网络搜索
└─ AMBIGUOUS(中间) → 知识库文档 + 网络搜索结果合并
↓
(网络搜索结果经 LLM 精炼,提取关键信息)
↓
基于最终文档生成答案
和 Self-RAG 的区别在于:
- Self-RAG 解决"要不要检索"——在查询前决策
- CRAG 解决"检索结果够不够好"——在检索后评估,不好就纠偏
用 LangGraph 实现
State 设计
class CRAGState(TypedDict):
question: str
retrieved_docs: list[Document]
doc_scores: list[float] # 每篇文档的相关性分数
overall_score: float # 综合评分
retrieval_verdict: str # "correct" | "ambiguous" | "incorrect"
web_results: str # 网络搜索原始结果
refined_web_docs: list[Document] # LLM 精炼后的网络文档
final_docs: list[Document] # 最终送入 LLM 的文档
answer: str
path: list[str]
关键节点:相关性评分(score)
这是 CRAG 的核心节点——对每篇检索文档逐一打分:
RELEVANCE_SCORE_PROMPT = ChatPromptTemplate.from_messages([
("system",
"给出一个 0.0 到 1.0 之间的分数,表示文档与问题的相关程度。\n"
"- 1.0:文档直接、完整地回答了问题\n"
"- 0.5:文档部分相关,但不完整\n"
"- 0.0:文档与问题完全无关\n\n"
"只输出一个浮点数,不要任何解释。"),
("human", "问题:{question}\n\n文档:{document}"),
])
def make_score_node(llm):
chain = RELEVANCE_SCORE_PROMPT | llm | StrOutputParser()
def score_docs(state):
scores = []
for doc in state["retrieved_docs"]:
raw = chain.invoke({
"question": state["question"],
"document": doc.page_content[:400],
})
score = float(raw.strip())
scores.append(max(0.0, min(1.0, score)))
overall = sum(scores) / len(scores)
if overall >= 0.7:
verdict = "correct"
elif overall <= 0.3:
verdict = "incorrect"
else:
verdict = "ambiguous"
return {**state, "doc_scores": scores, "overall_score": overall,
"retrieval_verdict": verdict}
return score_docs
关键节点:网络搜索 + 精炼(web_search)
搜索结果往往是噪声,直接喂给 LLM 效果差。CRAG 在这里加了一步 LLM 精炼:
REFINE_PROMPT = ChatPromptTemplate.from_messages([
("system",
"从以下网络搜索结果中,提取与问题最相关的关键信息,"
"整理成简洁的参考资料。去除无关内容,保留核心事实。"),
("human", "问题:{question}\n\n搜索结果:\n{search_results}\n\n请提取关键信息:"),
])
def make_web_search_node(search_tool, llm):
refine_chain = REFINE_PROMPT | llm | StrOutputParser()
def web_search(state):
try:
raw_results = search_tool.invoke(state["question"])
refined = refine_chain.invoke({
"question": state["question"],
"search_results": raw_results[:2000],
})
web_doc = Document(page_content=refined,
metadata={"source": "web_search"})
return {**state, "refined_web_docs": [web_doc]}
except Exception:
# 网络不可用时回退到知识库文档
return {**state, "refined_web_docs": []}
return web_search
关键节点:文档组装(assemble)
根据 verdict 决定最终使用哪些文档:
def make_assemble_node():
def assemble(state):
verdict = state["retrieval_verdict"]
if verdict == "correct":
# 只用知识库高分文档
scored = sorted(zip(state["retrieved_docs"], state["doc_scores"]),
key=lambda x: x[1], reverse=True)
final = [doc for doc, score in scored if score >= 0.3] or [scored[0][0]]
elif verdict == "incorrect":
# 优先用网络搜索;不可用时回退到知识库最优文档
final = state.get("refined_web_docs", [])
if not final:
scored = sorted(zip(state["retrieved_docs"], state["doc_scores"]),
key=lambda x: x[1], reverse=True)
final = [scored[0][0]]
else: # ambiguous
# 知识库高分文档 + 网络搜索结果合并
scored = zip(state["retrieved_docs"], state["doc_scores"])
kb_docs = [doc for doc, score in scored if score >= 0.3]
final = kb_docs + state.get("refined_web_docs", [])
return {**state, "final_docs": final}
return assemble
Graph 结构
graph.set_entry_point("retrieve")
graph.add_edge("retrieve", "score")
graph.add_conditional_edges(
"score",
lambda s: "web_search" if s["retrieval_verdict"] != "correct" else "assemble",
{"web_search": "web_search", "assemble": "assemble"},
)
graph.add_edge("web_search", "assemble")
graph.add_edge("assemble", "generate")
graph.add_edge("generate", END)
实验结果
执行路径明细
CRAG 执行路径明细:
Q1: retrieve → score(ambiguous, 0.62) → web_search(ok) → assemble(5docs) → generate
"什么是 RAG 技术,它主要解决什么问题?"
Q2: retrieve → score(incorrect, 0.12) → web_search(ok) → assemble(1docs) → generate
"企业级应用应该选择哪种向量数据库?"
Q3: retrieve → score(ambiguous, 0.45) → web_search(ok) → assemble(4docs) → generate
"中文场景应该选择哪个 Embedding 模型?"
Q4: retrieve → score(incorrect, 0.20) → web_search(ok) → assemble(1docs) → generate
"文档分块时 Chunk Size 一般推荐多少?"
Q5: retrieve → score(ambiguous, 0.38) → web_search(ok) → assemble(3docs) → generate
"RAGAS 框架包含哪四个核心评估指标?"
Q6: retrieve → score(incorrect, 0.00) → web_search(ok) → assemble(1docs) → generate
"RRF 融合算法的公式是什么?"
Q7: retrieve → score(incorrect, 0.17) → web_search(ok) → assemble(1docs) → generate
"HyDE 查询优化技术的原理是什么?"
Q8: retrieve → score(ambiguous, 0.33) → web_search(ok) → assemble(3docs) → generate
"生产级 RAG 系统如何实现多租户隔离?"
相关性评分分布:correct=0,ambiguous=4,incorrect=4
8 条问题中,没有一条被评为 correct:4 条 ambiguous,4 条 incorrect。说明评分模型对"知识库文档能否直接回答问题"的判断相当严格。
注意 Q6(RRF 融合算法的公式,分数 0.00)和 Q7(HyDE 的原理,分数 0.17)——这两个是前面几篇文章才讲到的新概念,知识库里确实没有对应内容,评分器正确识别并触发了网络搜索。
RAGAS 指标对比
======================================================================
RAGAS 指标对比(固定检索 vs CRAG)
======================================================================
指标 固定检索 CRAG 变化
──────────────────────────────────────────────────────
context_recall 0.625 0.625 →+0.000
context_precision 0.444 0.875 ↑+0.431 ◀
faithfulness 0.810 0.907 ↑+0.097
answer_relevancy 0.402 0.368 ↓-0.033
======================================================================
context_precision +0.431,这是本系列所有实验中单项提升最大的一次。
为什么提升这么大?
context_precision 衡量的是:相关文档是否排在不相关文档前面。
固定检索把所有 top-4 文档平等地塞给 LLM,分数高低不论。CRAG 的 score 节点给每篇文档打了精确的相关性分,assemble 节点按分数过滤和排序,最终送给 LLM 的文档集合更干净、更有针对性。
更关键的是:对 incorrect 的问题,CRAG 完全放弃了低质量的知识库文档,改用网络搜索的精炼结果。这些结果直接命中问题,context_precision 自然极高。
固定检索的 context_precision 只有 0.444(连 0.5 都不到),意味着许多问题里,相关文档甚至被排在了不相关文档后面。CRAG 的评分+过滤机制彻底扭转了这个局面。
与 Self-RAG 的核心差异
| 维度 | Self-RAG | CRAG |
|---|---|---|
| 决策时机 | 检索前(要不要检索?) | 检索后(结果够不够好?) |
| 核心问题 | 避免不必要的检索 | 低质量检索结果的纠偏 |
| 兜底机制 | 直接生成(无检索) | 网络搜索(外部知识源) |
| 适用场景 | 混合型对话,部分问题不需要检索 | 知识库覆盖有限,问题可能超出范围 |
| 关键节点 | decide → route | score → assemble |
两者不互斥,可以组合:Self-RAG 先判断要不要检索,检索后 CRAG 再判断结果够不够好。
适用场景与注意事项
CRAG 最适合的场景:
- 知识库覆盖有限,用户问题可能超出范围(本实验典型案例)
- 知识库内容更新较慢,部分问题需要最新的网络信息
- 对答案质量要求高,宁可触发网络搜索也不将就
需要注意的问题:
- 评分模型的校准:评分阈值(0.7 / 0.3)需要根据具体知识库调整。本实验阈值偏严,导致 0 条 correct,实际部署时可能需要放宽
- 网络搜索的可靠性:网络搜索结果质量参差不齐,精炼步骤很重要;网络不可用时需要优雅降级
- 成本上升:每次查询多出评分(4 次 LLM 调用)+ 可能的网络搜索 + 精炼,token 消耗显著增加
完整代码
代码已开源:
核心文件:
crag.py— LangGraph 实现的完整 CRAG 流程
运行方式:
git clone https://github.com/chendongqi/llm-in-action
cd 15-crag
cp .env.example .env
pip install -r requirements.txt
python crag.py
小结
本文用 LangGraph 实现了 CRAG,核心发现:
- 评分驱动的文档过滤是 context_precision 大幅提升(+0.431)的根本原因——给文档打分、过滤、按分排序,远比盲目使用 top-k 更有效
- 网络搜索 fallback 解决了知识库覆盖盲区,让系统在不熟悉的话题上也能给出有据可查的答案
- CRAG 与 Self-RAG 互补:前者解决"要不要检索",后者解决"检索结果够不够好"
从实验数据来看,评分+过滤这个机制本身就值得单独引入——即便不接入网络搜索,只是把 CRAG 的 score 节点嫁接到普通 RAG 流程上,context_precision 就会有显著提升。