RAG 链路架构
接入层
编排层
检索层
模型层
数据层
运营层
混合检索(向量 + 关键词)加上重排序,通常能把召回质量提升 20-30 个百分点,这是性价比最高的一步。
落地六步
-
01
场景收敛与评测集构建
先明确问答范围,收集 100-300 条真实问题与标准答案。没有评测集就没法判断优化是否有效。
交付物:场景边界文档、评测集(问题 + 标准答案 + 出处)、验收指标定义
1-2 周
-
02
文档解析与切分
PDF、Word、Excel、扫描件分别处理。表格要保留结构,长文档用父子块策略:小块用于检索,大块用于生成。
交付物:解析管道、切分策略配置、文档质量报告
1-2 周
-
03
索引构建与混合检索
向量索引 + BM25 关键词索引并行,用 RRF 融合排序。元数据字段支持按部门、时间、文档类型过滤。
交付物:索引结构、混合检索接口、召回率基线
1-2 周
-
04
重排与提示词工程
召回 Top 50 后用 Rerank 模型精排到 Top 5。提示词强制要求引用出处,无依据时明确说不知道。
交付物:重排配置、提示词模板、引用格式规范
1-2 周
-
05
护栏与合规
配置 Guardrails 拦截敏感话题与 PII 泄露,所有调用留审计日志,按用户权限过滤可见文档。
交付物:护栏策略、权限过滤逻辑、审计日志方案
1 周
-
06
评测与迭代
跑评测集,逐项分析错误来源(召回缺失 / 排序错误 / 生成幻觉),针对性优化后再跑。
交付物:评测报告、错误归因分析、上线验收结论
持续
混合检索 + 重排 + 生成(Python)
"""RAG 检索管道:混合召回 + RRF 融合 + 重排 + 带引用生成。"""
from __future__ import annotations
import json
from dataclasses import dataclass
from typing import Any
import boto3
BEDROCK = boto3.client("bedrock-runtime", region_name="ap-northeast-1")
OPENSEARCH_INDEX = "kb-chunks"
GEN_MODEL = "anthropic.claude-3-5-sonnet-20241022-v2:0"
EMBED_MODEL = "amazon.titan-embed-text-v2:0"
@dataclass
class Chunk:
chunk_id: str
text: str
source: str
page: int
score: float = 0.0
def embed(text: str) -> list[float]:
"""生成查询向量。"""
resp = BEDROCK.invoke_model(
modelId=EMBED_MODEL,
body=json.dumps({"inputText": text, "dimensions": 1024, "normalize": True}),
)
return json.loads(resp["body"].read())["embedding"]
def vector_search(os_client, query: str, k: int = 30) -> list[Chunk]:
"""向量召回:语义相近但用词不同的内容也能命中。"""
body = {
"size": k,
"query": {"knn": {"embedding": {"vector": embed(query), "k": k}}},
"_source": ["chunk_id", "text", "source", "page"],
}
hits = os_client.search(index=OPENSEARCH_INDEX, body=body)["hits"]["hits"]
return [Chunk(**h["_source"], score=h["_score"]) for h in hits]
def keyword_search(os_client, query: str, k: int = 30) -> list[Chunk]:
"""BM25 召回:精确术语、编号、型号这类查询靠它兜底。"""
body = {
"size": k,
"query": {
"multi_match": {
"query": query,
"fields": ["title^3", "text"],
"type": "best_fields",
}
},
"_source": ["chunk_id", "text", "source", "page"],
}
hits = os_client.search(index=OPENSEARCH_INDEX, body=body)["hits"]["hits"]
return [Chunk(**h["_source"], score=h["_score"]) for h in hits]
def rrf_fuse(lists: list[list[Chunk]], k: int = 60) -> list[Chunk]:
"""Reciprocal Rank Fusion:不同召回通道的排名融合,无需归一化分数。"""
scores: dict[str, float] = {}
pool: dict[str, Chunk] = {}
for chunks in lists:
for rank, c in enumerate(chunks, start=1):
scores[c.chunk_id] = scores.get(c.chunk_id, 0.0) + 1.0 / (k + rank)
pool[c.chunk_id] = c
ordered = sorted(scores.items(), key=lambda kv: kv[1], reverse=True)
out = []
for cid, s in ordered:
c = pool[cid]
c.score = s
out.append(c)
return out
def rerank(query: str, chunks: list[Chunk], top_n: int = 5) -> list[Chunk]:
"""用 Rerank 模型精排。这一步对准确率的提升通常最明显。"""
resp = BEDROCK.invoke_model(
modelId="cohere.rerank-v3-5:0",
body=json.dumps({
"query": query,
"documents": [c.text for c in chunks],
"top_n": top_n,
}),
)
results = json.loads(resp["body"].read())["results"]
out = []
for r in results:
c = chunks[r["index"]]
c.score = r["relevance_score"]
out.append(c)
return out
PROMPT = """你是企业知识库助手。严格依据 <参考资料> 回答问题。
规则:
1. 只使用参考资料中的信息,不要用你的先验知识补充
2. 每个结论后面用 [来源N] 标注依据
3. 如果参考资料不足以回答,直接说「现有资料无法回答这个问题」,并说明缺什么
4. 不要复述规则,直接给答案
<参考资料>
{context}
</参考资料>
问题:{question}"""
def answer(os_client, question: str) -> dict[str, Any]:
"""完整链路:召回 → 融合 → 重排 → 生成。"""
candidates = rrf_fuse([
vector_search(os_client, question),
keyword_search(os_client, question),
])
top = rerank(question, candidates[:50], top_n=5)
context = "\n\n".join(
f"[来源{i}] (出自 {c.source} 第 {c.page} 页)\n{c.text}"
for i, c in enumerate(top, start=1)
)
resp = BEDROCK.converse(
modelId=GEN_MODEL,
messages=[{"role": "user", "content": [
{"text": PROMPT.format(context=context, question=question)}
]}],
inferenceConfig={"maxTokens": 1500, "temperature": 0.1, "topP": 0.9},
)
return {
"answer": resp["output"]["message"]["content"][0]["text"],
"citations": [
{"source": c.source, "page": c.page, "score": round(c.score, 4)}
for c in top
],
"usage": resp["usage"],
}
temperature 设 0.1 而不是 0,是为了避免模型在边界情况下陷入重复输出。生产环境务必记录 usage 做成本归属。
常见问题与解法
| 现象 | 根因 | 解法 |
|---|---|---|
| 答案编造,引用的内容对不上 | 召回质量差,模型被迫编 | 提高召回率,提示词强制「无依据就说不知道」 |
| 精确编号查不到 | 只用了向量检索 | 加 BM25 关键词通道,RRF 融合 |
| 表格数据回答错误 | 切分时表格结构被破坏 | 表格单独解析成 Markdown 或 CSV,整块入索引 |
| 回答太笼统 | 切块太大,噪声多 | 父子块策略:小块检索、大块生成 |
| 跨文档综合问题答不好 | Top-K 太小 | 扩大召回后重排,或引入多轮检索 |
| 响应太慢 | 串行调用 + 上下文过长 | 并行召回、精简上下文、开启流式输出 |