检索增强生成(Retrieval-Augmented Generation, RAG)已成为大模型落地企业知识库、客服机器人等场景的标配架构。然而,从 Demo 到生产环境,开发者常面临以下痛点:
本文从工程视角出发,结合实际代码,构建一个可扩展、可评估的 RAG 系统。我们将依次讨论 文档分块与索引优化、检索策略(含重排序)、上下文增强与 Prompt 设计 及 离线评估与线上监控,每个环节均提供可复现的实现方案。
核心组件:
策略 | 描述 | 适用场景 |
|---|---|---|
固定大小(Fixed-size) | 按 Token 数或字符数切分,可设置重叠 | 通用文档,实现简单 |
递归字符分割(RecursiveCharacterTextSplitter) | 按段落、句子、词语层级递归切割,尽量保持语义边界 | 非结构化长文档(如 Markdown、HTML) |
语义分割(Semantic Splitter) | 基于句子向量相似度决定断点 | 需要保持主题一致性的文档 |
推荐:使用 LangChain 的 RecursiveCharacterTextSplitter,设置 chunk_size=512,chunk_overlap=50(Token 级),平衡粒度与上下文连续性。
from langchain.text_splitter import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=512,
chunk_overlap=50,
separators=["\n\n", "\n", "。", " ", ""],
length_function=len,
)
chunks = splitter.split_documents(docs)为每个 chunk 附加元数据(来源文件、标题、章节、时间戳),便于后续过滤和引用溯源。
for chunk in chunks:
chunk.metadata["source"] = file_path
chunk.metadata["section"] = extract_section(chunk.page_content)模型 | 维度 | 语言 | 成本($/1M tokens) | 备注 |
|---|---|---|---|---|
text-embedding-3-small | 1536 | 多语言 | 0.02 | OpenAI,性能均衡 |
text-embedding-3-large | 3072 | 多语言 | 0.13 | 更高精度 |
BAAI/bge-large-zh-v1.5 | 1024 | 中文 | 免费(本地) | 开源 SOTA |
intfloat/multilingual-e5-large | 1024 | 多语言 | 免费(本地) | 需加 query: / passage: 前缀 |
建议:若预算充足且需多语言,选 text-embedding-3-small;若部署私有化,选 bge-large 或 e5,使用 sentence-transformers 加载。
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("BAAI/bge-large-zh-v1.5")
embeddings = model.encode(chunk_text, normalize_embeddings=True) # L2归一化数据库 | 类型 | 特点 |
|---|---|---|
Qdrant | 开源向量数据库 | 支持过滤、多向量、高 QPS |
Milvus | 开源向量数据库 | 分布式,适合海量数据 |
Pinecone | 云服务 | 全托管,简便 |
Elasticsearch | 全文+向量混合 | 支持混合检索原生 |
本文选用 Qdrant(Docker 部署):
# docker-compose.yml
services:
qdrant:
image: qdrant/qdrant:latest
ports:
- "6333:6333"
volumes:
- ./qdrant_storage:/qdrant/storage创建 Collection 并索引:
from qdrant_client import QdrantClient
from qdrant_client.models import VectorParams, Distance
client = QdrantClient(host="localhost", port=6333)
client.recreate_collection(
collection_name="knowledge_base",
vectors_config=VectorParams(
size=1024, # 与 embedding 维度一致
distance=Distance.COSINE,
),
)纯向量检索可能遗漏低频关键词匹配。引入 BM25 关键词检索,将两者得分加权融合(RRF 或加权求和)。
方案:使用 Qdrant 自带的 hybrid 查询(需全文索引支持),或使用 Elasticsearch。此处以 Qdrant + rank_fusion 为例:
from qdrant_client.models import Filter, SearchRequest
# 向量检索
vector_results = client.search(
collection_name="knowledge_base",
query_vector=query_embedding,
limit=10,
)
# 关键词检索(需 Qdrant 开启全文索引)
keyword_results = client.search(
collection_name="knowledge_base",
query_filter=Filter(must=[...]),
query="用户输入关键词",
limit=10,
with_payload=True,
)
# RRF 融合
def reciprocal_rank_fusion(results_list, k=60):
scores = {}
for results in results_list:
for rank, hit in enumerate(results):
id = hit.id
score = 1.0 / (k + rank + 1)
scores[id] = scores.get(id, 0) + score
return sorted(scores.items(), key=lambda x: x[1], reverse=True)初召回结果(Top-20)通过 Cross-Encoder 模型重新评分,提升排序准确性。Cross-Encoder 将查询和文档拼接输入,输出相关性分数(0~1),效果优于双塔向量。
模型:BAAI/bge-reranker-v2-m3(多语言)或 cross-encoder/ms-marco-MiniLM-L-6-v2(英文)。
from transformers import AutoModelForSequenceClassification, AutoTokenizer
import torch
tokenizer = AutoTokenizer.from_pretrained("BAAI/bge-reranker-v2-m3")
model = AutoModelForSequenceClassification.from_pretrained("BAAI/bge-reranker-v2-m3")
def rerank(query, passages, top_k=5):
pairs = [[query, p] for p in passages]
inputs = tokenizer(pairs, padding=True, truncation=True, return_tensors="pt", max_length=512)
with torch.no_grad():
scores = model(**inputs).logits.squeeze().tolist()
scored = sorted(zip(passages, scores), key=lambda x: x[1], reverse=True)
return [p for p, s in scored[:top_k]]将重排序后的 Top-5 文档送入 LLM,可显著提升事实准确性。
检索到的文档片段可能内容冗余或顺序混乱。我们需要构建一个结构化的上下文:
推荐:采用策略二,并限制总 Token 数(如 4000 tokens),超出则截断低分内容。
def build_context(docs, max_tokens=4000):
context_parts = []
total_tokens = 0
for idx, (content, score) in enumerate(docs):
prefix = f"[{idx+1}] {content[:100]}... (来源: {metadata.get('source')})\n"
part = f"{prefix}{content}\n"
tokens = len(part) // 3 # 粗略估算
if total_tokens + tokens > max_tokens:
break
context_parts.append(part)
total_tokens += tokens
return "\n".join(context_parts), total_tokensSYSTEM_PROMPT = """你是一个基于知识库的问答助手。你必须严格按照以下规则回答:
1. 只使用下方提供的上下文信息,不要添加任何外部知识。
2. 如果上下文不足以回答问题,请明确回答“根据当前知识库,无法回答此问题”。
3. 在回答末尾,用[1][2]等标注所引用的文档编号。
4. 回答应简洁、条理清晰,不超过200字。
"""
USER_PROMPT_TEMPLATE = """
上下文:
{context}
问题:{question}
回答:
"""使用 OpenAI Python SDK:
from openai import OpenAI
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
def generate_answer(context, question):
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": USER_PROMPT_TEMPLATE.format(context=context, question=question)}
]
response = client.chat.completions.create(
model="gpt-4-turbo",
messages=messages,
temperature=0.1,
max_tokens=500,
stream=False
)
return response.choices[0].message.content若使用开源模型(如 Qwen-2.5-7B),可用 vLLM 或 TGI 部署,兼容 OpenAI API 格式。
RAGAS 提供四个核心指标:
安装 ragas 并评估:
pip install ragasfrom ragas import evaluate
from ragas.metrics import faithfulness, answer_relevancy, context_relevancy
dataset = {
"question": [...],
"answer": [...],
"contexts": [[...], ...],
"ground_truth": [...] # 可选
}
result = evaluate(dataset, metrics=[faithfulness, answer_relevancy, context_relevancy])
print(result)建议设定每日自动化任务,使用历史对话数据计算上述指标,并记录到监控面板(如 Grafana)。
对分块大小、重叠长度、检索权重(向量 vs 关键词)、重排序阈值等参数,使用 Optuna 结合离线评估指标(如 Faithfulness)进行贝叶斯优化:
import optuna
def objective(trial):
chunk_size = trial.suggest_int("chunk_size", 256, 1024, step=128)
overlap = trial.suggest_int("overlap", 20, 100, step=10)
weight_vector = trial.suggest_float("weight_vector", 0.3, 0.7)
# 重新运行索引和评估,返回 faithfulness
return -faithfulness_score # 最小化负值
study = optuna.create_study()
study.optimize(objective, n_trials=30)陷阱 | 对策 |
|---|---|
检索结果噪声大 | 引入重排序,提高 Top-5 准确率;添加过滤条件(如时间范围) |
上下文过长被截断 | 采用摘要压缩,或选择更长窗口的 LLM(如 Gemini 1.5) |
生成幻觉严重 | 强化 Prompt 约束(强制引用);使用 Faithfulness 指标把关 |
向量数据库更新延迟 | 实现增量索引;使用 CDC(Change Data Capture)触发重建 |
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。