RAG
RAG
基础概念
RAG,全称是 检索增强生成,是目前大语言模型最主流、最落地的应用架构之一。
它的核心思想很简单:给大模型配一个“外挂知识库”,让它先查资料再回答,而不是仅靠训练时记住的知识。
为什么需要RAG
大语言模型本身存在几个短板:
- 知识截止:训练数据有截止日期,无法回答最新问题。
- 容易“幻觉”:遇到不懂的会一本正经地胡说八道。
- 缺乏专业深度:对垂直领域和私有文档一无所知。
- 难以溯源:直接生成答案,无法指出依据。
RAG正是为解决这些痛点而设计的。它的作用就像考试时允许你带着参考书进考场,回答前先翻书找到依据,基于此作答,并标明出处。
RAG的工作流程
- 离线准备阶段 ( 知识入库 )
- 文档加载:读取各类私有文件,如PDF、Word、网页等。
- 文本分块:将文本切分成多块.比如每500字一个块,或者每一章一个块.
- 向量化:用嵌入模型将每个文本块转换成一串能代表其语义的数字。比如"RAG,全称是 检索增强生成,是目前大语言模型最主流、最落地的应用架构之一。......."经过向量化之后可能:
[0.32423525,0.98435345,0.234234234..........] - 存入向量库:将这些向量存入像FAISS、Milvus这样的专用数据库中,建好索引,等待查询。
- 在线查询阶段 ( 回答问题 )
- 问题向量化:使用相同的嵌入模型将你的问题也变成一个向量.
- 相似度检索:在向量库中找到与问题向量相似的几个文本块.
- 构建提示词:将检索到的文本块作为上下文填入预设的知识模板.
- 大模型生成:将组装好的提示词发送给大模型,生成答案.
应用场景
- 智能客服:基于最新的产品手册,准确回答用户问题。
- 企业知识库:让员工能“对话”内部制度、技术文档,瞬间找到答案。
- AI搜索:先搜索全网相关内容,再由AI总结,并附上参考链接。
- 法律/医疗助手:辅助专业人士快速查阅、分析海量法规或病历。
LlamaIndex实现一个简单的RAG
环境准备
下载包
uv pip install \
huggingface-hub \
llama-index \
llama-index-embeddings-huggingface \
llama-index-embeddings-openai \
llama-index-llms-openai \
llama-index-readers-file \
llama-index-readers-web \
lxml \
numpy \
openai \
pandas \
pymupdf \
pypdf \
requests \
torch \
transformers
下载嵌入向量模型
from modelscope import snapshot_download
# 下载模型到指定目录
model_dir = snapshot_download(
'iic/nlp_corom_sentence-embedding_chinese-base',
cache_dir='./my_models'
)
print(f"模型已下载到: {model_dir}")
# from huggingface_hub import snapshot_download
# model_dir = snapshot_download(
# 'BAAI/bge-base-zh', # 类似的中文embedding模型
# cache_dir='./my_models'
# )
env配置
在目录下创建一个.env文件
DEEPSEEK_API_KEY=sk-cf896432cb9****************
DEEPSEEK_BASE_URL=https://api.deepseek.com/v1
内容如上(key已经遮蔽处理).
实现
# 导入环境变量
import os
from dotenv import load_dotenv
load_dotenv()
# 导入嵌入向量模型
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader, Settings
Settings.embed_model = HuggingFaceEmbedding(model_name='./my_models/iic/nlp_corom_sentence-embedding_chinese-base')
# 读取本地文档
from llama_index.core import Settings, SimpleDirectoryReader
from llama_index.llms.openai_like import OpenAILike
reader = SimpleDirectoryReader(
input_dir="./data",
required_exts=[".pdf"],
recursive=True
)
reader = SimpleDirectoryReader("./data")
documents_origin = reader.load_data()
# 设置大模型
Settings.llm = OpenAILike(
model="deepseek-chat",
api_base=os.environ.get('DEEPSEEK_BASE_URL'),
api_key=os.environ.get("DEEPSEEK_API_KEY"),
is_chat_model=True,
)
# 可以尝试查看读取的字符
from pprint import pprint
import sys
for doc in documents_origin:
print(f"字符数: {len(doc.text)}")
# 完整打印,不省略
sys.stdout.write(doc.text[:2000]) # 前2000字符
print("\n" + "="*80)
# 构建查询索引
index_new = VectorStoreIndex.from_documents(documents_origin)
# 构建查询引擎
query_engine = index_new.as_query_engine()
# 输入问题得到结果
response = query_engine_new.query("同仁堂安宫牛黄丸的市场价格,中文回答")
print(response)
手动实现RAG以及改进方法
SimpleRAG
- 我们手动实现文本的分块,向量化,问题向量化,语义检索,提取构建提示词,提问.
# 导入有关的库
import fitz
import os
import numpy as np
import json
from openai import OpenAI
- 借助
fitz库实现从pdf提取文本.
def extract_text_from_pdf(pdf_path):
"""
从PDF文件中提取文本并打印前`num_chars`个字符。
参数:
pdf_path (str): PDF文件的路径。
返回:
str: 从PDF中提取的文本。
"""
# 打开PDF文件
mypdf = fitz.open(pdf_path)
all_text = "" # 初始化一个空字符串用于存储提取的文本
# 遍历PDF中的每一页
for page_num in range(mypdf.page_count):
page = mypdf[page_num] # 获取页面
text = page.get_text("text") # 从页面提取文本
all_text += text # 将提取的文本追加到all_text字符串中
return all_text # 返回提取的文本
- 提取文本之后,还需要将其划分为更小的,相互重叠的块(防止内容中间分开,语义断裂).
def chunk_text(text, n, overlap):
"""
将给定的文本分割为长度为 n 的段,并带有指定的重叠字符数。
参数:
text (str): 需要分割的文本。
n (int): 每个片段的字符数量。
overlap (int): 段与段之间的重叠字符数量。
返回:
List[str]: 一个包含文本片段的列表。
"""
chunks = [] # 初始化一个空列表用于存储片段
# 使用 (n - overlap) 的步长遍历文本
for i in range(0, len(text), n - overlap):
# 将从索引 i 到 i + n 的文本片段添加到 chunks 列表中
chunks.append(text[i:i + n])
return chunks # 返回包含文本片段的列表
- 创建OpenAI API客户端
from dotenv import load_dotenv
import os
load_dotenv()
client = OpenAI(
base_url= os.getenv("DEEPSEEK_BASE_URL")
api_key=os.getenv("DEEPSEEK_API_KEY")
)
- 整合:提取文本+文本分块+向量化
pdf_path = '../data/2024年大模型产业发展报告.pdf'
extract_text = extract_text_from_pdf(pdf_path)
text_chunks = extract_text
print("Number Of Text Chunks:",len(text_chunks))
text_chunks = text_chunks.replace('\n', '').replace('\r', '')
for i in text_chunks[0:10000]:
print(i,end='')
Number Of Text Chunks: 26968
前言伴随人工智能技术的加速演进,AI 大模型已成为全球科技竞争的新高地、未来产业的新赛..........
- 加载嵌入模型
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader, Settings
embed_model = HuggingFaceEmbedding(model_name='../my_models/iic/nlp_corom_sentence-embedding_chinese-base')
def create_embedding(text):
"""
使用指定的模型为给定文本创建嵌入。
参数:
text (str): 需要为其创建嵌入的输入文本。
返回:
dict: 包含嵌入结果的OpenAI API回复。
"""
response = embed_model.get_text_embedding(text)
return response
responses = []
text_chunks = [chunk for chunk in chunk_text(text_chunks,300,50)]
for i in text_chunks:
# print(i)
response = create_embedding(i)
responses.append(response)
responses
- 计算余弦相似度
两个有着相近意思的文本在向量空间是相近的,也许你会疑惑,千百字长度的文本块经过嵌入之后怎么会和长度十位数甚至个位数的问题的嵌入向量相近呢?
简单来说,无论输入多长,嵌入模型都输出固定维度的向量(比如 768 维)。这个向量不是文本的"摘要",而是文本在语义特征空间中的坐标。
当你的问题关键字和对应的文本块"匹配"的时候,在向量空间的指向是相近的,但是长度是不一样的.比如详细的可能长度更长,次之则更短.
因此我们计算两个向量的余弦来判断向量之间的关系,从而判断两个文本之间的相似性.
def cosine_similarity(vec1,vec2):
"""
计算两个向量之间的余弦相似度。
参数:
vec1 (np.ndarray): 第一个向量。
vec2 (np.ndarray): 第二个向量。
返回:
float: 两个向量之间的余弦相似度。
"""
return np.dot(vec1,vec2) / (np.linalg.norm(vec1) * np.linalg.norm(vec2))
- 查询相关的k个文本快
def semantic_search(query,text_chunks,embeddings,k=5):
"""
使用给定的查询和嵌入对文本块执行语义搜索。
------索引index+文本text+向量embedding-------
参数:
query (str): 语义搜索的查询。
text_chunks (List[str]): 要搜索的文本块列表。
embeddings (List[dict]): 文本块的嵌入列表。
k (int): 返回的相关文本块数量。默认值为5。
返回:
List[str]: 基于查询的前k个最相关文本块列表。
"""
query_embedding = embed_model.get_text_embedding(query)
similarity_scores = []
for i , chunk_embedding in enumerate(embeddings):
similarity_score = cosine_similarity(np.array(query_embedding),np.array(chunk_embedding))
similarity_scores.append((i,similarity_score))
similarity_scores.sort(key=lambda x:x[1],reverse=True)
top_indices = [index for index,_ in similarity_scores[:k]]
return [text_chunks[index] for index in top_indices]
- main
# 这里打开文件选取了一个问题,也可以直接定义一个问题变量,一样的
with open('../data/val.json')as f:
data = json.load(f)
query = data[2]['question']
# print(query)
top_chunks = semantic_search(query,text_chunks,responses,k=5)
# 查看检索到了哪些文本块
for i,chunk in enumerate(top_chunks):
print('='*20)
print(f"Context{i+1}:\n{chunk}")
print('='*20)
# 整合检索信息构建提示词交给llm
system_prompt = '你是一个人工智能助手,严格根据给定的上下文回答问题。当得到上下文信息之后,可以结合你的知识进行适当地背景介绍或者扩展.如果不能直接从所提供的上下文中得出答案,你可以这样回答:“我没有足够的信息来回答这个问题。"'
def generate_response(system_prompt,user_message,model="deepseek-v4-pro"):
"""
根据系统提示和用户消息生成AI模型的回复。
参数:
system_prompt (str): 用于指导AI行为的系统提示。
user_message (str): 用户的消息或查询。
model (str): 用于生成回复的模型。
返回:
dict: AI模型的回复。
"""
response = client.chat.completions.create(
model=model,
temperature=0.2,
messages = [
{
"role":"system","content":system_prompt
},
{
"role":"user","content":user_message
}
]
)
return response
user_prompt = "\n".join([f"Context {i + 1}:\n{chunk}\n=====================================\n" for i, chunk in enumerate(top_chunks)])
user_prompt = f"Question: {query}\n\n{user_prompt}\n"
ai_response = generate_response(system_prompt, user_prompt)
print(ai_response.choices[0].message.content)
根据提供的上下文,虽然没有直接给出“Explainable AI”(可解释人工智能)的明确定义,但我们可以结合上下文中的相关概念和我对AI领域的了解来回答。
**什么是Explainable AI?**
Explainable AI(可解释人工智能,简称XAI)是指一类人工智能系统或技术,其决策过程、输出结果和内部工作机制能够被人类以可理解的方式解读。传统的AI模型(尤其是复杂的深度学习模型)常被视为“黑箱”,难以解释为什么做出某个预测或决定。XAI的目标就是让这些过程变得透明、可追溯,以便用户、开发者和监管者理解AI的行为逻辑。
**为什么可解释AI很重要?**
上下文1明确指出,建立公众对AI的信任**需要透明度和可解释性**(“Building trust requires transparency, explainability, and responsible development and deployment of AI systems.”)。这直接点出了可解释AI的关键重要性:
- **建立信任**:如果人们无法理解AI如何做出影响他们生活的决定(例如贷款审批、医疗诊断),就很难相信并接受这些系统。
- **促进负责任的发展**:上下文中强调伦理考量需要“促进公平、透明和问责”(“promoting fairness, transparency, and accountability”)。可解释性是实现透明和问责的基础,帮助检测并纠正AI中的偏见或错误。
- **支持社会采纳与积极社会影响**:上下文提到AI有潜力解决社会挑战,但要实现这种积极影响,公众的认知和信任必不可少,而可解释性正是构建这种信任的核心要素。
总之,尽管上下文未详细展开XAI的技术细节,但已充分说明**可解释性是确保AI系统透明、可信并最终被社会广泛接纳的必要条件**。这就是为什么可解释AI被认为至关重要的原因。
- 效果评估
# 将问题直接发送给llm得出答案再和我们结合RAG的得到的内容,让llm对我们的RAG进行评估打分.
# 定义评估系统的系统提示
evaluate_system_prompt = "You are an intelligent evaluation system tasked with assessing the AI assistant's responses. If the AI assistant's response is very close to the true response, assign a score of 1. If the response is incorrect or unsatisfactory in relation to the true response, assign a score of 0. If the response is partially aligned with the true response, assign a score of 0.5."
# 通过组合用户查询、AI回复、真实回复和评估系统提示创建评估提示
evaluation_prompt = f"User Query: {query}\nAI Response:\n{ai_response.choices[0].message.content}\nTrue Response: {data[0]['ideal_answer']}\n{evaluate_system_prompt}"
# 使用评估系统提示和评估提示生成评估回复
evaluation_response = generate_response(evaluate_system_prompt, evaluation_prompt)
# 打印评估回复
print(evaluation_response.choices[0].message.content)
1
语义分块
概念
文本分块是检索增强生成(RAG)中的一个重要步骤,其中大段文本被划分为有意义的片段,以提高检索准确性。
与固定长度的分块不同,语义分块基于句子之间的内容相似性来划分文本。
-
百分位法(Percentile):找到所有相似度差异的第X百分位,并在下降幅度大于该值的位置进行分块。
-
标准差法(Standard Deviation):在相似度低于平均值X个标准差的位置进行分块。
-
四分位距法(Interquartile Range, IQR):使用四分位距(Q3 - Q1)来确定分块点。
这里演示基于 百分位法 的语义分块.

实现
# 首先计算分块之后每两个临近的文本块之间的相似度
def cosine_similarity(vec1,vec2):
"""
计算两个向量之间的余弦相似度。
参数:
vec1 (np.ndarray): 第一个向量。
vec2 (np.ndarray): 第二个向量。
返回:
float: 余弦相似度。
"""
return np.dot(vec1,vec2) / (np.linalg.norm(vec1) * np.linalg.norm(vec2))
similarities = [cosine_similarity(embeddings[i],embeddings[i+1]) for i in range(len(embeddings) - 1) ]
# 根据相似度使用百分位数进行断点查找
# 比如threshold=90的断点意味着,有90%的样本数据小于断点的数据
# 假设一个班有100个学生考试:
# 第90百分位数 = 95分
# 意思是:90%的学生分数 ≤ 95分(只有10个学霸超过95分)
# 95分就是"第90百分位数"
def compute_breakpoints(similarities,method='percentile',threshold=90):
"""
根据相似度下降计算分块断点。
参数:
similarities (List[float]): 句子之间的相似度分数列表。
method (str): 'percentile', 'standard_deviation' 或 'interquartile'。
threshold (float): 阈值(对于 'percentile' 是百分位数,对于 'standard_deviation' 是标准差的数量)。
返回:
List[int]: 应该发生分块分裂的索引位置列表。
"""
if method =='percentile':
threshold_value = np.percentile(similarities,threshold)
elif method == 'standar_deviation':
mean = np.mean(similarities)
std_dev = np.std(similarities)
threshold_value = mean - (threshold * std_dev)
elif method == 'interquartile':
q1,q3 = np.percentile(similarities,[25,75])
threshold_value = q1-1.5*(q3-q1)
else:
raise ValueError("Invalid method.Choose 'percentile','standard_deviation' or 'interquartile'.")
return [i for i ,sim in enumerate(similarities) if sim < threshold_value]
# 根据上面的得出的断点将文本分块
def split_into_chunks(sentences,breakpoints):
"""
将句子分割为语义块。
参数:
sentences (List[str]): 句子列表。
breakpoints (List[int]): 应该发生分割的索引位置。
返回:
List[str]: 文本块列表。
"""
chunks = []
start = 0
for bp in breakpoints:
chunks.append(". ".join(sentences[start:bp + 1]) + ".")
start = bp + 1 # 更新起始索引到断点后的下一个句子
chunks.append(". ".join(sentences[start:]))
return chunks
之后同理将得到的结果和提问构造提示词交给LLM然后得到结果.
上下文增强检索
概念
Retrieval-Augmented Generation(RAG)通过从外部来源检索相关知识来增强AI回复。传统的检索方法返回孤立的文本片段,这可能导致不完整的答案。
为了解决这个问题,我们引入了上下文增强检索,确保检索到的信息包括相邻片段以提高连贯性。
实现
def context_enriched_search(query, text_chunks, embeddings, k=1, context_size=1):
"""
获取最相关的片段及其邻近片段。
参数:
query (str): 搜索查询。
text_chunks (List[str]): 文本片段列表。
embeddings (List[dict]): 片段嵌入列表。
k (int): 要检索的相关片段数量。
context_size (int): 包含的邻近片段数量。
返回:
List[str]: 带有上下文信息的相关文本片段。
"""
# 将查询转换为嵌入向量
query_embedding = create_embeddings(query).data[0].embedding
similarity_scores = []
# 计算查询与每个文本片段嵌入之间的相似度分数
for i, chunk_embedding in enumerate(embeddings):
# 计算查询嵌入与当前片段嵌入之间的余弦相似度
similarity_score = cosine_similarity(np.array(query_embedding), np.array(chunk_embedding.embedding))
# 将索引和相似度分数存储为元组
similarity_scores.append((i, similarity_score))
# 按相似度分数降序排序(最高相似度优先)
similarity_scores.sort(key=lambda x: x[1], reverse=True)
# 获取最相关片段的索引
top_index = similarity_scores[0][0]
# 定义上下文包含的范围
# 确保不超出文本片段的起始或结束边界
start = max(0, top_index - context_size)
end = min(len(text_chunks), top_index + context_size + 1)
# 返回相关片段及其邻近上下文片段
return [text_chunks[i] for i in range(start, end)]
上下文片段标题CCH
概念
检索增强生成(RAG)通过在生成回复之前检索相关的外部知识来提高语言模型的事实准确性。然而,标准的分块方法经常丢失重要上下文,从而使检索效果降低。
上下文片段标题(CCH)通过在嵌入每个片段之前为其添加高级上下文(如文档标题或章节标题)来增强RAG。这提高了检索质量并防止了脱离上下文的回复。
简而言之就是分块之后,对每一个块都加一个标题总结.这个标题可以是根据原文内容直接提取 ( 要求干净整齐的格式 ),也可以是交给llm总结返回.这里我们使用后者.
实现
def generate_chunk_header(chunk, model="deepseek-v4-pro"):
"""
使用LLM为给定的文本块生成标题/头部。
参数:
chunk (str): 作为标题总结的文本块。
model (str): 用于生成标题的模型。默认值为"gpt-4o"。
返回:
str: 生成的标题/头部。
"""
# 定义系统提示以指导AI的行为
system_prompt = "Generate a concise and informative title for the given text."
# 基于系统提示和文本块从AI模型生成回复
response = client.chat.completions.create(
model=model,
temperature=0,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": chunk}
]
)
# 返回生成的标题/头部,并去除任何前导/尾随空白字符
return response.choices[0].message.content.strip()
def chunk_text_with_headers(text, n, overlap):
"""
将文本分割为较小的片段并生成标题。
参数:
text (str): 要被分割的完整文本。
n (int): 每个片段的字符长度。
overlap (int): 片段之间的重叠字符数量。
返回:
List[dict]: 包含 'header' 和 'text' 键的字典列表。
"""
chunks = [] # 初始化一个空列表以存储片段
# 使用指定的片段大小和重叠值遍历文本
for i in range(0, len(text), n - overlap):
chunk = text[i:i + n] # 提取一个文本片段
header = generate_chunk_header(chunk) # 使用LLM生成片段的标题
chunks.append({"header": header, "text": chunk}) # 将标题和片段追加到列表中
return chunks # 返回包含标题和片段的列表
构造QA对的RAG
概念
通过问题生成进行文档增强的改进型RAG方法。通过对每个文本块生成相关的问题,优化了检索过程,从而提高了语言模型的回答质量。
对每个文本块都令llm为其总结生成n个问题,在连同嵌入向量等存储在一起.这样当用户提问的时候,进行问题匹配,文本匹配.这样可以极高的增强相似度匹配.
但是对token消耗成本大,时间长,需要慎重考虑.
实现
def generate_questions(text_chunk, num_questions=5, model="gpt-4o"):
"""
根据给定的文本块生成相关的问题(中文)。
参数:
text_chunk (str): 用于生成问题的文本块。
num_questions (int): 要生成的问题数量。
model (str): 用于问题生成的模型。
返回:
List[str]: 生成的问题列表。
"""
# 中文系统提示
system_prompt = "你是一名擅长从文本中提炼关键信息并生成相关问题的专家。请只用中文,根据给定文本生成可以用该文本直接回答的简明问题,聚焦于核心信息和重要概念。"
# 中文用户提示
user_prompt = f"""
请根据以下文本内容,生成 {num_questions} 个不同的问题,这些问题都可以仅通过该文本内容来回答:
{text_chunk}
请只用中文作答,输出格式为编号的问题列表,不要有其他多余内容。
"""
# 使用OpenAI API生成问题
response = client.chat.completions.create(
model=model,
temperature=0.7,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
]
)
# 从回复中提取并清理问题
questions_text = response.choices[0].message.content.strip()
questions = []
# 使用正则表达式模式匹配提取问题
for line in questions_text.split('\n'):
# 移除编号并清理空白字符
cleaned_line = re.sub(r'^\d+\.\s*', '', line.strip())
if cleaned_line and cleaned_line.endswith('?'):
questions.append(cleaned_line)
return questions
这里使用numpy实现一个简单的向量存储系统.
class SimpleVectorStore:
"""
使用NumPy实现的简单向量存储。
"""
def __init__(self):
"""
初始化向量存储。
"""
self.vectors = []
self.texts = []
self.metadata = []
def add_item(self, text, embedding, metadata=None):
"""
向向量存储中添加一个项目。
参数:
text (str): 原始文本。
embedding (List[float]): 嵌入向量。
metadata (dict, 可选): 其他元数据。
"""
self.vectors.append(np.array(embedding))
self.texts.append(text)
self.metadata.append(metadata or {})
def similarity_search(self, query_embedding, k=5):
"""
查找与查询嵌入最相似的项目。
参数:
query_embedding (List[float]): 查询嵌入向量。
k (int): 返回的结果数量。
返回:
List[Dict]: 最相似的前k个项目及其文本和元数据。
"""
if not self.vectors:
return []
# 将查询嵌入转换为numpy数组
query_vector = np.array(query_embedding)
# 使用余弦相似度计算相似性
similarities = []
for i, vector in enumerate(self.vectors):
similarity = np.dot(query_vector, vector) / (np.linalg.norm(query_vector) * np.linalg.norm(vector))
similarities.append((i, similarity))
# 按相似度降序排序
similarities.sort(key=lambda x: x[1], reverse=True)
# 返回前k个结果
results = []
for i in range(min(k, len(similarities))):
idx, score = similarities[i]
results.append({
"text": self.texts[idx],
"metadata": self.metadata[idx],
"similarity": score
})
return results
整合,构建向量存储.
def process_document(pdf_path, chunk_size=1000, chunk_overlap=200, questions_per_chunk=5):
"""
处理带有问题增强的文档。
参数:
pdf_path (str): PDF文件的路径。
chunk_size (int): 每个文本块的字符大小。
chunk_overlap (int): 块之间的重叠字符数。
questions_per_chunk (int): 每个块生成的问题数量。
返回:
Tuple[List[str], SimpleVectorStore]: 文本块和向量存储。
"""
print("Extracting text from PDF...")
extracted_text = extract_text_from_pdf(pdf_path)
print("Chunking text...")
text_chunks = chunk_text(extracted_text, chunk_size, chunk_overlap)
print(f"Created {len(text_chunks)} text chunks")
vector_store = SimpleVectorStore()
print("Processing chunks and generating questions...")
for i, chunk in enumerate(tqdm(text_chunks, desc="Processing Chunks")):
# 为块本身创建嵌入
chunk_embedding_response = create_embeddings(chunk)
chunk_embedding = chunk_embedding_response.data[0].embedding
# 将块添加到向量存储中
vector_store.add_item(
text=chunk,
embedding=chunk_embedding,
metadata={"type": "chunk", "index": i}
)
# 为该块生成问题
questions = generate_questions(chunk, num_questions=questions_per_chunk)
# 为每个问题创建嵌入并添加到向量存储中
for j, question in enumerate(questions):
question_embedding_response = create_embeddings(question)
question_embedding = question_embedding_response.data[0].embedding
# 将问题添加到向量存储中
vector_store.add_item(
text=question,
embedding=question_embedding,
metadata={"type": "question", "chunk_index": i, "original_chunk": chunk}
)
return text_chunks, vector_store
其他的与上文一致.有关生成嵌入向量代码有些许不同,有的是使用了本地模型,有的是使用在线模型,因而不同.
查询转换
概念
三种查询转换技术,以在不依赖LangChain等专用库的情况下提升RAG系统中的检索性能。通过修改用户查询,可以显著提高检索到的信息的相关性和全面性。
- 查询重写:使查询更加具体和详细,从而提高搜索的精确度。
- 后退提示生成:生成更广泛的查询以检索有用的上下文信息。
- 子查询分解:将复杂的查询分解为更简单的组件,以实现全面检索。
实现
def rewrite_query(original_query, model="gpt-4o"):
"""
根据给定的原始查询重新编写查询,使其更加具体和详细,以便更好地检索信息。
参数:
original_query (str): 原始用户查询
model (str): 用于查询重写的模型名称
返回:
str: 重写后的查询
"""
# 中文系统提示,指导AI助手的行为
system_prompt = "你是一名擅长优化检索查询的AI助手。你的任务是将用户的查询改写得更加具体、详细,并包含有助于检索准确信息的相关术语和概念。"
# 中文用户提示,包含需要被重写的原始查询
user_prompt = f"""
请将下列查询改写为更具体、更详细的表达,补充相关的关键词和概念,以便更好地检索到准确的信息。
原始查询:{original_query}
改写后的查询:
"""
# 使用指定的模型生成重写后的查询
response = client.chat.completions.create(
model=model,
temperature=0.0, # 低温度值以确保输出确定性
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
]
)
# 返回重写后的查询,并去除任何前导或尾随空白字符
return response.choices[0].message.content.strip()
def generate_step_back_query(original_query, model="gpt-4o"):
"""
生成一个更通用的“后退一步”查询,以检索更广泛的上下文。
参数:
original_query (str): 原始用户查询
model (str): 用于生成后退一步查询的模型
返回:
str: 后退一步查询
"""
# 中文系统提示,指导AI助手的行为
system_prompt = "你是一名擅长检索策略的AI助手。你的任务是将具体的用户查询改写为更宽泛、更通用的问题,以便检索到相关的背景信息和更广泛的上下文。"
# 中文用户提示,包含需要被泛化的原始查询
user_prompt = f"""
请将下列查询改写为更宽泛、更通用的问题,以便有助于检索相关的背景信息和更广泛的上下文。
原始查询:{original_query}
后退一步的查询:
"""
# 使用指定的模型生成后退一步查询
response = client.chat.completions.create(
model=model,
temperature=0.1, # 稍高的温度以获得一些变化
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
]
)
# 返回后退一步查询,并去除任何前导/尾随空白字符
return response.choices[0].message.content.strip()
def decompose_query(original_query, num_subqueries=4, model="gpt-4o"):
"""
将复杂的查询分解为更简单的子查询。
参数:
original_query (str): 原始的复杂查询
num_subqueries (int): 生成的子查询数量
model (str): 用于查询分解的模型
返回:
List[str]: 一个包含简单子查询的列表
"""
# 中文系统提示,指导AI助手的行为
system_prompt = "你是一名擅长将复杂问题拆解为简单子问题的AI助手。你的任务是把复杂的用户查询分解为若干个更简单、聚焦不同方面的子问题,所有子问题的答案合起来可以完整回答原始问题。"
# 中文用户提示,包含待分解的原始查询
user_prompt = f"""
请将下列复杂查询拆解为 {num_subqueries} 个更简单的子问题。每个子问题应关注原始问题的不同方面。
原始查询:{original_query}
请生成 {num_subqueries} 个子问题,每行一个,格式如下:
1. [第一个子问题]
2. [第二个子问题]
以此类推……
"""
# 使用指定的模型生成子查询
response = client.chat.completions.create(
model=model,
temperature=0.2, # 稍高的温度以获得一些变化
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
]
)
# 处理回复以提取子查询
content = response.choices[0].message.content.strip()
# 使用简单的解析方法提取编号的查询
lines = content.split("\n")
sub_queries = []
for line in lines:
if line.strip() and any(line.strip().startswith(f"{i}.") for i in range(1, 10)):
# 移除编号和前导空格
query = line.strip()
query = query[query.find(".")+1:].strip()
sub_queries.append(query)
return sub_queries
# 示例查询
original_query = "中国政府在AI上有哪些政策?"
# 应用查询变换
print("Original Query:", original_query)
# 查询重写
rewritten_query = rewrite_query(original_query)
print("\n1. Rewritten Query:")
print(rewritten_query)
# 后退提示生成
step_back_query = generate_step_back_query(original_query)
print("\n2. Step-back Query:")
print(step_back_query)
# 子查询分解
sub_queries = decompose_query(original_query, num_subqueries=4)
print("\n3. Sub-queries:")
for i, query in enumerate(sub_queries, 1):
print(f" {i}. {query}")
重排序
概念
reranking(重排序)技术,是用以提高RAG系统中的检索质量。Reranking作为初始检索后的第二道过滤步骤,确保使用最相关的内容进行回复生成。
- 初始检索:第一轮使用基本的相似性搜索(准确性较低但速度较快)
- 文档评分:评估每个检索到的文档与查询的相关性
- 重新排序:根据相关性分数对文档进行排序
- 选择:仅使用最相关的文档进行回复生成
实现
基于LLM重排序实现
def rerank_with_llm(query, results, top_n=3, model="gpt-4o"):
"""
使用LLM进行相关性评分来重新排序搜索结果。
参数:
query (str): 用户查询
results (List[Dict]): 初始搜索结果
top_n (int): 重新排序后返回的结果数量
model (str): 用于评分的模型
返回:
List[Dict]: 重新排序后的结果
"""
print(f"Reranking {len(results)} documents...") # 打印将要重新排序的文档数量
scored_results = [] # 初始化一个空列表来存储评分结果
# 定义LLM的系统提示
system_prompt = """你是一名擅长评估文档与查询相关性的专家。
你的任务是根据文档对查询的回答程度,对文档进行0到10分的打分。
评分标准:
- 0-2分:文档与查询完全无关
- 3-5分:文档包含部分相关信息,但未能直接回答查询
- 6-8分:文档相关,能够部分回答查询
- 9-10分:文档高度相关,能够直接回答查询
你必须只回复一个0到10之间的整数分数,不要包含任何其他文本。"""
# 遍历每个结果
for i, result in enumerate(results):
# 每隔5个文档显示一次进度
if i % 5 == 0:
print(f"Scoring document {i+1}/{len(results)}...")
# 定义LLM的用户提示
user_prompt = f"""查询:{query}
文档内容:
{result['text']}
请根据上述查询,对该文档与查询的相关性进行0到10分的打分:"""
# 获取LLM的回复
response = client.chat.completions.create(
model=model,
temperature=0,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
]
)
# 从LLM回复中提取分数
score_text = response.choices[0].message.content.strip()
# 使用正则表达式提取数值分数
score_match = re.search(r'\b(10|[0-9])\b', score_text)
if score_match:
score = float(score_match.group(1))
else:
# 如果分数提取失败,则使用相似度分数作为回退方案
print(f"Warning: Could not extract score from response: '{score_text}', using similarity score instead")
score = result["similarity"] * 10
# 将评分结果追加到列表中
scored_results.append({
"text": result["text"],
"metadata": result["metadata"],
"similarity": result["similarity"],
"relevance_score": score
})
# 按相关性分数降序对结果进行排序
reranked_results = sorted(scored_results, key=lambda x: x["relevance_score"], reverse=True)
# 返回前top_n个结果
return reranked_results[:top_n]
基于关键词的简单重排
#pip install jieba
import jieba
def rerank_with_keywords(query, results, top_n=3):
"""
基于中文关键词分词和位置的简单重排序方法。
参数:
query (str): 用户查询(中文)
results (List[Dict]): 初始搜索结果
top_n (int): 重排序后返回的结果数量
返回:
List[Dict]: 重排序后的结果
"""
# 用jieba对查询进行分词,过滤掉长度为1的词(可根据实际需求调整)
keywords = [word for word in jieba.lcut(query) if len(word) > 1]
scored_results = [] # 初始化一个列表用于存储评分结果
for result in results:
document_text = result["text"]
# 基础分数从向量相似度开始
base_score = result["similarity"] * 0.5
# 初始化关键词分数
keyword_score = 0
for keyword in keywords:
if keyword in document_text:
# 每找到一个关键词就加分
keyword_score += 0.1
# 如果关键词出现在开头附近,则加更多分
first_position = document_text.find(keyword)
if 0 <= first_position < len(document_text) / 4: # 在文本的前四分之一部分
keyword_score += 0.1
# 根据关键词频率加分
frequency = document_text.count(keyword)
keyword_score += min(0.05 * frequency, 0.2) # 最多加到 0.2
# 通过结合基础分数和关键词分数计算最终分数
final_score = base_score + keyword_score
# 将评分结果追加到列表中
scored_results.append({
"text": result["text"],
"metadata": result["metadata"],
"similarity": result["similarity"],
"relevance_score": final_score
})
# 按最终相关性分数降序对结果进行排序
reranked_results = sorted(scored_results, key=lambda x: x["relevance_score"], reverse=True)
# 返回 top_n 个结果
return reranked_results[:top_n]
def rag_with_reranking(query, vector_store, reranking_method="llm", top_n=3, model="gpt-4o"):
"""
完整的RAG管道,包含重排序步骤。
参数:
query (str): 用户查询
vector_store (SimpleVectorStore): 向量存储
reranking_method (str): 重排序方法('llm' 或 'keywords')
top_n (int): 重排序后返回的结果数量
model (str): 回复生成模型
返回:
Dict: 包括查询、上下文和回复的结果字典
"""
# 创建查询嵌入
query_embedding = create_embeddings(query)
# 初始检索(获取比需要的多的结果用于重排序)
initial_results = vector_store.similarity_search(query_embedding, k=10)
# 应用重排序
if reranking_method == "llm":
reranked_results = rerank_with_llm(query, initial_results, top_n=top_n)
elif reranking_method == "keywords":
reranked_results = rerank_with_keywords(query, initial_results, top_n=top_n)
else:
# 没有重排序,直接使用初始检索的前N个结果
reranked_results = initial_results[:top_n]
# 组合重排序结果的上下文
context = "\n\n===\n\n".join([result["text"] for result in reranked_results])
# 基于上下文生成回复
response = generate_response(query, context, model)
return {
"query": query,
"reranking_method": reranking_method,
"initial_results": initial_results[:top_n],
"reranked_results": reranked_results,
"context": context,
"response": response
}
增强型RAG段落提取 ( RSE )
概念
相关片段往往在文档中聚集在一起。通过识别这些聚类并保留其连续性,可以为LLM提供更连贯的上下文。
在 Retrieval-Augmented Generation (RAG) 系统里,“上下文压缩”(Context Compression)指对检索回来的原始资料进行删减、摘要或重写,以更少的 token 携带同等或更高的信息密度,便于下游大模型消费。是否需要做压缩,本质上取决于两条硬约束和两类性能目标:
| 决策维度 | 典型触发条件 | 说明 |
|---|---|---|
| 模型与接口限制 | - 超过 LLM 单次最大输入 tokens - API 调用成本过高(按 token 计费) |
无法直接塞进 prompt 或成本不划算时,先压缩 |
| 场景性能目标 | - 对时延要求严苛 - 需多轮检索/链式思考,累积上下文不断膨胀 |
压缩能减少 I/O 量和推理时间 |
IF (输入 token 预计超限 OR 成本/延迟过高) THEN
压缩上下文
├─ IF 在检索后 -> 聚合/摘要当前文档块
├─ ELSE IF 离线数据准备阶段 -> 预压缩存库
└─ ELSE IF 多轮推理中 -> 对历史对话 & 中间结果滚动摘要
ELSE
直接拼接 prompt
在实践中,可以用 启发式阈值(如 total_tokens > 0.8 * max_context) 或 自适应调度器(监控 QPS、Latency、费用)来自动决定是否启动压缩,让 RAG 在质量和资源之间动态平衡。
实现
目的:为每个文档块分配一个数值,衡量其相对于查询的价值
- 使用向量检索计算每个块与查询的相似度(余弦相似度等)
- 引入惩罚因子(默认0.2),使低相关块获得负值
- 效果:高相关块为正,低相关块为负,不相关块为负值,负值在后续求和的时候会自动"惩罚"包含不相关块的段落.
# 为每个块计算数值,用于衡量查询的价值
def calculate_chunk_values(query, chunks, vector_store, irrelevant_chunk_penalty=0.2):
"""
通过结合相关性和位置来计算块的值。
参数:
query (str): 查询文本
chunks (List[str]): 文档块的列表
vector_store (SimpleVectorStore): 包含块的向量存储
irrelevant_chunk_penalty (float): 对不相关块的惩罚
返回:
List[float]: 块值的列表
"""
# 创建查询嵌入
query_embedding = create_embeddings([query])[0]
# 获取所有带有相似度分数的块
num_chunks = len(chunks)
results = vector_store.search(query_embedding, top_k=num_chunks)
# 创建块索引到相关性分数的映射
relevance_scores = {result["metadata"]["chunk_index"]: result["score"] for result in results}
# 计算块值(相关性分数减去惩罚)
chunk_values = []
for i in range(num_chunks):
# 获取相关性分数,如果不在结果中则默认为0.0
score = relevance_scores.get(i, 0.0)
# 计算块值:应用惩罚以转换为值,其中不相关的块具有负值
value = score - irrelevant_chunk_penalty
chunk_values.append(value)
return chunk_values
最优段选择:使用贪心算法找出价值最高的连续区间.
- 约束条件:
- 单段最大长度:
max_segment_length(默认20) - 总长度上限:
total_max_length(默认30) - 段最低价值:
min_segment_value(默认0.2)
- 单段最大长度:
def find_best_segments(chunk_values, max_segment_length=20, total_max_length=30, min_segment_value=0.2):
"""
使用最大子数组和算法的变体找到最佳段。
参数:
chunk_values (List[float]): 每个块的值
max_segment_length (int): 单个段的最大长度
total_max_length (int): 所有段的总长度最大值
min_segment_value (float): 段被考虑的最小值
返回:
List[Tuple[int, int]]: 最佳段的 (起始索引, 结束索引) 列表
"""
print("寻找最优连续文本段...")
best_segments = []
segment_scores = []
total_included_chunks = 0
# 在达到限制之前继续查找段
while total_included_chunks < total_max_length:
best_score = min_segment_value # 段的最低阈值
best_segment = None
# 尝试每个可能的起始位置
for start in range(len(chunk_values)):
# 如果该起始位置已经在选中的段中,则跳过
if any(start >= s[0] and start < s[1] for s in best_segments):
continue
# 尝试每个可能的段长度
for length in range(1, min(max_segment_length, len(chunk_values) - start) + 1):
end = start + length
# 如果结束位置已经在选中的段中,则跳过
if any(end > s[0] and end <= s[1] for s in best_segments):
continue
# 计算段值为块值的总和
segment_value = sum(chunk_values[start:end])
# 如果此段更好则更新最佳段
if segment_value > best_score:
best_score = segment_value
best_segment = (start, end)
# 如果找到了一个好段,则添加它
if best_segment:
best_segments.append(best_segment)
segment_scores.append(best_score)
total_included_chunks += best_segment[1] - best_segment[0]
print(f"Found segment {best_segment} with score {best_score:.4f}")
else:
# 没有更多的好段可以找到
break
# 按段的起始位置排序以提高可读性
best_segments = sorted(best_segments, key=lambda x: x[0])
return best_segments, segment_scores
段落重构:将选块的索引转换成可读文本.
def reconstruct_segments(chunks, best_segments):
"""
根据块索引来重构文本段落。
参数:
chunks (List[str]): 所有文档块的列表
best_segments (List[Tuple[int, int]]): 段落范围的(start, end)索引列表
返回:
List[str]: 重构后的文本段落列表
"""
reconstructed_segments = [] # 初始化一个空列表来存储重构的段落
for start, end in best_segments:
# 将此段落中的块连接起来形成完整的段落文本
segment_text = " ".join(chunks[start:end])
# 将段落文本及其范围追加到reconstructed_segments列表中
reconstructed_segments.append({
"text": segment_text,
"segment_range": (start, end),
})
return reconstructed_segments # 返回重构后的文本段落列表
完整RSE
def rag_with_rse(pdf_path, query, chunk_size=800, irrelevant_chunk_penalty=0.2):
"""
完整的RAG管道与相关段提取。
参数:
pdf_path (str): 文档路径
query (str): 用户查询
chunk_size (int): 每个块的大小
irrelevant_chunk_penalty (float): 对不相关块的惩罚权重
返回:
Dict: 包含查询、段落和回复的结果字典
"""
print("\n=== STARTING RAG WITH RELEVANT SEGMENT EXTRACTION ===")
print(f"Query: {query}")
# 处理文档以提取文本、分块并创建嵌入
chunks, vector_store, doc_info = process_document(pdf_path, chunk_size)
# 根据查询计算相关性分数和块值
print("\nCalculating relevance scores and chunk values...")
chunk_values = calculate_chunk_values(query, chunks, vector_store, irrelevant_chunk_penalty)
# 基于块值找到最佳的文本段落
best_segments, scores = find_best_segments(
chunk_values,
max_segment_length=20,
total_max_length=30,
min_segment_value=0.2
)
# 从最佳块中重构文本段落
print("\nReconstructing text segments from chunks...")
segments = reconstruct_segments(chunks, best_segments)
# 将段落格式化为语言模型的上下文字符串
context = format_segments_for_context(segments)
# 使用上下文生成语言模型的回复
response = generate_response(query, context)
# 将结果编译为字典
result = {
"query": query,
"segments": segments,
"response": response
}
print("\n=== FINAL RESPONSE ===")
print(response)
return result
上下文压缩
概念
我们将过滤并压缩检索到的文本块,仅保留最相关的内容,从而减少噪声并提升回复质量。主流方案是将检索到的文档传递给LLM,LLM根据查询判断哪些内容相关冗余,返回精简后的相关信息.
实现
上下文压缩:
def compress_chunk(chunk, query, compression_type="selective", model="gpt-3.5-turbo"):
"""
压缩从检索中获取的文本块,仅保留与查询相关的内容。
参数:
chunk (str): 要压缩的文本块
query (str): 用户查询
"选择性压缩" "总结性压缩" "抽取性压缩"
compression_type (str): 压缩类型("selective"、"summary" 或 "extraction")
model (str): 使用的LLM模型
返回:
str: 压缩后的文本块
"""
# 定义不同压缩方法的系统提示
if compression_type == "selective":
system_prompt = """你是一位信息过滤专家。你的任务是分析文档片段,并仅提取与用户查询直接相关的句子或段落。删除所有不相关的内容。
你的输出应当:
1. 仅包含有助于回答查询的文本
2. 保留相关句子的原始措辞(不要改写)
3. 保持文本的原始顺序
4. 包含所有相关内容,即使看起来冗余
5. 排除任何不相关的文本
请将你的回答格式化为纯文本,不要添加额外的评论。"""
elif compression_type == "summary":
system_prompt = """你是一位摘要专家。你的任务是创建一个简洁的摘要,专注于与用户查询相关的信息。
你的输出应当:
1. 简明扼要地涵盖与查询相关的信息
2. 仅关注与查询相关的信息
3. 省略不相关的细节
4. 以中立、事实的语气撰写
请将你的回答格式化为纯文本,不要添加额外的评论。"""
else: # extraction
system_prompt = """你是一位信息提取专家。你的任务是从文档片段中仅提取包含与用户查询相关信息的句子。
你的输出应当:
1. 仅包含原文中与查询直接相关的句子
2. 保留原始措辞(不要修改文本)
3. 仅包含与查询直接相关的句子
4. 用换行符分隔提取的句子
5. 不要添加任何评论或额外文本
请将你的回答格式化为纯文本,不要添加额外的评论。"""
# 定义带有查询和文档片段的用户提示
user_prompt = f"""
查询: {query}
文档片段:
{chunk}
仅提取与回答此查询相关的内容。
"""
# 使用OpenAI API生成回复
response = client.chat.completions.create(
model=model,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
],
temperature=0
)
# 从回复中提取压缩后的文本块
compressed_chunk = response.choices[0].message.content.strip()
# 计算压缩率
original_length = len(chunk)
compressed_length = len(compressed_chunk)
compression_ratio = (original_length - compressed_length) / original_length * 100
return compressed_chunk, compression_ratio
批量压缩块:
def batch_compress_chunks(chunks, query, compression_type="selective", model="gpt-3.5-turbo"):
"""
分别压缩多个片段。
参数:
chunks (List[str]): 待压缩的文本片段列表
query (str): 用户查询
compression_type (str): 压缩类型("selective"、"summary" 或 "extraction")
model (str): 使用的LLM模型
返回:
List[Tuple[str, float]]: 包含压缩片段及其压缩比率的列表
"""
print(f"Compressing {len(chunks)} chunks...") # 打印即将压缩的片段数量
results = [] # 初始化一个空列表来存储结果
total_original_length = 0 # 初始化变量以存储原始片段的总长度
total_compressed_length = 0 # 初始化变量以存储压缩片段的总长度
# 遍历每个片段
for i, chunk in enumerate(chunks):
print(f"Compressing chunk {i+1}/{len(chunks)}...") # 打印压缩进度
# 压缩片段并获取压缩后的片段和压缩比率
compressed_chunk, compression_ratio = compress_chunk(chunk, query, compression_type, model)
results.append((compressed_chunk, compression_ratio)) # 将结果追加到结果列表中
total_original_length += len(chunk) # 将原始片段的长度累加到总原始长度
total_compressed_length += len(compressed_chunk) # 将压缩片段的长度累加到总压缩长度
# 计算总体压缩比率
overall_ratio = (total_original_length - total_compressed_length) / total_original_length * 100
print(f"Overall compression ratio: {overall_ratio:.2f}%") # 打印总体压缩比率
return results # 返回包含压缩片段及其压缩比率的列表
RAG反馈回路机制
概念
传统的RAG系统是静态的——它们仅根据嵌入相似性检索信息。通过引入反馈回路,创建一个动态系统,该系统:
- 记录下哪些方法有效(以及哪些无效)
- 随着时间调整文档的相关性分数
- 将成功的问答对纳入其知识库
- 每次与用户交互后变得更智能
实现
分馈系统组件
def get_user_feedback(query, response, relevance, quality, comments=""):
"""
格式化用户反馈为字典。
参数:
query (str): 用户的查询
response (str): 系统的回复
relevance (int): 相关性评分 (1-5)
quality (int): 质量评分 (1-5)
comments (str): 可选的反馈评论
返回:
Dict: 格式化的反馈
"""
return {
"query": query,
"response": response,
"relevance": int(relevance),
"quality": int(quality),
"comments": comments,
"timestamp": datetime.now().isoformat()
}
def store_feedback(feedback, feedback_file="feedback_data.json"):
"""
将反馈存储到JSON文件中。
参数:
feedback (Dict): 反馈数据
feedback_file (str): 反馈文件路径
"""
with open(feedback_file, "a") as f:
json.dump(feedback, f)
f.write("\n")
def load_feedback_data(feedback_file="feedback_data.json"):
"""
从文件中加载反馈数据。
参数:
feedback_file (str): 反馈文件的路径
返回:
List[Dict]: 反馈条目的列表
"""
feedback_data = []
try:
with open(feedback_file, "r") as f:
for line in f:
if line.strip():
feedback_data.append(json.loads(line.strip()))
except FileNotFoundError:
print("No feedback data file found. Starting with empty feedback.")
return feedback_data
def assess_feedback_relevance(query, doc_text, feedback):
"""
使用LLM评估过去的反馈是否与当前查询和文档相关。
此函数帮助确定哪些过去的反馈应该影响当前的检索,通过向LLM发送当前查询、过去查询+反馈以及文档内容,
来进行相关性评估。
参数:
query (str): 当前用户需要信息检索的查询
doc_text (str): 正在评估的文档文本内容
feedback (Dict): 包含'query'和'response'键的过去反馈数据
返回:
bool: 如果反馈被认为与当前查询/文档相关,则返回True,否则返回False
"""
# 定义系统提示,指示LLM仅进行二元相关性判断
system_prompt = """你是一个AI系统,用于判断过去的反馈是否与当前查询和文档相关。
仅回答“是”或“否”。你的任务仅限于判断相关性,不提供解释。"""
# 构造用户提示,包含当前查询、过去反馈数据和截断的文档内容
user_prompt = f"""
当前查询: {query}
收到反馈的过去查询: {feedback['query']}
文档内容: {doc_text[:500]}... [已截断]
收到反馈的过去回复: {feedback['response'][:500]}... [已截断]
这个过去的反馈是否与当前查询和文档相关?(是/否)
"""
# 调用LLM API,使用零温度以确保确定性输出
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
],
temperature=0 # 使用temperature=0以获得一致且确定性的回复
)
# 提取并规范化回复以确定相关性
answer = response.choices[0].message.content.strip().lower()
return '是' in answer # 如果回答中包含“是”,则返回True
def adjust_relevance_scores(query, results, feedback_data):
"""
根据历史反馈调整文档相关性得分,以提高检索质量。
此函数分析过去的用户反馈,动态调整检索到的文档的相关性得分。它识别与当前查询上下文相关的反馈,
根据相关性评分计算得分修正值,并相应地重新排序结果。
参数:
query (str): 当前用户的查询
results (List[Dict]): 带有原始相似度得分的检索到的文档
feedback_data (List[Dict]): 包含用户评分的历史反馈数据
返回:
List[Dict]: 调整后相关性得分的结果,按新的得分排序
"""
# 如果没有可用的反馈数据,返回原始结果不变
if not feedback_data:
return results
print("Adjusting relevance scores based on feedback history...")
# 处理每个检索到的文档
for i, result in enumerate(results):
document_text = result["text"]
relevant_feedback = []
# 通过查询语言模型来评估每条历史反馈的相关性,找到与此特定文档和查询组合相关的反馈
for feedback in feedback_data:
is_relevant = assess_feedback_relevance(query, document_text, feedback)
if is_relevant:
relevant_feedback.append(feedback)
# 如果存在相关的反馈,则应用得分调整
if relevant_feedback:
# 计算所有适用反馈条目的平均相关性评分
# 反馈相关性评分范围为1-5(1表示不相关,5表示高度相关)
avg_relevance = sum(f['relevance'] for f in relevant_feedback) / len(relevant_feedback)
# 将平均相关性转换为0.5-1.5范围内的得分修正值
# - 低于3/5的评分会降低原始相似度(修正值<1.0)
# - 高于3/5的评分会增加原始相似度(修正值>1.0)
modifier = 0.5 + (avg_relevance / 5.0)
# 将修正值应用于原始相似度得分
original_score = result["similarity"]
adjusted_score = original_score * modifier
# 更新结果字典中的新得分和反馈元数据
result["original_similarity"] = original_score # 保留原始得分
result["similarity"] = adjusted_score # 更新主要得分
result["relevance_score"] = adjusted_score # 更新相关性得分
result["feedback_applied"] = True # 标记已应用反馈
result["feedback_count"] = len(relevant_feedback) # 使用的反馈条目数量
# 记录调整详情
print(f" Document {i+1}: Adjusted score from {original_score:.4f} to {adjusted_score:.4f} based on {len(relevant_feedback)} feedback(s)")
# 按调整后的得分重新排序结果,确保更高质量的匹配项排在前面
results.sort(key=lambda x: x["similarity"], reverse=True)
return results
# 步骤4.1:用户反馈界面
print("Rate relevance (1-5, with 5 being most relevant):")
relevance = input() # 用户输入:5
print("Rate quality (1-5, with 5 being highest quality):")
quality = input() # 用户输入:4
# 步骤4.2:结构化存储反馈
feedback = {
"query": "中国政府对AI大模型发展有哪些支持政策?",
"response": "根据上下文,中国政府的支持政策包括...",
"relevance": 5,
"quality": 4,
"comments": "回答很全面,但缺少具体时间节点",
"timestamp": "2026-06-02T10:30:00"
}
# 步骤4.3:持久化到文件
store_feedback(feedback, "feedback_data.json")
def fine_tune_index(current_store, chunks, feedback_data):
# 步骤5.1:筛选高质量反馈(评分≥4)
good_feedback = [f for f in feedback_data
if f['relevance'] >= 4 and f['quality'] >= 4]
# 步骤5.2:为每条高质量反馈创建合成文档
for feedback in good_feedback:
# 将"问题+答案"拼接成新文档
enhanced_text = f"Question: {feedback['query']}\nAnswer: {feedback['response']}"
# 生成嵌入
embedding = create_embeddings(enhanced_text)
# 步骤5.3:添加到向量库,给予更高权重
new_store.add_item(
text=enhanced_text,
embedding=embedding,
metadata={
"type": "feedback_enhanced",
"query": feedback['query'],
"relevance_score": 1.2, # 高初始权重
"feedback_count": 1
}
)
return new_store # 向量库大小从N变成N+M
自适应检索
概念
根据查询类型动态选择最合适的检索策略。这种方法显著提升了RAG系统在各种问题上提供准确且相关回复的能力。不同的问题需要不同的检索策略。
- 对查询类型进行分类(事实性、分析性、观点性或情境性)
- 选择适当的检索策略
- 执行专门的检索技术
- 生成定制化的回复
流程:
1. 拆解问题,生成多个视角
流程开始,系统收到一个查询,比如“人们对于远程办公的看法”。它不会直接去搜索,而是先去问一个大语言模型(GPT-4o)。
它会发给AI这样一个指令:
“你是一位观点分析专家。对于‘人们对于远程办公的看法’这个话题,请识别出恰好3个不同的观点角度。”
AI收到指令后,会进行分析并返回结果,例如:
支持远程办公,认为其灵活高效
反对远程办公,认为其影响协作和公司文化
持中立态度,认为其有利有弊,关键在于如何管理
至此,原始的一个问题,被拆解成了三个指向明确的子问题。
- 针对每个视角生成新的搜索指令
得到这三个视角后,系统不会直接用原问题去搜索,而是将每个视角和原始问题结合起来,形成新的搜索指令。
例如,针对第一个视角,它会组合成:“人们对于远程办公的看法 支持远程办公,认为其灵活高效”。这个组合后的句子,比原始问题更具体,搜索目标更明确。
- 分头行动,多路检索
有了三个新的搜索指令后,系统开始分头行动:
向量化:将这三个组合后的句子,分别转换成计算机能理解的向量(一串数字)。
独立搜索:拿着这三个向量,分别去知识库里进行相似性搜索。并且,为每个视角都指定要找2篇最相关的文档。
打标签:找到的每一篇文档,都会被贴上一个标签,注明它是根据哪个“视角”找到的。
这样一轮下来,系统手里就有了最多 3个视角 × 2篇文档 = 6篇候选文档,它们分别代表了不同的立场。
- 择优录取,保证多样性
现在,系统要从这6篇候选文档中,最终挑出用户指定的文档数量(比如参数k=4,就是只要4篇)。
挑选策略很讲究“多样性优先”:
优先保底:首先,它会遍历第一步生成的三个视角。对于每个视角,都至少选一篇最能代表它的文档。这一步确保了最终结果里,三种立场的声音都会出现。
择优补全:如果保底选完之后,数量还没凑够4篇,它就会从剩下的候选文档里挑。这次不看立场了,而是纯粹看“谁和搜索指令的相似度最高”,按分数从高到低补齐。
- 输出结果
流程结束。系统把筛选出来、既多样又相关的这最多4篇文档,作为最终结果返回。这些文档为后续AI生成一个观点平衡、不偏激的答案,提供了坚实的事实基础。
实现
分类查询: 得出事实性、分析性、观点性或情境性四个类别之一.
def classify_query(query, model="gpt-4o"):
"""
将查询分类为四个类别之一:事实性、分析性、观点性或情境性。
参数:
query (str): 用户查询
model (str): 要使用的LLM模型
返回:
str: 查询类别
"""
# 定义系统提示以指导AI的分类
system_prompt = """你是一位问题分类专家。
请将给定的查询准确分类为以下类别之一:
- 事实性:寻求具体、可验证信息的查询。
- 分析性:需要全面分析或解释的查询。
- 观点性:关于主观事物或寻求不同观点的查询。
- 情境性:依赖于用户特定上下文的查询。
仅返回类别名称,不要包含任何解释或额外文本。
"""
# 创建带有待分类查询的用户提示
user_prompt = f"请对这个查询进行分类:{query}"
# 从AI模型生成分类回复
response = client.chat.completions.create(
model=model,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
],
temperature=0
)
# 提取并清理回复中的类别
category = response.choices[0].message.content.strip()
# 定义有效的类别列表
valid_categories = ["事实性", "分析性", "观点性", "情境性"]
# 确保返回的类别有效
for valid in valid_categories:
if valid in category:
return valid
# 如果分类失败,默认返回"事实性"
return "事实性"
事实性
def factual_retrieval_strategy(query, vector_store, k=4):
"""
针对精确性的情报查询检索策略。
参数:
query (str): 用户查询
vector_store (SimpleVectorStore): 向量存储
k (int): 返回的文档数量
返回:
List[Dict]: 检索到的文档
"""
print(f"执行针对查询的精确检索策略: '{query}'")
# 使用LLM增强查询以提高精确性
system_prompt = """你是一位搜索查询优化专家。
你的任务是重新构造给定的事实性查询,使其在信息检索时更加精确和具体。
重点关注关键实体及其之间的关系。
仅提供优化后的查询,不要包含任何解释。
"""
user_prompt = f"Enhance this factual query: {query}"
# 使用LLM生成增强后的查询
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
],
temperature=0
)
# 提取并打印增强后的查询
enhanced_query = response.choices[0].message.content.strip()
print(f"Enhanced query: {enhanced_query}")
# 为增强后的查询创建嵌入向量
query_embedding = create_embeddings(enhanced_query)
# 执行初始相似性搜索以检索文档
initial_results = vector_store.similarity_search(query_embedding, k=k*2)
# 初始化一个列表用于存储排名结果
ranked_results = []
# 使用LLM对文档的相关性进行评分和排名
for doc in initial_results:
relevance_score = score_document_relevance(enhanced_query, doc["text"])
ranked_results.append({
"text": doc["text"],
"metadata": doc["metadata"],
"similarity": doc["similarity"],
"relevance_score": relevance_score
})
# 按相关性分数降序排序结果
ranked_results.sort(key=lambda x: x["relevance_score"], reverse=True)
# 返回前k个结果
return ranked_results[:k]
分析类
def analytical_retrieval_strategy(query, vector_store, k=4):
"""
针对分析型查询的检索策略,重点关注全面覆盖。
参数:
query (str): 用户查询
vector_store (SimpleVectorStore): 向量存储
k (int): 返回的文档数量
返回:
List[Dict]: 检索到的文档
"""
print(f"Executing Analytical retrieval strategy for: '{query}'")
# 定义系统提示,引导AI生成子问题
system_prompt = """你是一位复杂问题分解专家。
生成探索主要分析性查询不同方面的子问题。
这些子问题应该涵盖主题的广度,有助于检索全面的信息。
返回恰好3个子问题,每行一个。
"""
# 创建带有主查询的用户提示
user_prompt = f"Generate sub-questions for this analytical query: {query}"
# 使用LLM生成子问题
response = client.chat.completions.create(
model="gpt-3.5-turbo",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
],
temperature=0.3
)
# 提取并清理子问题
sub_queries = response.choices[0].message.content.strip().split('\n')
sub_queries = [q.strip() for q in sub_queries if q.strip()]
print(f"Generated sub-queries: {sub_queries}")
# 为每个子问题检索文档
all_results = []
for sub_query in sub_queries:
# 为子问题创建嵌入
sub_query_embedding = create_embeddings(sub_query)
# 对子问题执行相似性搜索
results = vector_store.similarity_search(sub_query_embedding, k=2)
all_results.extend(results)
# 确保多样性,从不同子问题的结果中选择
# 去除重复项(相同文本内容)
unique_texts = set()
diverse_results = []
for result in all_results:
if result["text"] not in unique_texts:
unique_texts.add(result["text"])
diverse_results.append(result)
# 如果需要更多结果以达到k,从初始结果中添加更多
if len(diverse_results) < k:
# 直接检索主要查询
main_query_embedding = create_embeddings(query)
main_results = vector_store.similarity_search(main_query_embedding, k=k)
for result in main_results:
if result["text"] not in unique_texts and len(diverse_results) < k:
unique_texts.add(result["text"])
diverse_results.append(result)
# 返回前k个多样化结果
return diverse_results[:k]
观点类
def opinion_retrieval_strategy(query, vector_store, k=4):
"""
针对意见查询的检索策略,重点关注多样化视角。
参数:
query (str): 用户查询
vector_store (SimpleVectorStore): 向量存储
k (int): 返回的文档数量
返回:
List[Dict]: 检索到的文档
"""
print(f"执行观点检索策略,查询:'{query}'")
# 定义系统提示,用于指导AI识别不同视角
system_prompt = """你是一位观点分析专家。
对于给定的关于观点或视角的查询,识别人们可能对这个话题持有的不同观点。
返回恰好3个不同的观点角度,每行一个。
"""
# 创建包含主要查询的用户提示
user_prompt = f"识别以下话题的不同观点:{query}"
# 使用LLM生成不同的视角
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
],
temperature=0.3
)
# 提取并清理视角
viewpoints = response.choices[0].message.content.strip().split('\n')
viewpoints = [v.strip() for v in viewpoints if v.strip()]
print(f"已识别的观点:{viewpoints}")
# 检索代表每个视角的文档
all_results = []
for viewpoint in viewpoints:
# 将主查询与视角结合
combined_query = f"{query} {viewpoint}"
# 为组合查询创建嵌入
viewpoint_embedding = create_embeddings(combined_query)
# 对组合查询进行相似性搜索
results = vector_store.similarity_search(viewpoint_embedding, k=2)
# 标记结果表示的视角
for result in results:
result["viewpoint"] = viewpoint
# 将结果添加到所有结果列表中
all_results.extend(results)
# 选择多样化的意见范围
# 如果可能,确保从每个视角获取至少一份文档
selected_results = []
for viewpoint in viewpoints:
# 过滤由视角表示的文档
viewpoint_docs = [r for r in all_results if r.get("viewpoint") == viewpoint]
if viewpoint_docs:
selected_results.append(viewpoint_docs[0])
# 填充剩余的槽位,使用相似度最高的文档
remaining_slots = k - len(selected_results)
if remaining_slots > 0:
# 按相似度对剩余文档排序
remaining_docs = [r for r in all_results if r not in selected_results]
remaining_docs.sort(key=lambda x: x["similarity"], reverse=True)
selected_results.extend(remaining_docs[:remaining_slots])
# 返回前k个结果
return selected_results[:k]
情景类
def contextual_retrieval_strategy(query, vector_store, k=4, user_context=None):
"""
针对上下文查询的检索策略,集成用户上下文。
参数:
query (str): 用户查询
vector_store (SimpleVectorStore): 向量存储
k (int): 返回的文档数量
user_context (str): 额外的用户上下文
返回:
List[Dict]: 检索到的文档
"""
print(f"执行上下文检索策略,查询为: '{query}'")
# 如果未提供用户上下文,则尝试从查询中推断
if not user_context:
system_prompt = """你是一位理解问题隐含上下文的专家。
对于给定的查询,推断可能相关或隐含但未明确说明的上下文信息。
重点关注哪些背景信息有助于回答这个查询。
返回对隐含上下文的简要描述。"""
user_prompt = f"推断这个查询中的隐含上下文:{query}"
# 使用LLM生成推断的上下文
response = client.chat.completions.create(
model="gpt-3.5-turbo",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
],
temperature=0.1
)
# 提取并打印推断的上下文
user_context = response.choices[0].message.content.strip()
print(f"推断的上下文:{user_context}")
# 根据上下文重构查询
system_prompt = """你是一位结合上下文重构问题的专家。
根据查询和上下文信息,创建一个更具体的查询,
将上下文整合进去以获得更相关的信息。
仅返回重构后的查询,不要包含解释。"""
user_prompt = f"""
查询:{query}
上下文:{user_context}
请重构查询以整合这个上下文:"""
# 使用LLM生成结合上下文的查询
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
],
temperature=0
)
# 提取并打印结合上下文的查询
contextualized_query = response.choices[0].message.content.strip()
print(f"结合上下文的查询:{contextualized_query}")
# 基于结合上下文的查询检索文档
query_embedding = create_embeddings(contextualized_query)
initial_results = vector_store.similarity_search(query_embedding, k=k*2)
# 考虑相关性和用户上下文对文档进行排名
ranked_results = []
for doc in initial_results:
# 根据上下文评分文档的相关性
context_relevance = score_document_context_relevance(query, user_context, doc["text"])
ranked_results.append({
"text": doc["text"],
"metadata": doc["metadata"],
"similarity": doc["similarity"],
"context_relevance": context_relevance
})
# 按上下文相关性排序并返回前k个结果
ranked_results.sort(key=lambda x: x["context_relevance"], reverse=True)
return ranked_results[:k]
相关性评分
def score_document_context_relevance(query, context, document, model="gpt-4o"):
"""
根据查询和上下文对文档的相关性进行评分。
参数:
query (str): 用户查询
context (str): 用户上下文
document (str): 文档文本
model (str): 大语言模型
返回:
float: 相关性评分,范围从 0 到 10
"""
# 系统提示,用于指导模型如何根据上下文评估相关性
system_prompt = """你是一位考虑上下文的文档相关性评估专家。
根据提供的上下文,对文档与查询的相关性进行0到10分的评分,其中:
0分 = 完全不相关
10分 = 在给定上下文中完美回答查询
仅返回0到10之间的数值分数,不要包含其他内容。
"""
# 如果文档过长,则截断
doc_preview = document[:1500] + "..." if len(document) > 1500 else document
# 用户提示,包含查询、上下文和文档预览
user_prompt = f"""
查询:{query}
上下文:{context}
文档:{doc_preview}
考虑上下文的相关性评分(0-10分):
"""
# 生成模型回复
response = client.chat.completions.create(
model=model,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
],
temperature=0
)
# 从模型回复中提取评分文本
score_text = response.choices[0].message.content.strip()
# 使用正则表达式提取数值分数
match = re.search(r'(\d+(\.\d+)?)', score_text)
if match:
score = float(match.group(1))
return min(10, max(0, score)) # 确保评分在 0 到 10 之间
else:
# 如果提取失败,则返回默认评分
return 5.0
自适应检索器
def adaptive_retrieval(query, vector_store, k=4, user_context=None):
"""
根据适当的策略执行自适应检索。
参数:
query (str): 用户查询
vector_store (SimpleVectorStore): 向量存储
k (int): 要检索的文档数量
user_context (str): 可选的用户上下文,用于上下文查询
返回:
List[Dict]: 检索到的文档
"""
# 对查询进行分类以确定其类型
query_type = classify_query(query)
print(f"Query classified as: {query_type}")
# 根据查询类型选择并执行适当的检索策略
if query_type == "Factual":
# 使用事实检索策略获取精确信息
results = factual_retrieval_strategy(query, vector_store, k)
elif query_type == "Analytical":
# 使用分析检索策略获取全面覆盖
results = analytical_retrieval_strategy(query, vector_store, k)
elif query_type == "Opinion":
# 使用观点检索策略获取多样化视角
results = opinion_retrieval_strategy(query, vector_store, k)
elif query_type == "Contextual":
# 使用上下文检索策略,结合用户上下文
results = contextual_retrieval_strategy(query, vector_store, k, user_context)
else:
# 如果分类失败,默认使用事实检索策略
results = factual_retrieval_strategy(query, vector_store, k)
return results # 返回检索到的文档
def generate_response(query, results, query_type, model="gpt-4o"):
"""
根据查询、检索到的文档和查询类型生成回复。
参数:
query (str): 用户查询
results (List[Dict]): 检索到的文档
query_type (str): 查询类型
model (str): 大语言模型
返回:
str: 生成的回复
"""
# 通过连接检索到的文档的文本并用分隔符分隔来准备上下文
context = "\n\n---\n\n".join([r["text"] for r in results])
# 根据查询类型创建自定义系统提示
if query_type == "事实性":
system_prompt = """你是一位提供事实信息的助手。
根据提供的上下文回答问题。注重准确性和精确性。
如果上下文中没有所需信息,请说明信息的局限性。"""
elif query_type == "分析性":
system_prompt = """你是一位提供分析见解的助手。
基于提供的上下文,对主题进行全面分析。
在解释中涵盖不同的方面和视角。
如果上下文有信息缺口,在提供最佳分析的同时说明这些局限。"""
elif query_type == "观点性":
system_prompt = """你是一位讨论多视角话题的助手。
基于提供的上下文,呈现对该主题的不同观点。
确保公平地展示多样化的观点,不显示偏见。
如果上下文中的观点有限,请说明这一点。"""
elif query_type == "情境性":
system_prompt = """你是一位提供与情境相关信息的助手。
在回答问题时同时考虑查询及其上下文。
建立查询情境与提供文档中信息之间的联系。
如果上下文未能完全覆盖特定情况,请说明局限性。"""
else:
system_prompt = """你是一位助手。根据提供的上下文回答问题。如果无法从上下文中找到答案,请说明局限性。"""
# 创建用户提示,通过结合上下文和查询
user_prompt = f"""
上下文:
{context}
问题:{query}
请根据上下文提供有帮助的回答。
"""
# 使用OpenAI客户端生成回复
response = client.chat.completions.create(
model=model,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
],
temperature=0.2
)
# 返回生成的回复内容
return response.choices[0].message.content
命题分块
概念
命题分块——一种先进的技术,用于将文档分解为原子化的、事实性的陈述,以实现更准确的检索。与传统分块方法不同,传统方法只是通过字符数量来分割文本,命题分块能够保留各个事实的语义完整性。
命题分块通过以下方式提供更精确的检索:
- 将内容分解为原子化且自包含的事实
- 创建更小、更细粒度的单位以供检索
- 实现查询与相关内容之间的更精确匹配
- 过滤掉低质量或不完整的命题
实现
命题生成
def generate_propositions(chunk):
"""
从文本片段中生成原子化的、自包含的命题。
参数:
chunk (Dict): 包含内容和元数据的文本片段
返回:
List[str]: 生成的命题列表
"""
# 系统提示,用于指导AI如何生成命题
system_prompt = """请将下列文本拆解为简单、自包含的命题。每条命题需满足以下要求:
1. 只表达一个具体事实:每条命题只陈述一个具体事实或观点。
2. 独立可理解:命题应自包含,无需额外上下文即可理解。
3. 使用全称而非代词:避免使用代词或模糊指代,直接用实体全称。
4. 包含必要的时间/限定词:如有必要,需包含时间、条件等限定信息,使事实更精确。
5. 只包含一个主谓关系:聚焦于单一主语及其动作或属性,避免并列或复合句。
只输出命题列表,不要包含任何解释或额外文本。"""
# 用户提示,包含需要转换为命题的文本片段
user_prompt = f"需要拆解为命题的文本:\n\n{chunk['text']}"
# 从模型生成回复
response = client.chat.completions.create(
model="gpt-4o", # 使用更强的模型以确保命题生成的准确性
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
],
temperature=0
)
# 从回复中提取命题
raw_propositions = response.choices[0].message.content.strip().split('\n')
# 清理命题(移除编号、项目符号等)
clean_propositions = []
for prop in raw_propositions:
# 移除编号(如1.、2.等)和项目符号
cleaned = re.sub(r'^\s*(\d+\.|\-|\*)\s*', '', prop).strip()
if cleaned and len(cleaned) > 10: # 简单过滤空命题或非常短的命题
clean_propositions.append(cleaned)
return clean_propositions
多模态RAG与图像描述
概念
多模态RAG系统,该系统从文档中提取文本和图像,为图像生成描述,并使用这两种内容类型来回复查询。这种方法通过将视觉信息纳入知识库中增强了传统的RAG。
传统的RAG系统仅处理文本,但许多文档包含以图像、图表和表格形式存在的关键信息。通过为这些视觉元素添加描述并将它们纳入检索系统,我们可以:
- 访问图表和示意图中锁定的信息
- 理解补充文本的表格和图表
- 构建更全面的知识库
- 回答依赖于视觉数据的问题
实现
文档处理功能
def extract_content_from_pdf(pdf_path, output_dir=None):
"""
从PDF文件中提取文本和图像。
参数:
pdf_path (str): PDF文件的路径
output_dir (str, 可选): 提取的图像保存的目录
返回:
Tuple[List[Dict], List[Dict]]: 文本数据和图像数据
"""
# 如果未提供输出目录,则创建一个临时目录用于存储图像
temp_dir = None
if output_dir is None:
temp_dir = tempfile.mkdtemp()
output_dir = temp_dir
else:
os.makedirs(output_dir, exist_ok=True)
text_data = [] # 用于存储提取的文本数据的列表
image_paths = [] # 用于存储提取的图像路径的列表
print(f"Extracting content from {pdf_path}...")
try:
with fitz.open(pdf_path) as pdf_file:
# 遍历PDF中的每一页
for page_number in range(len(pdf_file)):
page = pdf_file[page_number]
# 从页面提取文本
text = page.get_text().strip()
if text:
text_data.append({
"content": text,
"metadata": {
"source": pdf_path,
"page": page_number + 1,
"type": "text"
}
})
# 从页面提取图像
image_list = page.get_images(full=True)
for img_index, img in enumerate(image_list):
xref = img[0] # 图像的XREF
base_image = pdf_file.extract_image(xref)
if base_image:
image_bytes = base_image["image"]
image_ext = base_image["ext"]
# 将图像保存到输出目录
img_filename = f"page_{page_number+1}_img_{img_index+1}.{image_ext}"
img_path = os.path.join(output_dir, img_filename)
with open(img_path, "wb") as img_file:
img_file.write(image_bytes)
image_paths.append({
"path": img_path,
"metadata": {
"source": pdf_path,
"page": page_number + 1,
"image_index": img_index + 1,
"type": "image"
}
})
print(f"Extracted {len(text_data)} text segments and {len(image_paths)} images")
return text_data, image_paths
except Exception as e:
print(f"Error extracting content: {e}")
if temp_dir and os.path.exists(temp_dir):
shutil.rmtree(temp_dir)
raise
分块文本内容
def chunk_text(text_data, chunk_size=1000, overlap=200):
"""
将文本数据分割为重叠的块。
参数:
text_data (List[Dict]): 从PDF中提取的文本数据
chunk_size (int): 每个块的字符数大小
overlap (int): 块之间的重叠字符数
返回:
List[Dict]: 分块后的文本数据
"""
chunked_data = [] # 初始化一个空列表来存储分块数据
for item in text_data:
text = item["content"] # 提取文本内容
metadata = item["metadata"] # 提取元数据
# 如果文本太短,则跳过并保留原始数据
if len(text) < chunk_size / 2:
chunked_data.append({
"content": text,
"metadata": metadata
})
continue
# 创建带重叠的块
chunks = []
for i in range(0, len(text), chunk_size - overlap):
chunk = text[i:i + chunk_size] # 提取指定大小的块
if chunk: # 确保不添加空块
chunks.append(chunk)
# 添加每个块并更新元数据
for i, chunk in enumerate(chunks):
chunk_metadata = metadata.copy() # 复制原始元数据
chunk_metadata["chunk_index"] = i # 在元数据中添加块索引
chunk_metadata["chunk_count"] = len(chunks) # 在元数据中添加总块数
chunked_data.append({
"content": chunk, # 块文本
"metadata": chunk_metadata # 更新后的元数据
})
print(f"Created {len(chunked_data)} text chunks") # 打印创建的块的数量
return chunked_data # 返回分块后的数据列表
AI描述图片内容
def encode_image(image_path):
"""
将图像文件编码为base64。
参数:
image_path (str): 图像文件的路径
返回:
str: base64编码的图像
"""
# 以二进制读取模式打开图像文件
with open(image_path, "rb") as image_file:
# 读取图像文件并将其编码为base64
encoded_image = base64.b64encode(image_file.read())
# 将base64字节解码为字符串并返回
return encoded_image.decode('utf-8')
def generate_image_caption(image_path):
"""
使用OpenAI的视觉能力为图像生成描述。
参数:
image_path (str): 图像文件的路径
返回:
str: 生成的描述
"""
# 检查文件是否存在并且是图像
if not os.path.exists(image_path):
return "Error: Image file not found"
try:
# 打开并验证图像
Image.open(image_path)
# 将图像编码为base64格式
base64_image = encode_image(image_path)
# 创建API请求以生成描述
response = client.chat.completions.create(
model="llava-hf/llava-1.5-7b-hf", # 使用llava-1.5-7b模型
messages=[
{
"role": "system",
"content": "You are an assistant specialized in describing images from academic papers. "
"Provide detailed captions for the image that capture key information. "
"If the image contains charts, tables, or diagrams, describe their content and purpose clearly. "
"Your caption should be optimized for future retrieval when people ask questions about this content."
},
{
"role": "user",
"content": [
{"type": "text", "text": "Describe this image in detail, focusing on its academic content:"},
{
"type": "image_url",
"image_url": {
"url": f"data:image/jpeg;base64,{base64_image}"
}
}
]
}
],
max_tokens=300
)
# 从回复中提取描述
caption = response.choices[0].message.content
return caption
except Exception as e:
# 如果发生异常,返回错误消息
return f"Error generating caption: {str(e)}"
融合检索:向量搜索和关键词搜索
概念
融合检索系统,该系统结合了语义向量搜索和基于关键词的BM25检索的优势。这种方法通过捕获概念相似性和精确关键词匹配,提升了检索质量。
传统的RAG(Retrieval-Augmented Generation)系统通常仅依赖于向量搜索,但这种方法存在局限性:
- 向量搜索在语义相似性方面表现出色,但可能会遗漏精确的关键词匹配
- 关键词搜索对于特定术语非常有用,但缺乏语义理解能力
- 不同查询在不同的检索方法上表现更佳
融合检索通过以下方式提供了两者的最佳组合:
- 执行基于向量和基于关键词的检索
- 对每种方法的得分进行归一化处理
- 使用加权公式将它们结合起来
- 根据综合得分对文档进行排名
实现
添加库
uv add rank-bm25
添加BM25索引
def create_bm25_index(chunks):
"""
根据给定的片段创建BM25索引。
参数:
chunks (List[Dict]): 文本片段的列表
返回:
BM25Okapi: 一个BM25索引
"""
# 从每个片段中提取文本
texts = [chunk["text"] for chunk in chunks]
# 将每个文档按空格拆分为标记
tokenized_docs = [text.split() for text in texts]
# 使用标记化的文档创建BM25索引
bm25 = BM25Okapi(tokenized_docs)
# 打印BM25索引中文档的数量
print(f"Created BM25 index with {len(texts)} documents")
return bm25
索引查询
def bm25_search(bm25, chunks, query, k=5):
"""
使用查询搜索BM25索引。
参数:
bm25 (BM25Okapi): BM25索引
chunks (List[Dict]): 文本块列表
query (str): 查询字符串
k (int): 返回的结果数量
返回:
List[Dict]: 带有分数的前k个结果
"""
# 将查询按空格分割为单词列表
query_tokens = query.split()
# 获取查询词对已索引文档的BM25得分
scores = bm25.get_scores(query_tokens)
# 初始化一个空列表来存储带分数的结果
results = []
# 遍历得分和对应的文本块
for i, score in enumerate(scores):
# 复制元数据以避免修改原始数据
metadata = chunks[i].get("metadata", {}).copy()
# 将索引添加到元数据中
metadata["index"] = i
results.append({
"text": chunks[i]["text"], # 文本内容
"metadata": metadata, # 包含索引的元数据
"bm25_score": float(score) # BM25得分
})
# 按BM25得分降序排序结果
results.sort(key=lambda x: x["bm25_score"], reverse=True)
# 返回前k个结果
return results[:k]
融合检索函数
def fusion_retrieval(query, chunks, vector_store, bm25_index, k=5, alpha=0.5):
"""
使用向量搜索和BM25搜索结合的方法进行融合检索。
参数:
query (str): 查询字符串
chunks (List[Dict]): 原始文本块
vector_store (SimpleVectorStore): 向量存储对象
bm25_index (BM25Okapi): BM25索引对象
k (int): 返回结果的数量
alpha (float): 向量分数的权重 (0-1),1-alpha为BM25权重
返回:
List[Dict]: 基于组合分数的前k个结果
"""
print(f"为查询执行融合检索: {query}")
# 定义一个小的epsilon值以避免除以零
epsilon = 1e-8
# 获取向量搜索结果
query_embedding = create_embeddings(query) # 为查询创建嵌入
vector_results = vector_store.similarity_search_with_scores(query_embedding, k=len(chunks)) # 执行向量搜索
# 获取BM25搜索结果
bm25_results = bm25_search(bm25_index, chunks, query, k=len(chunks)) # 执行BM25搜索
# 创建字典以将文档索引映射到分数
vector_scores_dict = {result["metadata"]["index"]: result["similarity"] for result in vector_results}
bm25_scores_dict = {result["metadata"]["index"]: result["bm25_score"] for result in bm25_results}
# 确保所有文档在两种方法中都有分数
all_docs = vector_store.get_all_documents()
combined_results = []
for i, doc in enumerate(all_docs):
vector_score = vector_scores_dict.get(i, 0.0) # 获取向量分数,若未找到则为0
bm25_score = bm25_scores_dict.get(i, 0.0) # 获取BM25分数,若未找到则为0
combined_results.append({
"text": doc["text"],
"metadata": doc["metadata"],
"vector_score": vector_score,
"bm25_score": bm25_score,
"index": i
})
# 提取分数作为数组
vector_scores = np.array([doc["vector_score"] for doc in combined_results])
bm25_scores = np.array([doc["bm25_score"] for doc in combined_results])
# 归一化分数
norm_vector_scores = (vector_scores - np.min(vector_scores)) / (np.max(vector_scores) - np.min(vector_scores) + epsilon)
norm_bm25_scores = (bm25_scores - np.min(bm25_scores)) / (np.max(bm25_scores) - np.min(bm25_scores) + epsilon)
# 计算组合分数
combined_scores = alpha * norm_vector_scores + (1 - alpha) * norm_bm25_scores
# 将组合分数添加到结果中
for i, score in enumerate(combined_scores):
combined_results[i]["combined_score"] = float(score)
# 按组合分数降序排序
combined_results.sort(key=lambda x: x["combined_score"], reverse=True)
# 返回前k个结果
top_results = combined_results[:k]
print(f"Retrieved {len(top_results)} documents with fusion retrieval")
return top_results
图增强型检索增强生成(Graph RAG)
概念
图增强型检索增强生成(Graph RAG)——一种通过将知识组织为连接的图而不是扁平的文档集合来增强传统RAG系统的技术。这使得系统能够导航相关概念并检索比标准向量相似性方法更具上下文相关的更多信息。
图增强型RAG的主要优势
- 保留了信息之间的关系
- 支持通过连接的概念进行遍历以找到相关的上下文
- 改进对复杂、多部分查询的处理能力
- 通过可视化知识路径提供更好的可解释性
实现
知识图谱的创建
def extract_concepts(text):
"""
使用OpenAI的API从文本中提取关键概念。
参数:
text (str): 要从中提取概念的文本
返回:
List[str]: 概念列表
"""
# 系统消息,用于指示模型需要做什么
system_message = """Extract key concepts and entities from the provided text.
Return ONLY a list of 5-10 key terms, entities, or concepts that are most important in this text.
Format your response as a JSON array of strings."""
# 向OpenAI API发出请求
response = client.chat.completions.create(
model="gpt-3.5-turbo",
messages=[
{"role": "system", "content": system_message},
{"role": "user", "content": f"Extract key concepts from:\n\n{text[:3000]}"} # API限制
],
temperature=0.0,
response_format={"type": "json_object"}
)
try:
# 从回复中解析概念
concepts_json = json.loads(response.choices[0].message.content)
concepts = concepts_json.get("concepts", [])
if not concepts and "concepts" not in concepts_json:
# 如果未找到“concepts”,尝试获取回复中的任何数组
for key, value in concepts_json.items():
if isinstance(value, list):
concepts = value
break
return concepts
except (json.JSONDecodeError, AttributeError):
# 如果JSON解析失败,则回退
content = response.choices[0].message.content
# 尝试提取任何看起来像列表的内容
matches = re.findall(r'\[(.*?)\]', content, re.DOTALL)
if matches:
items = re.findall(r'"([^"]*)"', matches[0])
return items
return []
def build_knowledge_graph(chunks):
"""
从文本片段构建知识图谱。
参数:
chunks (List[Dict]): 包含元数据的文本片段列表
返回:
Tuple[nx.Graph, List[np.ndarray]]: 知识图谱和片段嵌入
"""
print("正在构建知识图谱...")
# 创建一个图
graph = nx.Graph()
# 提取片段文本
texts = [chunk["text"] for chunk in chunks]
# 为所有片段创建嵌入
print("Creating embeddings for chunks...")
embeddings = create_embeddings(texts)
# 向图中添加节点
print("Adding nodes to the graph...")
for i, chunk in enumerate(chunks):
# 从片段中提取概念
print(f"Extracting concepts for chunk {i+1}/{len(chunks)}...")
concepts = extract_concepts(chunk["text"])
# 添加带有属性的节点
graph.add_node(i,
text=chunk["text"],
concepts=concepts,
embedding=embeddings[i])
# 根据共享概念连接节点
print("Creating edges between nodes...")
for i in range(len(chunks)):
node_concepts = set(graph.nodes[i]["concepts"])
for j in range(i + 1, len(chunks)):
# 计算概念重叠
other_concepts = set(graph.nodes[j]["concepts"])
shared_concepts = node_concepts.intersection(other_concepts)
# 如果它们共享概念,则添加一条边
if shared_concepts:
# 使用嵌入计算语义相似度
similarity = np.dot(embeddings[i], embeddings[j]) / (np.linalg.norm(embeddings[i]) * np.linalg.norm(embeddings[j]))
# 根据概念重叠和语义相似度计算边权重
concept_score = len(shared_concepts) / min(len(node_concepts), len(other_concepts))
edge_weight = 0.7 * similarity + 0.3 * concept_score
# 只添加显著关系的边
if edge_weight > 0.6:
graph.add_edge(i, j,
weight=edge_weight,
similarity=similarity,
shared_concepts=list(shared_concepts))
print(f"Knowledge graph built with {graph.number_of_nodes()} nodes and {graph.number_of_edges()} edges")
return graph, embeddings
图遍历与查询处理
def traverse_graph(query, graph, embeddings, top_k=5, max_depth=3):
"""
遍历知识图谱以查找与查询相关的信息。
参数:
query (str): 用户的问题
graph (nx.Graph): 知识图谱
embeddings (List): 节点嵌入列表
top_k (int): 考虑的初始节点数量
max_depth (int): 最大遍历深度
返回:
List[Dict]: 来自图遍历的相关信息
"""
print(f"Traversing graph for query: {query}")
# 获取查询嵌入
query_embedding = create_embeddings(query)
# 计算查询与所有节点之间的相似度
similarities = []
for i, node_embedding in enumerate(embeddings):
similarity = np.dot(query_embedding, node_embedding) / (np.linalg.norm(query_embedding) * np.linalg.norm(node_embedding))
similarities.append((i, similarity))
# 按相似度降序排序
similarities.sort(key=lambda x: x[1], reverse=True)
# 获取前 top_k 个最相似的节点作为起始点
starting_nodes = [node for node, _ in similarities[:top_k]]
print(f"Starting traversal from {len(starting_nodes)} nodes")
# 初始化遍历
visited = set() # 用于跟踪已访问节点的集合
traversal_path = [] # 存储遍历路径的列表
results = [] # 存储结果的列表
# 使用优先队列进行遍历
queue = []
for node in starting_nodes:
heapq.heappush(queue, (-similarities[node][1], node)) # 负值用于实现最大堆
# 使用基于广度优先搜索的修改版遍历图谱
while queue and len(results) < (top_k * 3): # 将结果限制为 top_k * 3
_, node = heapq.heappop(queue)
if node in visited:
continue
# 标记为已访问
visited.add(node)
traversal_path.append(node)
# 将当前节点的文本添加到结果中
results.append({
"text": graph.nodes[node]["text"],
"concepts": graph.nodes[node]["concepts"],
"node_id": node
})
# 如果未达到最大深度,则探索邻居
if len(traversal_path) < max_depth:
neighbors = [(neighbor, graph[node][neighbor]["weight"])
for neighbor in graph.neighbors(node)
if neighbor not in visited]
# 根据边权重将邻居添加到队列中
for neighbor, weight in sorted(neighbors, key=lambda x: x[1], reverse=True):
heapq.heappush(queue, (-weight, neighbor))
print(f"Graph traversal found {len(results)} relevant chunks")
return results, traversal_path
Other
还有其他的比如分层索引,假设文档嵌入,动态纠正,强化学习增强...















浙公网安备 33010602011771号