文章目录
- RAG的整体架构设计:

RAG系统完整流程:
- 数据准备阶段:把你的文档、数据库等各种知识源准备好
- 索引构建阶段:把这些知识"消化"成机器能快速查找的格式
- 检索阶段:用户问问题时,快速找到相关的知识
- 生成阶段:结合找到的知识和问题,生成靠谱的答案
1. Overview

AG全称是Retrieval-Augmented Generation,中文叫"检索增强生成"。听起来有点学术,咱们用大白话解释:
传统LLM的问题: 想象你养了一只鹦鹉(LLM),它很聪明,能流利地说话。但问题是,它只会说训练时学到的话,而且:
- 如果你问它昨天的新闻,它不知道(知识过时)
- 如果你问它你公司的内部规定,它也不知道(没见过的知识)
- 有时候它还会"胡编乱造",一本正经地瞎说(幻觉问题)
RAG的解决方案: 给这只鹦鹉配一个助手,助手手里拿着一堆资料。每次你提问时:
- 助手先翻资料,找到相关内容
- 把资料递给鹦鹉
- 鹦鹉看着资料回答问题
这样,鹦鹉的回答就有理有据了!
RAG技术通过将检索机制与生成模型结合,使LLM能够从外部知识源获取相关文档并基于这些文档生成答案,从而解决了传统LLM的知识过时、缺乏领域知识和幻觉等问题。
完整系统架构参考:

RAG的核心优势

举个实际例子:
假设你在做一个医疗问答系统: 用户问:“阿司匹林的常见副作用是什么?”
传统LLM可能回答: “阿司匹林可能导致胃部不适、恶心等。(但不确定是否完整或最新)”
RAG系统的工作流程:
- 检索系统在医学数据库中搜索"阿司匹林 副作用"
- 找到3篇相关医学文献
- 提取关键信息:胃肠道反应、出血风险、过敏反应等
- LLM基于这些文献生成答案: "根据医学文献,阿司匹林的常见副作用包括:
- 胃肠道反应(胃痛、恶心、消化不良)
- 出血风险增加
- 过敏反应(如荨麻疹) 参考来源:[文献1]、[文献2]"
看到区别了吗?RAG不仅更准确,还能告诉你信息来源!
2. Index(索引构建)

索引构建是RAG的"预处理"阶段,就像你上学前要先整理笔记、做好书签一样。这个阶段要把原始文档转化成机器能快速检索的格式。
2.1 数据获取(Data Ingestion)
第一步是收集数据,可能来自:
- 📄 PDF、Word、Markdown等文档
- 🌐 网页、Wiki
- 💾 数据库
- 📧 邮件、聊天记录
- 📊 Excel、CSV等结构化数据
实际案例: 某电商公司想做客服机器人,他们需要收集:
- 产品说明文档(PDF)
- 常见问题FAQ(网页)
- 历史工单数据(数据库)
- 退换货政策(Word文档)
2.2 文档预处理(Preprocessing)
拿到原始数据后,需要"清洗"一下:
#示例:文档预处理代码
def preprocess_document(doc):
# 1. 移除多余的空格和换行
doc = re.sub(r'\s+', ' ', doc)
# 2. 提取纯文本(从PDF、HTML等)
if doc_type == 'pdf':
text = extract_text_from_pdf(doc)
# 3. 规范化格式
text = text.strip().lower()
# 4. 去除无用信息(页眉、页脚等)
text = remove_headers_footers(text)
return text
2.3 文档分块(Chunking)
这是最关键的一步!为什么要分块?
想象一下: 你有一本500页的《红楼梦》,如果有人问"林黛玉是谁",你不可能把整本书都递给他看吧?你会翻到介绍林黛玉的那几页。
分块就是这个道理:把大文档切成小段,每段包含一个相对完整的语义单元。
常见的分块策略:
- 固定大小分块(最简单,但可能切断语义)
#每300个字符一块
chunk_size = 300
chunks = [text[i:i+chunk_size] for i in range(0, len(text), chunk_size)]
- 句子分块(按自然语言边界)
#按句号、问号、感叹号分割
import nltk
sentences = nltk.sent_tokenize(text)
段落分块(保留逻辑结构)
#按换行符或段落标记分割
chunks = text.split('\n\n')
滑动窗口分块(带重叠,避免信息丢失)
chunk_size = 300
overlap = 50 # 重叠50字符
chunks = []
for i in range(0, len(text), chunk_size - overlap):
chunks.append(text[i:i+chunk_size])
分块的关键参数:
- 块大小(Chunk Size):太小 → 语义不完整;太大 → 检索不精准 ;推荐200-500 tokens(约150-400个汉字)。
- 重叠(Overlap):避免关键信息被切断,推荐10-20% 的块大小。
举个例子:
原文:
阿司匹林是一种常用的解热镇痛药。它的主要作用包括:
解热:降低发烧体温
镇痛:缓解轻到中度疼痛
抗血小板:预防血栓形成
但是,阿司匹林也有副作用。常见的副作用包括:
胃肠道反应:胃痛、恶心
出血风险:特别是长期服用
分块后:
Chunk 1: "阿司匹林是一种常用的解热镇痛药。它的主要作用包括:
1. 解热:降低发烧体温
2. 镇痛:缓解轻到中度疼痛
3. 抗血小板:预防血栓形成"
Chunk 2: "阿司匹林的主要作用包括预防血栓形成。但是,阿司匹林也有副作用。
常见的副作用包括:
1. 胃肠道反应:胃痛、恶心
2. 出血风险:特别是长期服用"
2.4 向量化(Embedding)
分块后的文本还是人类语言,机器不懂。我们需要把它转换成向量(一串数字,Embedding)。
什么是Embedding?
把文字转换成数字向量,相似的文字会得到相似的向量。
#使用OpenAI的Embedding模型
from openai import OpenAI
client = OpenAI()
text = "阿司匹林是一种解热镇痛药"
response = client.embeddings.create(
model="text-embedding-3-small",
input=text
)
vector = response.data[0].embedding
print(f"向量维度: {len(vector)}") # 输出: 1536
print(f"前5个值: {vector[:5]}") # 输出: [0.023, -0.014, 0.089, ...]
为什么需要向量化?
想象你在找"感冒药",但文档里写的是"抗感冒药物"、"治疗感冒的药品"等各种说法。如果只靠关键词匹配,就找不到了。
但是!如果用向量化:
"感冒药" → [0.1, 0.3, 0.5, ...]
"抗感冒药物" → [0.12, 0.29, 0.51, ...] # 向量很接近!
"汽车" → [0.8, 0.1, 0.2, ...] # 向量差很远
向量之间可以计算相似度(余弦相似度),数值越接近1越相似:
from sklearn.metrics.pairwise import cosine_similarity
import numpy as np
vec1 = np.array([0.1, 0.3, 0.5])
vec2 = np.array([0.12, 0.29, 0.51])
similarity = cosine_similarity([vec1], [vec2])[0][0]
print(f"相似度: {similarity:.3f}") # 输出: 0.999(非常相似)
常用的Embedding模型:

2.5 存储到向量数据库
最后,把这些向量存起来,用专门的向量数据库(Vector Database)。
为什么不用普通数据库?
普通数据库(MySQL、MongoDB)擅长精确查询:“找ID=123的记录”。但向量搜索是相似性查询:“找和[0.1, 0.3, 0.5]最相似的10个向量”。
向量数据库用了特殊的索引算法(如HNSW、IVF),能在百万、千万级向量中毫秒级找到最相似的。
常用向量数据库:
- Pinecone(云服务,简单好用)
import pinecone
pinecone.init(api_key="your-api-key")
index = pinecone.Index("my-rag-index")
#插入向量
index.upsert([
("doc1_chunk1", vector1, {"text": "阿司匹林是..."}),
("doc1_chunk2", vector2, {"text": "副作用包括..."})
])
#查询
results = index.query(query_vector, top_k=3)
- Milvus(开源,功能强大)
- FAISS(Facebook开源,本地使用)
- Weaviate(支持混合搜索)
完整的索引构建代码示例:
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import Pinecone
import pinecone
#1. 读取文档
with open("medical_docs.txt", "r") as f:
document = f.read()
#2. 分块
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=50,
separators=["\n\n", "\n", "。", "!", "?", ","]
)
chunks = text_splitter.split_text(document)
#3. 初始化embedding模型
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
#4. 初始化向量数据库
pinecone.init(api_key="your-key")
index_name = "medical-rag"
#5. 创建索引并存储
vectorstore = Pinecone.from_texts(
texts=chunks,
embedding=embeddings,
index_name=index_name
)
print(f"成功索引了 {len(chunks)} 个文本块!")
3. Retrieval(向量检索)
- BM25与TF-IDF参考:https://blog.youkuaiyun.com/Tracycoder/article/details/158663878?sharetype=blogdetail&sharerId=158663878&sharerefer=PC&sharesource=Tracycoder&spm=1011.2480.3001.8118
- 混合搜索(Hybrid Search)参考:
索引建好后,用户来提问了。检索阶段的任务是:从海量知识中快速找出最相关的那几条。

3.1 查询处理
用户输入的问题往往不是完美的搜索词。比如:
用户原始问题:“我吃了阿司匹林后胃疼怎么办?”
需要优化为:
“阿司匹林 胃痛 副作用”(关键词提取)
“阿司匹林导致的胃部不适如何处理”(问题改写)
3.2 向量检索
把用户问题也转换成向量,然后在向量数据库中搜索:
#用户问题
question = "阿司匹林有哪些副作用?"
#问题向量化
question_embedding = embeddings.embed_query(question)
#向量检索(找最相似的3个)
results = vectorstore.similarity_search_by_vector(
embedding=question_embedding,
k=3 # 返回top3
)
for i, doc in enumerate(results):
print(f"结果{i+1}:")
print(doc.page_content)
print(f"相似度: {doc.metadata['score']}")
print("-" * 50)
输出示例:
结果1:
阿司匹林的常见副作用包括:1. 胃肠道反应:胃痛、恶心、消化不良...
相似度: 0.89
结果2:
长期服用阿司匹林可能增加出血风险,特别是胃肠道出血...
相似度: 0.85
结果3:
少数患者对阿司匹林过敏,可能出现荨麻疹、呼吸困难等症状...
相似度: 0.81
3.3 检索策略
检索系统通常采用混合搜索方法,结合向量搜索(找语义相似的文档)和关键词搜索(精确匹配),然后对结果进行排序和过滤。
- 单纯向量搜索
- 优点:能理解语义
- 缺点:对专有名词、数字等不敏感
- 混合搜索(Hybrid Search)
- 向量搜索 + 关键词搜索
- 综合排序,取最优结果
- 常采用RRF进行融合:
RRF 用“排名”而不是“原始分数”来融合多个检索器结果。 比如你有两路召回:
- BM25 排名列表
- 向量检索排名列表
RRF 会给每个文档一个融合分:
RRF ( d ) = ∑ i = 1 m 1 k + r i ( d ) \text{RRF}(d)=\sum_{i=1}^{m}\frac{1}{k+r_i(d)} RRF(d)=i=1∑mk+ri(d)1
- m m m:检索器个数
- r i ( d ) r_i(d) ri(d):文档 (d) 在第 (i) 个检索器中的名次(从 1 开始)
- k k k:平滑常数(常用 60)
#混合搜索示例
def hybrid_search(query, alpha=0.5):
# alpha: 向量搜索权重(0-1)
# 向量搜索结果
vector_results = vectorstore.similarity_search(query, k=10)
# 关键词搜索结果(BM25算法)
keyword_results = bm25_search(query, k=10)
# 融合排序
final_results = merge_results(vector_results, keyword_results, alpha)
return final_results[:3] # 返回top3
- 重排序(Re-ranking)
初步检索后,用更精细的模型重新排序,提高精度。
from sentence_transformers import CrossEncoder
#加载重排序模型
reranker = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')
#对检索结果重新打分
query = "阿司匹林副作用"
candidate_docs = ["文档1内容", "文档2内容", "文档3内容"]
scores = reranker.predict([(query, doc) for doc in candidate_docs])
#按分数排序
ranked_docs = [doc for _, doc in sorted(zip(scores, candidate_docs), reverse=True)]
3.4 检索效果评估指标
| 类别 | 指标 | 作用 | 公式/口径(简) | 数值含义(越大越好?) | 适用阶段 |
|---|---|---|---|---|---|
| 检索 | Recall@K | 看“相关证据有没有被召回” | TopK 中相关数 / 全部相关数 | ✅ 越大越好 | 离线检索评估核心 |
| 检索 | HitRate@K (Success@K) | TopK 是否至少命中一条相关 | 命中=1,否则0;再求平均 | ✅ 越大越好 | 快速看“能不能找到” |
| 检索 | Precision@K | TopK 结果纯度 | TopK 中相关数 / K | ✅ 越大越好 | 控制噪声文档 |
| 检索 | MRR@K | 第一条相关结果排位质量 | 平均(1/首个相关排名) | ✅ 越大越好 | 问答类场景很实用 |
| 检索 | MAP@K | 多相关文档时的整体排序质量 | AP 的平均(截断到K) | ✅ 越大越好 | 多证据任务 |
| 检索 | nDCG@K | 支持“分级相关性”的排序质量 | DCG/IDCG | ✅ 越大越好 | 有 graded relevance 标注时 |
| 上下文 | Context Recall | 金标准证据是否进入上下文 | 覆盖到的证据比例 | ✅ 越大越好 | 检索→生成衔接评估 |
| 上下文 | Context Precision | 上下文是否“少噪声” | 相关上下文 / 总上下文 | ✅ 越大越好 | 防止喂给LLM太多无关信息 |
| 上下文 | Context Relevance | 上下文与问题语义相关程度 | 模型/人工打分 | ✅ 越大越好 | 语义检索质量检查 |
| 生成 | EM | 答案与标准答案完全一致 | exact match 比例 | ✅ 越大越好 | 有标准答案任务 |
| 生成 | F1 | 答案与标准答案重合程度 | token 级 P/R 调和平均 | ✅ 越大越好 | 抽取/事实问答 |
| 生成 | Faithfulness / Groundedness | 回答是否基于证据、少幻觉 | 模型/人工判定 | ✅ 越大越好 | RAG 可信度核心 |
| 生成 | Answer Relevance | 回答是否真正回答问题 | 模型/人工打分 | ✅ 越大越好 | 用户体验关键 |
| 工程 | Latency(P50/P95) | 响应速度 | 请求耗时分位数 | ❌ 越小越好 | 线上 SLA |
| 工程 | Cost / Query | 单次请求成本 | 检索+重排+生成总成本 | ❌ 越小越好 | 线上运营 |
| 工程 | Throughput(QPS) | 吞吐能力 | 每秒请求数 | ✅ 越大越好 | 高并发场景 |
| 业务 | 人工满意度/转化率 | 最终业务价值 | 问卷、点击、转化 | ✅ 越大越好 | A/B 与上线决策 |
4. Generation(生成)

4.1 提示词构建(Prompt Engineering)
把检索到的文档和用户问题组合成一个完整的提示词(Prompt),发给LLM。
基础模板:
prompt_template = """
你是一个专业的医疗助手。请基于以下参考资料回答用户的问题。
如果参考资料中没有相关信息,请明确告知用户。
参考资料:
{context}
用户问题:
{question}
请提供详细、准确的回答,并标注信息来源。
"""
#填充内容
context = "\n\n".join([doc.page_content for doc in retrieved_docs])
question = "阿司匹林有哪些副作用?"
prompt = prompt_template.format(context=context, question=question)
进阶模板(结构化输出):
advanced_prompt = """
参考资料:
{context}
用户问题:{question}
请按以下格式回答:
直接回答:用1-2句话概括答案
详细说明:展开解释,分点列出
注意事项:相关的警告或建议
信息来源:标注参考了哪些资料
回答:
"""
4.2 调用LLM生成
from openai import OpenAI
client = OpenAI()
response = client.chat.completions.create(
model="gpt-4",
messages=[
{"role": "system", "content": "你是一个专业的医疗助手"},
{"role": "user", "content": prompt}
],
temperature=0.3, # 降低随机性,提高准确性
max_tokens=500
)
answer = response.choices[0].message.content
print(answer)
4.3 答案后处理
生成答案后,可能还需要:
- 引用标注:给答案加上来源链接
def add_citations(answer, sources):
"""给答案添加引用"""
for i, source in enumerate(sources):
answer += f"\n[{i+1}] {source['title']} - {source['url']}"
return answer
- 安全过滤:检查答案是否包含有害内容
def safety_check(answer):
harmful_keywords = ["自杀", "违法", "...]
for keyword in harmful_keywords:
if keyword in answer:
return "抱歉,这个问题涉及敏感内容,建议咨询专业人士。"
return answer
- 格式美化:Markdown格式化、表格化等
4.4 完整的RAG生成代码
def rag_query(question):
"""完整的RAG查询流程"""
# 1. 检索相关文档
retrieved_docs = vectorstore.similarity_search(question, k=3)
# 2. 构建prompt
context = "\n\n".join([
f"【文档{i+1}】{doc.page_content}"
for i, doc in enumerate(retrieved_docs)
])
prompt = f"""
参考以下资料回答问题:
{context}
问题:{question}
要求:
1. 回答要准确、专业
2. 必须基于参考资料
3. 标注信息来源
"""
# 3. 调用LLM生成
response = client.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": prompt}],
temperature=0.3
)
answer = response.choices[0].message.content
# 4. 添加引用
sources = [
{"title": f"文档{i+1}", "score": doc.metadata.get('score', 0)}
for i, doc in enumerate(retrieved_docs)
]
return {
"answer": answer,
"sources": sources,
"retrieved_docs": [doc.page_content for doc in retrieved_docs]
}
#使用示例
result = rag_query("阿司匹林有哪些副作用?")
print("答案:", result['answer'])
print("\n参考来源:", result['sources'])

236



被折叠的 条评论
为什么被折叠?



