RAG vs 长上下文:企业场景完整决策框架与技术实战
本文是「Claude 企业级工程实战手册」专栏第 13 篇。语义分块、混合检索 RRF、Cohere Reranker、幻觉检测------完整企业级 RAG 技术栈,附生产可用代码。
一、先做选择:RAG 还是长上下文?
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 单份合同/单个代码文件分析 | 长上下文 | 简单,无需 RAG 工程投入 |
| 内部知识库问答(<10 万文档) | RAG + 重排序 | 成本可控,质量高 |
| 超大语料库(>100 万文档) | RAG 必须 | 长上下文根本放不下 |
| 高精度法律/医疗查询 | RAG + 长上下文混合 | 先检索,再深度分析 |
| 高频实时查询(>1000 次/天) | RAG + Haiku | 速度快,成本低 |
| 跨文档综合分析推理 | 长上下文 | RAG 碎片化,无法整体推理 |
选型建议:用 20 个有代表性的查询,分别跑长上下文和 RAG,比较准确率和总成本。质量差距对业务足够重要 + 成本差距可接受 → 长上下文。否则 RAG 几乎总是更好的工程答案。
二、RAG 完整流水线
css
【文档入库阶段】
原始文档 → 解析清洗 → 语义分块 → 向量化 → 写入向量数据库
【查询阶段】
用户查询 → 查询扩展(生成变体) → 混合检索(向量 + BM25)
→ Reranker 重排序 → Top-5 上下文
→ Claude 生成(强制引用 + 幻觉检测)
三、语义分块(最容易被低估的环节)
固定大小分块(每 500 字切一刀)会在句子中间截断,破坏语义完整性。
python
from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai import OpenAIEmbeddings
from unstructured.partition.auto import partition
# 方案一:语义分块(推荐)
# 检测主题边界,不在句子中间切割
def semantic_chunking(text: str) -> list[str]:
splitter = SemanticChunker(
OpenAIEmbeddings(),
breakpoint_threshold_type="percentile",
breakpoint_threshold_amount=95 # 相似度低于 95 百分位时切割
)
return splitter.split_text(text)
# 方案二:文档感知分块(最佳,按文档结构切割)
def document_aware_chunking(file_path: str) -> list[dict]:
"""识别标题、段落等结构,沿自然边界切割"""
elements = partition(filename=file_path)
chunks = []
current = {"content": "", "section": ""}
for elem in elements:
elem_type = type(elem).__name__
if elem_type in ["Title", "Header"]:
if current["content"]:
chunks.append(current)
current = {"content": str(elem), "section": str(elem)}
else:
current["content"] += f"\n{str(elem)}"
if current["content"]:
chunks.append(current)
return chunks
对比数据:在企业知识库问答场景,语义分块比固定大小分块的召回率高约 12%,幻觉率降低约 8%。
四、混合检索(召回率 +17%)
单用向量检索会漏掉包含精确关键词的相关文档;单用 BM25 会漏掉语义相关但措辞不同的文档。混合检索 + 倒数排名融合(RRF)取两者之长。
python
from qdrant_client import QdrantClient
from rank_bm25 import BM25Okapi
from sentence_transformers import SentenceTransformer
class HybridRetriever:
def __init__(self, collection_name: str):
self.vector_client = QdrantClient("localhost", port=6333)
self.collection = collection_name
self.encoder = SentenceTransformer("all-MiniLM-L6-v2")
def search(
self,
query: str,
corpus: list[str],
top_k: int = 20,
final_k: int = 5,
alpha: float = 0.7 # 向量权重 70%,BM25 权重 30%
) -> list[dict]:
# 向量检索
query_vec = self.encoder.encode(query)
dense_results = self.vector_client.search(
collection_name=self.collection,
query_vector=query_vec.tolist(),
limit=top_k
)
# BM25 关键词检索
tokenized = [doc.lower().split() for doc in corpus]
bm25 = BM25Okapi(tokenized)
scores = bm25.get_scores(query.lower().split())
sparse_top = sorted(range(len(scores)), key=lambda i: scores[i], reverse=True)[:top_k]
# 倒数排名融合(RRF)
rrf_scores: dict[str, float] = {}
k = 60
for rank, result in enumerate(dense_results):
doc_id = str(result.id)
rrf_scores[doc_id] = rrf_scores.get(doc_id, 0) + alpha * (1 / (k + rank + 1))
for rank, idx in enumerate(sparse_top):
doc_id = str(idx)
rrf_scores[doc_id] = rrf_scores.get(doc_id, 0) + (1 - alpha) * (1 / (k + rank + 1))
# 返回 Top-K
top_ids = sorted(rrf_scores, key=rrf_scores.get, reverse=True)[:final_k]
return [{"id": i, "score": rrf_scores[i]} for i in top_ids]
五、查询扩展(提升覆盖率)
用 Haiku 把一个模糊查询扩展为多个精确查询变体,再分别检索取并集:
python
import anthropic
client = anthropic.Anthropic()
def expand_query(original_query: str) -> list[str]:
"""生成 3 个语义等价但措辞不同的查询变体"""
resp = client.messages.create(
model="claude-haiku-4-5-20251001", # Haiku 省成本
max_tokens=256,
messages=[{
"role": "user",
"content": f"""原始查询:{original_query}
生成 3 个语义等价但措辞不同的查询变体,提高检索覆盖率。
每行一个,不要编号,不要解释。"""
}]
)
variants = [v.strip() for v in resp.content[0].text.strip().split("\n") if v.strip()]
return [original_query] + variants[:3]
六、Cohere Reranker(最关键的质量提升)
向量检索召回 20-100 个候选,Reranker 精选 Top-5 给 Claude。这一步对精确率的提升往往超过 20%。
python
import cohere
co = cohere.Client("YOUR_COHERE_API_KEY")
def rerank_results(query: str, candidates: list[dict], top_n: int = 5) -> list[dict]:
"""用交叉编码器模型重新评分,精选最相关的 Top-N"""
documents = [c["content"] for c in candidates]
response = co.rerank(
query=query,
documents=documents,
model="rerank-v3.5",
top_n=top_n
)
return [
{
**candidates[r.index],
"relevance_score": r.relevance_score,
"original_rank": r.index
}
for r in response.results
]
七、Claude 生成(强制引用 + 幻觉检测)
python
import re
def rag_generate(query: str, retrieved_chunks: list[dict]) -> dict:
"""生成回答,强制引用,检测幻觉"""
# 组装上下文
context = "\n\n---\n\n".join([
f"[来源{i+1}] {chunk.get('source', '未知来源')}:\n{chunk['content']}"
for i, chunk in enumerate(retrieved_chunks)
])
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=2048,
system="""你是一个严格基于文档的问答助手。
规则(不可违反):
1. 只使用 <context> 中提供的信息回答
2. 每个关键声明必须标注 [来源N] 引用
3. 如果来源文档中没有相关信息,明确说"根据提供的文档,无法回答此问题"
4. 不推断、不编造文档中没有的信息
5. 回答末尾给出置信度:高 / 中 / 低""",
messages=[{
"role": "user",
"content": f"<context>\n{context}\n</context>\n\n{query}"
}]
)
answer = response.content[0].text
# 幻觉检测:验证引用编号是否超出范围
citations = re.findall(r'\[来源(\d+)\]', answer)
hallucination_issues = [
f"引用[来源{n}]超出文档范围(共 {len(retrieved_chunks)} 个来源)"
for n in citations if int(n) > len(retrieved_chunks)
]
# 检测是否没有任何引用(可能是幻觉风险)
if len(citations) == 0 and len(answer) > 100:
hallucination_issues.append("回答较长但缺少引用标注,存在幻觉风险")
return {
"answer": answer,
"sources": retrieved_chunks,
"hallucination_detected": len(hallucination_issues) > 0,
"issues": hallucination_issues,
"citation_count": len(set(citations))
}
八、长上下文的正确使用姿势
长上下文不是把所有文档一股脑放进去,有几个关键技巧:
python
def long_context_analysis(documents: list[dict], query: str) -> str:
# 1. 最重要的文档放最前面(紧接系统提示)
sorted_docs = sorted(documents, key=lambda d: d.get("relevance", 0), reverse=True)
# 2. XML 标签结构化,帮助 Claude 定位
docs_xml = "\n\n".join([
f"<document id='{i+1}' title='{doc.get('title', f'文档{i+1}')}'>\n"
f"{doc['content']}\n"
f"</document>"
for i, doc in enumerate(sorted_docs)
])
# 3. 大量文档必须开 Prompt Cache,否则成本失控
response = client.messages.create(
model="claude-opus-4-8",
max_tokens=8096,
system=[
{
"type": "text",
"text": f"以下是需要分析的文档集合:\n\n{docs_xml}",
"cache_control": {"type": "ephemeral"} # 必须!节省大量成本
}
],
# 4. 指令放在用户消息末尾(最接近生成位置,注意力最强)
messages=[{
"role": "user",
"content": f"{query}\n\n请跨文档综合分析,标注每个发现来自哪个文档(文档ID)。"
}]
)
return response.content[0].text
长上下文的位置效应:Claude 对上下文开头和结尾的注意力最强,中间部分容易被稀释。重要文档放最前,核心指令放最后。
九、质量评估(RAGAS 框架)
python
from ragas import evaluate
from ragas.metrics import faithfulness, answer_relevancy, context_precision, context_recall
from datasets import Dataset
def evaluate_rag_quality(
questions: list[str],
answers: list[str],
contexts: list[list[str]],
ground_truths: list[str]
) -> dict:
dataset = Dataset.from_dict({
"question": questions,
"answer": answers,
"contexts": contexts,
"ground_truth": ground_truths
})
result = evaluate(dataset, metrics=[
faithfulness, # 忠实度(目标 > 0.85)
answer_relevancy, # 答案相关性(目标 > 0.80)
context_precision, # 检索精准度(目标 > 0.75)
context_recall # 检索召回率(目标 > 0.80)
])
# 自动告警
if result["faithfulness"] < 0.85:
alert(f"⚠️ 幻觉率过高:{(1 - result['faithfulness']):.1%},需要排查")
if result["context_recall"] < 0.80:
alert(f"⚠️ 召回率不足:{result['context_recall']:.1%},建议优化分块策略")
return dict(result)
十、完整端到端流程
python
def enterprise_rag_pipeline(query: str, knowledge_base_dir: str) -> dict:
"""企业级 RAG 完整流程"""
retriever = HybridRetriever("enterprise_kb")
# 1. 查询扩展
expanded_queries = expand_query(query)
# 2. 混合检索(对每个变体检索,取并集)
all_candidates = []
for q in expanded_queries:
results = retriever.search(q, corpus, top_k=20, final_k=20)
all_candidates.extend(results)
# 去重
seen = set()
unique_candidates = [c for c in all_candidates if c["id"] not in seen and not seen.add(c["id"])]
# 3. Reranker 精选 Top-5
top_chunks = rerank_results(query, unique_candidates[:50], top_n=5)
# 4. Claude 生成
result = rag_generate(query, top_chunks)
return result
专栏首页:Claude 企业级工程实战手册
专栏导航 · Claude 企业级工程实战手册
⬅️ 上一篇:12. MCP 企业级集成全指南:从协议原理到 OAuth 2.1 安全配置四层体系 ➡️ 下一篇:14. AI Agent 安全全景:Promptware Kill Chain 与深度防御五层体系
本专栏共 14 篇,系统覆盖 Claude 模型选型 / Prompt 工程 / Claude Code 工作流 / API 高级用法 / MCP / RAG / AI 安全合规全链路。欢迎收藏:Claude 企业级工程实战手册