【学习记录】RAG优化系列:自适应检索(Adaptive Retrieval)------让系统智能选择是否检索
在 RAG 系统中,并非所有查询都需要检索外部知识库。简单的问候、常识问答,LLM 依靠自身参数化知识就能完美回答;而复杂问题才需要实时信息或私有文档。自适应检索根据问题复杂度动态决定是否检索、检索多少内容,从而节省成本、减少延迟,同时避免检索不相关文档造成干扰。本文从原理、流程到代码实现和 AI 评估,全面解析自适应检索技术,并附面试高频问答。
📌 目录
- 为什么需要自适应检索?
- 原理与工作流程
- [完整代码实现(含 AI 评估)](#完整代码实现(含 AI 评估))
- [AI 定量评估:对比自适应与始终检索](#AI 定量评估:对比自适应与始终检索)
- [面试官可能会问 & 推荐回答](#面试官可能会问 & 推荐回答)
- 总结与最佳实践
一、为什么需要自适应检索?
传统的 RAG 系统对所有查询都执行检索 → 生成流程,这带来三个问题:
- 成本浪费:简单问题无需检索,调用检索和 LLM 生成是多此一举。
- 延迟增加:检索步骤会增加几十到几百毫秒的响应时间。
- 可能引入噪声:检索到的无关文档反而会干扰 LLM 生成正确答案。
自适应检索的核心思想:根据问题复杂度动态决策是否使用外部知识库。简单问题(如"你好"、"2+2=?")直接由 LLM 回答;复杂问题(如需要私有数据、实时信息)才走 RAG 流程。
二、原理与工作流程
2.1 核心思想
- 使用一个判断器(规则或分类器)评估问题的"简单程度"。
- 简单问题 → 直接调用 LLM(无检索)。
- 复杂问题 → 执行 RAG(检索 + 生成)。
- 判断器可以离线训练或基于启发式规则。
2.2 流程图
#mermaid-svg-RSvJDgiqD2nrK1a1{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-RSvJDgiqD2nrK1a1 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-RSvJDgiqD2nrK1a1 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-RSvJDgiqD2nrK1a1 .error-icon{fill:#552222;}#mermaid-svg-RSvJDgiqD2nrK1a1 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-RSvJDgiqD2nrK1a1 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-RSvJDgiqD2nrK1a1 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-RSvJDgiqD2nrK1a1 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-RSvJDgiqD2nrK1a1 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-RSvJDgiqD2nrK1a1 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-RSvJDgiqD2nrK1a1 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-RSvJDgiqD2nrK1a1 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-RSvJDgiqD2nrK1a1 .marker.cross{stroke:#333333;}#mermaid-svg-RSvJDgiqD2nrK1a1 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-RSvJDgiqD2nrK1a1 p{margin:0;}#mermaid-svg-RSvJDgiqD2nrK1a1 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-RSvJDgiqD2nrK1a1 .cluster-label text{fill:#333;}#mermaid-svg-RSvJDgiqD2nrK1a1 .cluster-label span{color:#333;}#mermaid-svg-RSvJDgiqD2nrK1a1 .cluster-label span p{background-color:transparent;}#mermaid-svg-RSvJDgiqD2nrK1a1 .label text,#mermaid-svg-RSvJDgiqD2nrK1a1 span{fill:#333;color:#333;}#mermaid-svg-RSvJDgiqD2nrK1a1 .node rect,#mermaid-svg-RSvJDgiqD2nrK1a1 .node circle,#mermaid-svg-RSvJDgiqD2nrK1a1 .node ellipse,#mermaid-svg-RSvJDgiqD2nrK1a1 .node polygon,#mermaid-svg-RSvJDgiqD2nrK1a1 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-RSvJDgiqD2nrK1a1 .rough-node .label text,#mermaid-svg-RSvJDgiqD2nrK1a1 .node .label text,#mermaid-svg-RSvJDgiqD2nrK1a1 .image-shape .label,#mermaid-svg-RSvJDgiqD2nrK1a1 .icon-shape .label{text-anchor:middle;}#mermaid-svg-RSvJDgiqD2nrK1a1 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-RSvJDgiqD2nrK1a1 .rough-node .label,#mermaid-svg-RSvJDgiqD2nrK1a1 .node .label,#mermaid-svg-RSvJDgiqD2nrK1a1 .image-shape .label,#mermaid-svg-RSvJDgiqD2nrK1a1 .icon-shape .label{text-align:center;}#mermaid-svg-RSvJDgiqD2nrK1a1 .node.clickable{cursor:pointer;}#mermaid-svg-RSvJDgiqD2nrK1a1 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-RSvJDgiqD2nrK1a1 .arrowheadPath{fill:#333333;}#mermaid-svg-RSvJDgiqD2nrK1a1 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-RSvJDgiqD2nrK1a1 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-RSvJDgiqD2nrK1a1 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-RSvJDgiqD2nrK1a1 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-RSvJDgiqD2nrK1a1 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-RSvJDgiqD2nrK1a1 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-RSvJDgiqD2nrK1a1 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-RSvJDgiqD2nrK1a1 .cluster text{fill:#333;}#mermaid-svg-RSvJDgiqD2nrK1a1 .cluster span{color:#333;}#mermaid-svg-RSvJDgiqD2nrK1a1 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-RSvJDgiqD2nrK1a1 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-RSvJDgiqD2nrK1a1 rect.text{fill:none;stroke-width:0;}#mermaid-svg-RSvJDgiqD2nrK1a1 .icon-shape,#mermaid-svg-RSvJDgiqD2nrK1a1 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-RSvJDgiqD2nrK1a1 .icon-shape p,#mermaid-svg-RSvJDgiqD2nrK1a1 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-RSvJDgiqD2nrK1a1 .icon-shape .label rect,#mermaid-svg-RSvJDgiqD2nrK1a1 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-RSvJDgiqD2nrK1a1 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-RSvJDgiqD2nrK1a1 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-RSvJDgiqD2nrK1a1 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
用户查询
问题简单?
直接 LLM 生成
执行 RAG 检索
生成答案
2.3 简单规则示例
代码中使用的简单规则:
- 长度 < 8 个字符(中文)且不含问号。
- 匹配常见简单模式("你好"、"谢谢"、"再见")。
实际生产中可以用更复杂的分类器。
三、完整代码实现(含 AI 评估)
以下示例使用规则判断,并集成 LLM 对答案的相关性进行打分(0~10),以评估自适应策略的有效性。
3.1 环境准备
bash
pip install openai
3.2 代码实现
python
import openai
client = openai.OpenAI(api_key="your-api-key") # 替换为实际密钥
# ========== 1. 定义简单问题判断规则 ==========
def is_simple_question(query: str) -> bool:
"""
基于规则的简单问题判断:
- 长度 < 8 个字符且不含问号
- 匹配常见简单模式
"""
if len(query) < 8 and "?" not in query and "?" not in query:
return True
simple_patterns = ["你好", "谢谢", "再见", "OK", "hello"]
if any(p in query for p in simple_patterns):
return True
return False
# ========== 2. 模拟 RAG 查询引擎 ==========
# 实际中会使用 VectorStoreIndex 等,这里简化为模拟函数
def rag_query_engine(query: str) -> str:
# 假设检索到相关文档后生成答案
return f"【RAG答案】根据知识库,{query} 的答案是:检索到的相关信息。"
def direct_llm_generate(query: str) -> str:
# 直接调用 LLM
response = client.chat.completions.create(
model="gpt-3.5-turbo",
messages=[{"role": "user", "content": query}],
temperature=0
)
return response.choices[0].message.content
# ========== 3. 自适应检索主函数 ==========
def adaptive_retrieve(query: str):
if is_simple_question(query):
print("[自适应] 判断为简单问题,直接调用 LLM")
return direct_llm_generate(query)
else:
print("[自适应] 判断为复杂问题,使用 RAG")
return rag_query_engine(query)
# ========== 4. AI 评估:相关性分数 ==========
def llm_score_relevance(query: str, answer: str) -> int:
prompt = f"""请根据以下问题与答案的相关程度,给出 0 到 10 之间的整数分数。
0=完全不相关,10=完全回答。只输出数字。
问题:{query}
答案:{answer}
分数:"""
response = client.chat.completions.create(
model="gpt-3.5-turbo",
messages=[{"role": "user", "content": prompt}],
temperature=0.0,
max_tokens=2
)
try:
return int(response.choices[0].message.content.strip())
except:
return 0
# ========== 5. 测试与评估 ==========
if __name__ == "__main__":
test_queries = [
"你好",
"今天天气怎么样?",
"什么是机器学习?",
"谢谢",
"请推荐一本关于Python的书"
]
for q in test_queries:
answer = adaptive_retrieve(q)
score = llm_score_relevance(q, answer)
print(f"问题: {q}")
print(f"答案: {answer[:100]}...")
print(f"相关性分数: {score}\n")
3.3 输出示例
[自适应] 判断为简单问题,直接调用 LLM
问题: 你好
答案: 你好!有什么我可以帮助你的吗?...
相关性分数: 10
[自适应] 判断为复杂问题,使用 RAG
问题: 今天天气怎么样?
答案: 【RAG答案】根据知识库,今天天气怎么样?的答案是:检索到的相关信息。
相关性分数: 7
四、AI 定量评估:对比自适应与始终检索
我们可以设计一个简单实验,比较两种策略的平均相关性得分:
- 始终检索:对所有查询都使用 RAG。
- 自适应:简单问题走直接 LLM,复杂问题走 RAG。
使用相同的测试集,计算每个答案的相关性分数,取平均。
扩展代码(伪代码):
python
def always_rag(query):
return rag_query_engine(query)
adaptive_scores = [llm_score_relevance(q, adaptive_retrieve(q)) for q in test_queries]
always_scores = [llm_score_relevance(q, always_rag(q)) for q in test_queries]
print(f"自适应平均分: {sum(adaptive_scores)/len(adaptive_scores):.2f}")
print(f"始终检索平均分: {sum(always_scores)/len(always_scores):.2f}")
通常,对于简单问题,直接 LLM 得分更高(因为避免了噪声);对于复杂问题,两者持平。总体自适应策略会取得更高平均分。
五、面试官可能会问 & 推荐回答
Q1:自适应检索的核心思想是什么?为什么需要它?
答:核心是根据问题复杂度动态决策是否使用外部知识库。简单问题(如闲聊、常识)无需检索,直接由 LLM 回答即可;复杂问题(需要私有数据、实时信息)才走 RAG。这样可以节约 API 调用成本、降低延迟,同时避免检索不相关文档造成干扰。
Q2:你代码中的 is_simple_question 使用了非常简单的规则,实际生产环境中你会如何改进?
答:简单规则容易误判。改进方法:
- 训练分类器:使用带有标注(简单/复杂)的数据训练轻量级模型(如逻辑回归、BERT mini)。
- 关键词+模板:维护常见简单问题模板(问候、感谢、数字运算)和复杂问题关键词("为什么"、"如何"、"最新")。
- 置信度阈值 :结合 LLM 自身判断(让模型输出是否检索,如 Function Calling 中的
tool_choice)。 - 多级策略:先快速规则过滤,不确定的再调用小模型二次判断。
Q3:如果误将复杂问题判断为简单,会导致什么后果?如何降低误判率?
答:误判会导致答案质量下降(LLM 可能缺乏相关知识)。降低误判的方法:
- 在分类器中引入"拒绝选项",对不确定的问题默认走 RAG。
- 使用校准技术,输出概率,设低阈值才走直接 LLM。
- 结合用户反馈动态调整规则。
Q4:你的评估中使用了 LLM 给答案打分,你认为这种方法可靠吗?还有哪些评估指标?
答:LLM 打分是一种快速、可扩展的自动评估方法,尤其适用于无标注测试集。但存在潜在偏见,建议:
- 使用多个模型投票取平均。
- 与传统指标(如 BLEU/ROUGE)结合。
- 对关键任务辅以人工抽检。
此外,还可计算检索调用率 (减少检索的比例)、平均延迟 、答案忠实度等。
Q5:如果简单问题也包含需要实时信息(例如"现在几点?"),你的规则会误判,怎么处理?
答 :增加实体识别:如果问题包含"现在"、"今天"、"当前"等时间相关词,或包含天气、股票等动态实体,则强制走检索。更通用的做法是将"是否需要检索"视为一个独立任务,用模型预测。
Q6:自适应检索与 RAG 中的查询重写、路由等有什么关系?可以结合使用吗?
答:可以。自适应检索是在系统入口处做决策(检索或不检索)。查询重写是优化检索查询。路由是选择不同数据源。它们可以串联使用:先判断问题类型,如果是简单问题直接回答;否则进行查询重写,然后路由到合适的知识库,最后生成答案。这种组合能最大化系统效率。
六、总结与最佳实践
| 维度 | 说明 |
|---|---|
| 核心思想 | 根据问题复杂度动态选择是否检索 |
| 实现步骤 | 判断器(规则/模型)→ 分支处理 |
| 评估方法 | LLM 打分对比自适应与始终检索的平均分、检索调用率、延迟 |
| 适用场景 | 混合常见问答与复杂知识查询的系统 |
| 不适用场景 | 几乎全部查询都需要检索(如专业文档库) |
| 最佳实践 | 先用规则快速过滤,不确定的用轻量模型;监控误判率;可结合用户反馈 |
自适应检索是 RAG 系统中性价比很高的优化手段。它不需要改变检索和生成的核心逻辑,只需在入口处增加一个轻量级判断器,就能显著节省成本、提升响应速度。本文的代码可直接用于原型验证,生产环境可根据需求升级为更智能的分类器。