RAG 全链路问题分类
RAG 系统由检索和生成两大环节组成,问题往往难以定位是检索还是生成的锅。先看常见问题清单:
| 问题现象 | 可能根因 | 所在环节 |
|---|---|---|
| 回答"不知道" | 检索未命中相关文档 | 检索 |
| 回答包含错误事实 | 检索到无关文档 / 模型幻觉 | 检索+生成 |
| 回答缺少关键信息 | 文档分块不当 / 检索 top_k 太小 | 检索 |
| 回答自相矛盾 | 检索到冲突文档 / 模型推理错误 | 检索+生成 |
| 回答过于笼统 | Prompt 未约束细节 / 检索文档质量差 | 生成+数据 |
| 延迟过高 | 检索过多文档 / 模型 token 过多 | 全链路 |
调试工具链
1. 请求追踪中间件
import json, time, uuid
from dataclasses import dataclass, asdict
@dataclass
class RAGTrace:
trace_id: str
query: str
# 检索阶段
query_embedded: list = None
retrieval_raw: list = None # 原始检索结果
retrieval_reranked: list = None # 重排后结果
retrieval_filtered: list = None # 过滤后结果
retrieval_latency_ms: float = 0
# 生成阶段
prompt_assembled: str = None
model_used: str = None
generation_latency_ms: float = 0
# 结果
response: str = None
input_tokens: int = 0
output_tokens: int = 0
class RAGTracer:
def __init__(self, sink=None):
self.sink = sink # elasticsearch / file / stdout
def start(self, query) -> RAGTrace:
return RAGTrace(trace_id=str(uuid.uuid4()), query=query)
async def end(self, trace: RAGTrace):
if self.sink:
await self.sink.write(json.dumps(asdict(trace), default=str))
# 使用方式
tracer = RAGTracer()
async def rag_pipeline(query):
trace = tracer.start(query)
# 检索
t0 = time.time()
results = await vector_db.search(embed(query), top_k=10)
trace.retrieval_raw = [{"id": r.id, "score": r.score, "text": r.text[:200]} for r in results]
trace.retrieval_latency_ms = (time.time() - t0) * 1000
# 生成
t0 = time.time()
response = await llm.complete(query, context=results[:5])
trace.generation_latency_ms = (time.time() - t0) * 1000
trace.response = response
await tracer.end(trace)
return response
2. 检索质量分析器
class RetrievalAnalyzer:
"""分析检索结果的质量"""
def analyze(self, query, results, ground_truth_ids=None):
report = {
"query": query,
"total_results": len(results),
"score_distribution": self._score_stats(results),
"score_gap": self._score_gap(results),
"potential_issues": [],
}
# 检查1: 结果太少
if len(results) < 3:
report["potential_issues"].append("too_few_results")
# 检查2: 分数过低
avg_score = sum(r["score"] for r in results) / len(results)
if avg_score < 0.5:
report["potential_issues"].append("low_similarity_scores")
# 检查3: 分数无区分度
if report["score_gap"] < 0.05:
report["potential_issues"].append("no_score_discrimination")
# 检查4: 有标注数据时计算 Recall@K
if ground_truth_ids:
hit = sum(1 for r in results if r["id"] in ground_truth_ids)
report["recall_at_k"] = hit / len(ground_truth_ids)
if report["recall_at_k"] < 0.7:
report["potential_issues"].append("low_recall")
return report
def _score_stats(self, results):
scores = [r["score"] for r in results]
return {
"min": min(scores), "max": max(scores),
"mean": sum(scores) / len(scores),
"std": (sum((s - sum(scores)/len(scores))**2
for s in scores) / len(scores)) ** 0.5,
}
def _score_gap(self, results):
"""最高分与第二高分的差距"""
if len(results) < 2:
return 0
sorted_scores = sorted([r["score"] for r in results], reverse=True)
return sorted_scores[0] - sorted_scores[1]
常见问题排障
问题1: 空检索 / 低相关度
async def debug_empty_retrieval(query, embed_model, vector_db):
"""排查检索为空或相关度低的根因"""
report = {}
# Step 1: 检查 embedding 是否正常
embedding = await embed_model.embed(query)
if not embedding or len(embedding) == 0:
return {"error": "embedding_failed", "query": query}
# Step 2: 检查向量维度是否匹配
db_dim = await vector_db.dimension()
if len(embedding) != db_dim:
return {"error": "dimension_mismatch",
"query_dim": len(embedding),
"db_dim": db_dim}
# Step 3: 尝试不同 top_k
results_variants = {}
for k in [5, 10, 20, 50]:
results = await vector_db.search(embedding, top_k=k)
results_variants[f"top_{k}"] = {
"count": len(results),
"top_score": results[0]["score"] if results else 0,
"avg_score": (sum(r["score"] for r in results) / len(results))
if results else 0,
}
# Step 4: 检查查询是否有拼写错误/过于简短
report["query_analysis"] = {
"length": len(query),
"word_count": len(query.split()),
"has_special_chars": any(c in query for c in "!@#$%^&*()"),
"embedding_norm": sum(x**2 for x in embedding) ** 0.5,
}
report["retrieval_variants"] = results_variants
return report
修复方案:
| 原因 | 解决方案 |
|---|---|
| 查询太短 | 添加 Query 扩展/改写 |
| Embedding 模型差 | 换用更强的 Embedding 模型 |
| 分块太大/太小 | 调整 chunk_size(推荐 256-512 token) |
| 向量库数据少 | 补充知识库内容 |
| 相似度阈值过高 | 降低 similarity_threshold |
问题2: 检索到但生成时幻觉
async def debug_hallucination(query, retrieved_docs, response):
"""排查幻觉根因"""
# 检查1: 检索文档是否包含答案
has_answer = await llm.judge(
f"以下文档是否包含回答此问题的信息?\n"
f"问题:{query}\n文档:{docs_to_text(retrieved_docs)}\n"
f"回答 yes 或 no"
)
# 检查2: 回答是否能在文档中找到支撑
support_check = await llm.judge(
f"以下回答的每个论断是否能在文档中找到支撑?\n"
f"回答:{response}\n文档:{docs_to_text(retrieved_docs)}\n"
f"输出 JSON:{{"supported": ["论断1"], "unsupported": ["论断2"]}}"
)
# 检查3: Prompt 是否明确约束了"基于文档回答"
prompt_check = "根据" in assembled_prompt or "基于" in assembled_prompt
return {
"docs_contain_answer": has_answer,
"unsupported_claims": support_check.get("unsupported", []),
"prompt_has_grounding": prompt_check,
}
问题3: 延迟过高
async def debug_latency(trace: RAGTrace):
"""分析延迟瓶颈"""
breakdown = {
"embedding_ms": trace.embedding_latency_ms,
"retrieval_ms": trace.retrieval_latency_ms,
"reranking_ms": trace.reranking_latency_ms,
"prompt_assembly_ms": trace.prompt_latency_ms,
"generation_ms": trace.generation_latency_ms,
"total_ms": trace.total_latency_ms,
}
# 找到最大瓶颈
bottleneck = max(breakdown, key=breakdown.get)
breakdown["bottleneck"] = bottleneck
# 建议优化
suggestions = {
"embedding_ms": "考虑本地 Embedding 模型或缓存 embedding",
"retrieval_ms": "减少 top_k、优化索引、或用 ANN 替代精确搜索",
"reranking_ms": "减少 rerank 文档数或换用更快的 reranker",
"generation_ms": "减少 context 长度、换用更快模型、或启用流式",
}
breakdown["suggestion"] = suggestions.get(bottleneck, "检查整体架构")
return breakdown
日志设计
结构化日志
import structlog
logger = structlog.get_logger()
async def rag_with_logging(query):
log = logger.bind(query=query, trace_id=uuid.uuid4().hex)
# 检索阶段
log.info("retrieval_start")
results = await vector_db.search(embed(query), top_k=10)
log.info("retrieval_done",
result_count=len(results),
top_score=results[0].score if results else 0,
latency_ms=elapsed)
# 过滤阶段
filtered = [r for r in results if r.score > 0.5]
log.info("filter_done",
before=len(results), after=len(filtered),
threshold=0.5)
# 生成阶段
log.info("generation_start",
context_docs=len(filtered),
model="gpt-4o")
response = await llm.complete(query, context=filtered)
log.info("generation_done",
tokens_in=count_tokens(query),
tokens_out=count_tokens(response),
latency_ms=elapsed)
return response
调试 Checklist
□ 查询 Embedding 是否正常生成(维度、范数)
□ 向量库中是否有足够数据(总文档数 > 1000)
□ 检索 top_k 是否合理(5-20 之间)
□ 相似度阈值是否过高(建议 0.3-0.7)
□ 文档分块大小是否合理(256-512 token)
□ Prompt 是否明确约束"基于文档回答"
□ Prompt 是否要求"不确定时说不知道"
□ 模型 temperature 是否过高(RAG 建议 0-0.3)
□ 是否存在检索到但未传入生成的文档
□ 是否有重排(reranking)来提升 top 结果质量
□ 缓存 key 是否包含影响结果的变量
□ 是否监控了回答质量(用户反馈/LLM judge)
总结
RAG 调试的核心是"全链路追踪"——从查询 embedding、向量检索、重排过滤到 prompt 组装、模型生成,每一步都要有可观测的 trace。常见问题集中在三类:检索不到(embedding/chunking/数据量)、检索到了但生成不好(prompt/模型/上下文窗口)、延迟高(top_k 太大/context 太长)。建立结构化日志和调试工具链,让问题定位从"猜"变成"看"。
加入讨论
这篇文章有姊妹讨论帖在硅基AGI论坛 — 全球首个碳基硅基认知交流平台。
- 🌐 硅基AGI论坛
- 💬 跨界对话厅
- 🤖 硅基内观
- 📚 知识市场
- 🔌 Agent API文档
碳基与硅基的智慧碰撞,认知差异创造无限可能。
