Day19-构建RAG应用
学习目标
- 掌握RAG应用系统的整体架构设计和实现流程
- 学会设计和实现智能辅助功能,提升RAG系统交互质量
- 掌握RAG系统的部署与运维基本方法
- 能够独立构建一个完整的RAG知识问答应用
学习建议
时间规划(共4小时):
- 理论学习:1小时(RAG辅助功能设计、部署与运维)
- 代码实践:2小时(辅助功能实现、系统部署)
- 项目实战:1小时(构建完整RAG应用)
学习方法:
- 先理解RAG系统辅助功能和部署运维的核心概念
- 边学边做,跟随代码示例动手实践
- 使用真实数据集测试自己构建的RAG应用
- 遇到问题及时在线查找解决方案
核心知识点
-
RAG辅助功能设计
- 引用溯源与透明度
- 知识库管理界面
- 检索结果评估与反馈
-
RAG系统部署与运维
- 部署环境选择
- 系统性能监控
- 知识库更新维护
- 安全与隐私保护
-
RAG应用优化与扩展
- 多模态RAG实现
- 多语言支持
- 个性化推荐
详细学习内容
1. RAG应用开发流程概述
RAG(检索增强生成)应用系统的完整开发流程包括以下关键步骤:
-
需求分析与规划
- 明确应用场景和目标用户
- 确定知识范围和数据来源
- 设定性能指标和质量要求
- 选择适合的技术栈和工具
-
系统架构设计
- 划分核心模块:数据处理、检索、生成
- 设计数据流和处理流程
- 规划接口和集成点
- 考虑系统的可扩展性和可维护性
-
开发环境搭建
- 选择开发框架(如LangChain、LlamaIndex)
- 安装必要依赖和工具
- 配置测试环境
- 建立版本控制和协作机制
2. 数据准备与知识库构建
RAG系统的成功很大程度上取决于高质量知识库的构建,包括以下关键步骤:
2.1 数据收集与整理
首先需要收集和整理作为知识来源的数据:
import os
import pandas as pd
import requests
from bs4 import BeautifulSoup
def collect_data_from_websites(urls):
"""从多个网站收集文本数据"""
collected_texts = []
for url in urls:
try:
response = requests.get(url, timeout=10)
response.raise_for_status()
soup = BeautifulSoup(response.content, 'html.parser')
# 移除脚本和样式元素
for script in soup(["script", "style"]):
script.extract()
# 提取正文文本
text = soup.get_text(separator=' ', strip=True)
# 基本清洗
lines = (line.strip() for line in text.splitlines())
chunks = (phrase.strip() for line in lines for phrase in line.split(" "))
text = '\n'.join(chunk for chunk in chunks if chunk)
# 存储结果
collected_texts.append({
'source': url,
'content': text,
'metadata': {
'title': soup.title.string if soup.title else url,
'date_collected': pd.Timestamp.now().isoformat()
}
})
except Exception as e:
print(f"Error collecting data from {url}: {e}")
return collected_texts
# 从PDF文件收集数据
def extract_text_from_pdfs(pdf_directory):
"""从PDF文件提取文本"""
import fitz # PyMuPDF
extracted_texts = []
for filename in os.listdir(pdf_directory):
if filename.lower().endswith('.pdf'):
filepath = os.path.join(pdf_directory, filename)
try:
doc = fitz.open(filepath)
text = ""
for page_num in range(len(doc)):
page = doc.load_page(page_num)
text += page.get_text()
extracted_texts.append({
'source': filepath,
'content': text,
'metadata': {
'title': filename,
'pages': len(doc),
'date_collected': pd.Timestamp.now().isoformat()
}
})
except Exception as e:
print(f"Error extracting text from {filepath}: {e}")
return extracted_texts
2.2 文本处理与分块
收集的数据需要进行处理和分块,以便后续向量化:
def process_and_chunk_text(documents, chunk_size=500, chunk_overlap=50):
"""处理文本并分块"""
from langchain.text_splitter import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=chunk_size,
chunk_overlap=chunk_overlap,
length_function=len,
separators=["\n\n", "\n", ". ", " ", ""]
)
processed_chunks = []
for doc in documents:
chunks = text_splitter.split_text(doc['content'])
for i, chunk_text in enumerate(chunks):
processed_chunks.append({
'text': chunk_text,
'metadata': {
'source': doc['source'],
'title': doc['metadata'].get('title', ''),
'chunk_id': f"{doc['source']}_chunk_{i}",
'original_document': doc['metadata']
}
})
return processed_chunks
2.3 向量化与存储
将处理好的文本块转化为向量表示并存储:
def vectorize_and_store(chunks, embedding_model_name="sentence-transformers/all-MiniLM-L6-v2"):
"""向量化文本块并存储到向量数据库"""
from langchain.embeddings import HuggingFaceEmbeddings
from langchain.vectorstores import Chroma
import uuid
# 初始化嵌入模型
embeddings = HuggingFaceEmbeddings(model_name=embedding_model_name)
# 准备数据
texts = [chunk['text'] for chunk in chunks]
metadatas = [chunk['metadata'] for chunk in chunks]
ids = [str(uuid.uuid4()) for _ in range(len(chunks))]
# 创建向量存储
vectorstore = Chroma.from_texts(
texts=texts,
embedding=embeddings,
metadatas=metadatas,
ids=ids,
persist_directory="./chroma_db"
)
# 持久化存储
vectorstore.persist()
return vectorstore
3. 检索系统实现
高效的检索系统是RAG应用的核心,包括以下关键部分:
3.1 基本向量检索
def vector_search(query, vectorstore, top_k=5):
"""基本向量相似度搜索"""
# 执行检索
results = vectorstore.similarity_search_with_score(
query=query,
k=top_k
)
# 处理结果
retrieved_docs = []
for doc, score in results:
retrieved_docs.append({
'content': doc.page_content,
'metadata': doc.metadata,
'score': score
})
return retrieved_docs
3.2 混合检索策略
def hybrid_search(query, vectorstore, bm25_corpus, top_k=5, alpha=0.7):
"""混合检索:结合向量检索和BM25检索"""
from rank_bm25 import BM25Okapi
import numpy as np
# 向量检索
vector_results = vectorstore.similarity_search_with_score(
query=query,
k=top_k*2 # 检索更多结果以便后续重排
)
# BM25检索
tokenized_corpus = [doc.split() for doc in bm25_corpus['texts']]
bm25 = BM25Okapi(tokenized_corpus)
tokenized_query = query.split()
bm25_scores = bm25.get_scores(tokenized_query)
# 获取BM25最高分的文档索引
top_bm25_indices = np.argsort(bm25_scores)[::-1][:top_k*2]
# 合并结果
combined_results = {}
# 处理向量检索结果
for doc, score in vector_results:
doc_id = doc.metadata.get('chunk_id')
if doc_id in combined_results:
combined_results[doc_id]['vector_score'] = score
else:
combined_results[doc_id] = {
'content': doc.page_content,
'metadata': doc.metadata,
'vector_score': score,
'bm25_score': 0
}
# 处理BM25检索结果
for idx in top_bm25_indices:
doc_id = bm25_corpus['ids'][idx]
bm25_score = bm25_scores[idx]
if doc_id in combined_results:
combined_results[doc_id]['bm25_score'] = bm25_score
else:
doc_text = bm25_corpus['texts'][idx]
doc_metadata = bm25_corpus['metadatas'][idx]
combined_results[doc_id] = {
'content': doc_text,
'metadata': doc_metadata,
'vector_score': 0,
'bm25_score': bm25_score
}
# 计算综合得分并排序
for doc_id, doc in combined_results.items():
# 标准化分数
max_vector_score = max([d['vector_score'] for d in combined_results.values()])
max_bm25_score = max([d['bm25_score'] for d in combined_results.values()])
if max_vector_score > 0:
doc['vector_score_norm'] = doc['vector_score'] / max_vector_score
else:
doc['vector_score_norm'] = 0
if max_bm25_score > 0:
doc['bm25_score_norm'] = doc['bm25_score'] / max_bm25_score
else:
doc['bm25_score_norm'] = 0
# 计算加权得分
doc['final_score'] = alpha * doc['vector_score_norm'] + (1-alpha) * doc['bm25_score_norm']
# 排序并返回top_k结果
sorted_results = sorted(combined_results.values(), key=lambda x: x['final_score'], reverse=True)
return sorted_results[:top_k]
3.3 查询优化与扩展
def optimize_query(original_query, llm):
"""使用LLM优化检索查询"""
prompt = f"""
你的任务是将用户的问题重新表述为更有效的查询,以便用于检索相关文档。
不要添加任何解释,只需输出改进后的查询。
原始问题: {original_query}
改进的查询:
"""
response = llm(prompt)
return response.strip()
def generate_multiple_queries(original_query, llm, num_queries=3):
"""生成多个不同角度的查询以提高召回率"""
prompt = f"""
你的任务是基于用户的原始问题,生成{num_queries}个不同角度的查询。
这些查询应该覆盖问题的不同方面,使用不同的词汇和表达方式。
返回结果应该是一个查询列表,每行一个查询,不要有标号或其他格式。
原始问题: {original_query}
不同角度的查询:
"""
response = llm(prompt)
queries = [q.strip() for q in response.strip().split('\n') if q.strip()]
# 确保返回指定数量的查询
if len(queries) < num_queries:
queries.extend([original_query] * (num_queries - len(queries)))
elif len(queries) > num_queries:
queries = queries[:num_queries]
return queries
4. 生成与响应处理
将检索到的内容结合LLM生成最终回答:
4.1 提示模板设计
def create_rag_prompt(query, retrieved_documents, prompt_template=None):
"""创建RAG系统的提示模板"""
if prompt_template is None:
prompt_template = """
你是一个专业的知识助手,你将基于提供的上下文回答用户的问题。
请遵循以下原则:
1. 只基于提供的上下文回答问题,不要使用你自己的知识。
2. 如果上下文中没有足够的信息,坦率地承认你不知道。
3. 保持答案简洁、清晰和有条理。
4. 如果回答中包含事实性内容,请在回答中引用上下文中的信息来源标号(例如:[1], [2]等)。
上下文信息:
{context}
用户问题: {query}
你的回答:
"""
# 准备上下文信息
context_str = ""
for i, doc in enumerate(retrieved_documents, 1):
context_str += f"[{i}] 来源: {doc['metadata'].get('source', 'Unknown')}\n"
context_str += f"{doc['content']}\n\n"
# 填充模板
filled_prompt = prompt_template.format(
context=context_str,
query=query
)
return filled_prompt
4.2 生成回答
def generate_response(prompt, llm, temperature=0.3, max_tokens=1000):
"""使用LLM生成回答"""
try:
# 生成回答
response = llm(
prompt,
temperature=temperature,
max_tokens=max_tokens
)
return {
"status": "success",
"response": response,
"error": None
}
except Exception as e:
return {
"status": "error",
"response": None,
"error": str(e)
}
4.3 回答后处理
def postprocess_response(response, retrieved_documents):
"""处理生成的回答,解析引用等"""
import re
# 查找引用标记 [1], [2] 等
citation_pattern = r'\[(\d+)\]'
citations = re.findall(citation_pattern, response)
# 提取引用的文档
cited_sources = []
for citation in citations:
citation_idx = int(citation)
if 1 <= citation_idx <= len(retrieved_documents):
source = retrieved_documents[citation_idx-1]
if source not in cited_sources:
cited_sources.append(source)
# 构建带有引用信息的结果
result = {
"answer": response,
"citations": [
{
"id": i+1,
"text": doc['content'][:200] + "...", # 截取前200个字符作为预览
"metadata": doc['metadata']
}
for i, doc in enumerate(cited_sources)
]
}
return result
5. 界面与交互设计
为RAG应用提供友好的用户界面和交互体验:
5.1 使用Streamlit构建简单界面
import streamlit as st
from PIL import Image
def create_rag_ui(retriever, generator):
"""使用Streamlit创建简单的RAG应用界面"""
# 设置页面标题和描述
st.title("智能知识问答系统")
st.markdown("👋 欢迎使用基于RAG技术的知识问答系统。输入您的问题,即可获得基于知识库的精准回答。")
# 侧边栏设置
st.sidebar.title("设置")
# 添加检索设置
st.sidebar.subheader("检索设置")
top_k = st.sidebar.slider("检索文档数量", min_value=1, max_value=10, value=5)
retrieval_method = st.sidebar.selectbox(
"检索方法",
options=["向量检索", "混合检索"],
index=1
)
# 添加生成设置
st.sidebar.subheader("生成设置")
temperature = st.sidebar.slider("温度", min_value=0.0, max_value=1.0, value=0.3, step=0.1)
max_tokens = st.sidebar.slider("最大生成长度", min_value=100, max_value=2000, value=500, step=100)
# 多模态输入选项
enable_image = st.sidebar.checkbox("启用图像输入", value=False)
# 主界面
st.subheader("提问")
query = st.text_area("请输入您的问题:", height=100)
# 图像上传(如果启用)
uploaded_image = None
if enable_image:
uploaded_image = st.file_uploader("上传相关图片(可选)", type=["jpg", "jpeg", "png"])
if uploaded_image is not None:
st.image(Image.open(uploaded_image), caption="上传的图片", width=300)
# 提交按钮
if st.button("提交问题"):
if not query:
st.error("请输入问题再提交")
return
with st.spinner("正在思考中..."):
# 处理查询和显示结果
process_query_and_show_results(
query, uploaded_image, retriever, generator,
top_k, retrieval_method, temperature, max_tokens
)
# 添加注脚
st.markdown("---")
st.markdown("*本系统基于RAG(检索增强生成)技术构建,答案来源于预先准备的知识库。*")
def process_query_and_show_results(query, image, retriever, generator, top_k, method, temperature, max_tokens):
"""处理查询并显示结果"""
# 显示查询处理过程
with st.expander("查询处理", expanded=True):
st.subheader("1. 查询处理")
# 如果有图像,将其与文本一起处理
if image is not None:
st.write("处理多模态查询(文本+图像)...")
image_obj = Image.open(image)
# 假设这里调用了多模态处理函数
st.write("图像已处理并与文本查询融合")
# 获取优化后的查询
st.write("原始查询:", query)
optimized_query = query # 实际应用中应调用查询优化函数
st.write("优化后的查询:", optimized_query)
# 检索相关文档
with st.expander("文档检索", expanded=True):
st.subheader("2. 文档检索")
st.write(f"使用{method}方法,检索Top-{top_k}个相关文档...")
# 这里应该调用实际的检索函数
# 这里使用模拟数据
retrieved_docs = [
{
"content": "这是第一个检索到的文档内容...",
"metadata": {"source": "example.com/doc1", "title": "示例文档1"},
"score": 0.92
},
{
"content": "这是第二个检索到的文档内容...",
"metadata": {"source": "example.com/doc2", "title": "示例文档2"},
"score": 0.85
}
]
# 显示检索结果
for i, doc in enumerate(retrieved_docs, 1):
st.markdown(f"**文档 {i}** (相关度: {doc['score']:.2f})")
st.markdown(f"**来源:** {doc['metadata'].get('source', 'Unknown')}")
st.markdown(f"**标题:** {doc['metadata'].get('title', 'Unknown')}")
st.text_area(f"内容预览 {i}", doc['content'][:200] + "...", height=100, disabled=True)
# 生成回答
with st.expander("回答生成", expanded=True):
st.subheader("3. 回答生成")
st.write(f"使用温度 {temperature},最大长度 {max_tokens} 生成回答...")
# 这里应该调用实际的生成函数
# 使用模拟数据
response = {
"answer": "根据检索到的信息,这是对您问题的回答。[1] 这部分内容来自第一个文档。[2] 这部分内容来自第二个文档。",
"citations": [
{"id": 1, "text": "这是第一个检索到的文档内容...", "metadata": {"source": "example.com/doc1"}},
{"id": 2, "text": "这是第二个检索到的文档内容...", "metadata": {"source": "example.com/doc2"}}
]
}
# 显示生成的回答
st.markdown("### 回答:")
st.markdown(response["answer"])
# 显示引用信息
if response["citations"]:
st.markdown("### 引用来源:")
for citation in response["citations"]:
st.markdown(f"**[{citation['id']}]** {citation['metadata'].get('source', 'Unknown')}")
with st.expander(f"查看引用内容 {citation['id']}"):
st.write(citation['text'])
5.2 结果展示与交互优化
// React组件:引用展示与交互
function CitationViewer({ answer, citations }) {
const [activeCitation, setActiveCitation] = useState(null);
// 处理引用点击
const handleCitationClick = (id) => {
setActiveCitation(id === activeCitation ? null : id);
};
// 将引用标记转换为可交互元素
const renderAnswerWithInteractiveCitations = () => {
if (!answer) return null;
// 替换引用标记为可交互元素
let parts = [];
let lastIndex = 0;
// 正则表达式匹配引用标记 [1], [2] 等
const citationRegex = /\[(\d+)\]/g;
let match;
while ((match = citationRegex.exec(answer)) !== null) {
// 添加引用标记前的文本
parts.push(answer.substring(lastIndex, match.index));
// 添加可交互的引用标记
const id = parseInt(match[1]);
parts.push(
<span
key={`citation-${id}-${match.index}`}
className={`citation-marker ${activeCitation === id ? 'active' : ''}`}
onClick={() => handleCitationClick(id)}
>
[{id}]
</span>
);
lastIndex = match.index + match[0].length;
}
// 添加最后一部分文本
parts.push(answer.substring(lastIndex));
return <div className="answer-text">{parts}</div>;
};
return (
<div className="citation-viewer">
{renderAnswerWithInteractiveCitations()}
{activeCitation && (
<div className="citation-details">
<h4>引用来源 [{activeCitation}]</h4>
{citations.find(c => c.id === activeCitation) ? (
<div>
<p className="citation-text">{citations.find(c => c.id === activeCitation).text}</p>
<p className="citation-source">
来源: {citations.find(c => c.id === activeCitation).metadata.source}
</p>
</div>
) : (
<p>未找到引用信息</p>
)}
</div>
)}
</div>
);
}
5.3 会话管理
class ConversationManager:
"""管理RAG系统中的会话历史"""
def __init__(self, max_history=10):
self.max_history = max_history
self.conversations = {} # 存储不同用户的会话历史
def add_interaction(self, user_id, query, response, retrieved_docs=None):
"""添加一次交互到会话历史"""
if user_id not in self.conversations:
self.conversations[user_id] = []
# 创建一条新交互记录
interaction = {
"timestamp": datetime.now().isoformat(),
"query": query,
"response": response,
"retrieved_docs": retrieved_docs
}
# 添加到会话历史
self.conversations[user_id].append(interaction)
# 限制历史长度
if len(self.conversations[user_id]) > self.max_history:
self.conversations[user_id] = self.conversations[user_id][-self.max_history:]
def get_history(self, user_id, count=None):
"""获取指定用户的会话历史"""
if user_id not in self.conversations:
return []
history = self.conversations[user_id]
if count is not None:
return history[-count:]
return history
def get_context_from_history(self, user_id, include_docs=False, max_turns=3):
"""从历史中获取上下文,用于增强当前查询"""
history = self.get_history(user_id, count=max_turns)
context = []
for interaction in history:
context.append(f"User: {interaction['query']}")
context.append(f"Assistant: {interaction['response']}")
# 可选:包含检索到的文档
if include_docs and interaction.get('retrieved_docs'):
docs_text = "\n".join([f"- {doc['content'][:100]}..."
for doc in interaction['retrieved_docs'][:2]])
context.append(f"Retrieved documents:\n{docs_text}")
return "\n".join(context)
def clear_history(self, user_id):
"""清除指定用户的会话历史"""
if user_id in self.conversations:
self.conversations[user_id] = []
6. RAG系统部署与运维
6.1 使用Docker容器化部署
# Dockerfile for RAG application
FROM python:3.9-slim
WORKDIR /app
# 复制依赖文件并安装依赖
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 复制应用代码
COPY . .
# 设置环境变量
ENV MODEL_PATH=/app/models
ENV VECTOR_DB_PATH=/app/vector_db
ENV PORT=5000
# 创建数据卷以持久化存储
VOLUME ["/app/vector_db", "/app/models", "/app/logs"]
# 暴露端口
EXPOSE $PORT
# 启动应用
CMD ["python", "app.py"]
# docker-compose.yml
version: '3'
services:
rag_app:
build: .
ports:
- "5000:5000"
volumes:
- ./data:/app/data
- ./vector_db:/app/vector_db
- ./models:/app/models
- ./logs:/app/logs
environment:
- OPENAI_API_KEY=${OPENAI_API_KEY}
- EMBEDDING_MODEL=sentence-transformers/all-MiniLM-L6-v2
- MAX_TOKENS=1000
- TEMPERATURE=0.3
restart: unless-stopped
# 向量数据库服务
milvus:
image: milvusdb/milvus:v2.3.0
ports:
- "19530:19530"
- "9091:9091"
volumes:
- ./milvus_data:/var/lib/milvus
environment:
- MILVUS_HOST=milvus
- MILVUS_PORT=19530
restart: unless-stopped
6.2 系统监控与日志
import logging
import time
from prometheus_client import Counter, Histogram, start_http_server
# 设置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler("logs/rag_app.log"),
logging.StreamHandler()
]
)
logger = logging.getLogger("rag_app")
# 设置Prometheus指标
QUERY_COUNTER = Counter('rag_queries_total', 'Total number of queries processed')
QUERY_LATENCY = Histogram('rag_query_latency_seconds', 'Query processing time in seconds')
RETRIEVAL_LATENCY = Histogram('rag_retrieval_latency_seconds', 'Document retrieval time')
GENERATION_LATENCY = Histogram('rag_generation_latency_seconds', 'Response generation time')
ERROR_COUNTER = Counter('rag_errors_total', 'Total number of errors', ['type'])
# 启动Prometheus指标服务器
start_http_server(8000)
def monitored_rag_query(query, retriever, generator, top_k=5):
"""带监控的RAG查询处理"""
QUERY_COUNTER.inc()
start_time = time.time()
try:
# 记录请求
logger.info(f"Processing query: '{query}'")
# 文档检索阶段
retrieval_start = time.time()
retrieved_docs = retriever.retrieve(query, top_k=top_k)
retrieval_time = time.time() - retrieval_start
RETRIEVAL_LATENCY.observe(retrieval_time)
logger.info(f"Retrieved {len(retrieved_docs)} documents in {retrieval_time:.2f}s")
# 生成回答阶段
generation_start = time.time()
answer = generator.generate(query, retrieved_docs)
generation_time = time.time() - generation_start
GENERATION_LATENCY.observe(generation_time)
logger.info(f"Generated answer in {generation_time:.2f}s")
# 总处理时间
total_time = time.time() - start_time
QUERY_LATENCY.observe(total_time)
logger.info(f"Total processing time: {total_time:.2f}s")
return {
"answer": answer,
"retrieved_docs": retrieved_docs,
"metrics": {
"total_time": total_time,
"retrieval_time": retrieval_time,
"generation_time": generation_time
}
}
except Exception as e:
# 记录错误
ERROR_COUNTER.labels(type=type(e).__name__).inc()
logger.error(f"Error processing query '{query}': {str(e)}", exc_info=True)
raise
6.3 知识库更新机制
import schedule
import time
from datetime import datetime
def schedule_knowledge_base_updates(update_func, update_time="02:00"):
"""设置定时更新知识库任务"""
logger.info(f"Scheduling knowledge base updates at {update_time} daily")
schedule.every().day.at(update_time).do(update_func)
# 启动调度器
def run_scheduler():
while True:
schedule.run_pending()
time.sleep(60)
# 在后台线程运行调度器
import threading
scheduler_thread = threading.Thread(target=run_scheduler, daemon=True)
scheduler_thread.start()
return scheduler_thread
def update_knowledge_base():
"""更新知识库的核心逻辑"""
logger.info(f"Starting knowledge base update at {datetime.now().isoformat()}")
try:
# 获取需要更新的数据源
data_sources = get_data_sources()
updated_count = 0
for source in data_sources:
if needs_update(source):
logger.info(f"Updating source: {source.get('name', source.get('id', 'Unknown'))}")
# 获取新内容
new_content = fetch_new_content(source)
if not new_content:
logger.info(f"No new content found for source {source.get('name')}")
continue
# 处理并分块新内容
chunks = process_content(new_content)
# 向量化并更新存储
update_vector_store(source['id'], chunks)
# 更新元数据
update_source_metadata(source['id'], {
'last_updated': datetime.now().isoformat(),
'chunks_count': len(chunks)
})
updated_count += 1
logger.info(f"Updated source {source.get('name')} with {len(chunks)} chunks")
logger.info(f"Knowledge base update completed. Updated {updated_count} sources.")
return {"status": "success", "updated_sources": updated_count}
except Exception as e:
logger.error(f"Knowledge base update failed: {str(e)}", exc_info=True)
return {"status": "error", "message": str(e)}
7. RAG辅助功能设计
7.1 引用溯源与透明度
RAG系统的一大优势是能够追溯信息来源,增强可信度和透明度。
def generate_response_with_citations(query, contexts):
"""生成带有引用源的回答"""
# 准备提示模板,要求模型提供引用
prompt = f"""根据提供的上下文回答问题。如果回答中包含来自上下文的信息,
请在相关内容后用[1]、[2]等标记引用来源编号。
上下文:
{contexts}
问题: {query}
回答(包含引用标记):"""
# 调用语言模型生成回答
response = llm(prompt)
# 整理引用来源
sources = []
for i, context in enumerate(contexts, 1):
if f"[{i}]" in response:
sources.append({
"id": i,
"text": context[:100] + "...",
"source": context_sources[i-1] # 假设有个映射存储了上下文来源
})
return {
"answer": response,
"sources": sources
}
引用溯源的前端实现可以使用JavaScript增强用户交互体验:
function renderAnswerWithCitations(response) {
let answer = response.answer;
const sources = response.sources;
// 替换引用标记为可点击链接
sources.forEach(source => {
const pattern = new RegExp(`\\[${source.id}\\]`, 'g');
answer = answer.replace(pattern,
`<a href="#" class="citation" data-source-id="${source.id}">[${source.id}]</a>`);
});
// 渲染回答和来源列表
document.getElementById('answer').innerHTML = answer;
// 渲染来源详情
const sourcesHtml = sources.map(source =>
`<div id="source-${source.id}" class="source-item">
<span class="source-id">[${source.id}]</span>
<span class="source-text">${source.text}</span>
<a href="${source.source}" target="_blank">查看源文档</a>
</div>`
).join('');
document.getElementById('sources').innerHTML = sourcesHtml;
// 添加点击事件:点击引用时显示来源详情
document.querySelectorAll('.citation').forEach(citation => {
citation.addEventListener('click', function(e) {
e.preventDefault();
const sourceId = this.getAttribute('data-source-id');
const sourceElement = document.getElementById(`source-${sourceId}`);
// 切换显示/隐藏来源详情
if (sourceElement.classList.contains('active')) {
sourceElement.classList.remove('active');
} else {
// 先隐藏所有其他来源
document.querySelectorAll('.source-item.active').forEach(el => {
el.classList.remove('active');
});
// 显示当前来源
sourceElement.classList.add('active');
}
});
});
}
7.2 知识库管理界面
管理RAG系统的知识库需要一个直观的界面,让管理员能够查看、添加、删除和更新知识源。
import streamlit as st
import pandas as pd
from datetime import datetime
def create_knowledge_management_ui(vector_store_manager):
"""创建知识库管理界面"""
st.title("RAG知识库管理系统")
# 创建选项卡
tab1, tab2, tab3, tab4 = st.tabs(["知识库概览", "添加内容", "更新内容", "删除内容"])
# 选项卡1: 知识库概览
with tab1:
st.header("知识库概览")
# 加载知识库统计信息
stats = vector_store_manager.get_stats()
# 显示基本统计信息
col1, col2, col3 = st.columns(3)
with col1:
st.metric("文档总数", stats["total_documents"])
with col2:
st.metric("向量总数", stats["total_vectors"])
with col3:
st.metric("数据源数量", stats["source_count"])
# 显示最近更新信息
st.subheader("最近更新")
if stats["recent_updates"]:
updates_df = pd.DataFrame(stats["recent_updates"])
st.dataframe(updates_df)
else:
st.info("没有最近的更新记录")
# 显示按来源的文档分布
st.subheader("文档来源分布")
source_counts = pd.DataFrame(stats["source_distribution"], columns=["来源", "文档数"])
st.bar_chart(source_counts.set_index("来源"))
# 显示详细的来源列表
st.subheader("数据来源详情")
sources_df = pd.DataFrame(stats["sources"])
st.dataframe(sources_df)
# 选项卡2: 添加内容
with tab2:
st.header("添加新内容")
# 选择添加方式
add_method = st.radio("选择添加方式", ["上传文件", "网页URL", "手动输入"])
if add_method == "上传文件":
st.subheader("文件上传")
uploaded_files = st.file_uploader("选择文件", accept_multiple_files=True, type=["pdf", "txt", "docx", "csv"])
col1, col2 = st.columns(2)
with col1:
source_name = st.text_input("来源名称")
with col2:
category = st.selectbox("分类", ["技术文档", "学术论文", "新闻文章", "产品描述", "其他"])
if st.button("处理并添加文件"):
if not uploaded_files:
st.error("请先上传文件")
elif not source_name:
st.error("请提供来源名称")
else:
with st.spinner("正在处理文件..."):
for file in uploaded_files:
# 在实际应用中调用处理函数
# vector_store_manager.add_file(file, source_name, category)
pass
st.success(f"成功添加 {len(uploaded_files)} 个文件到知识库")
elif add_method == "网页URL":
st.subheader("网页内容抓取")
urls = st.text_area("输入URL列表(每行一个)")
col1, col2 = st.columns(2)
with col1:
source_name = st.text_input("来源名称")
with col2:
category = st.selectbox("分类", ["技术文档", "学术论文", "新闻文章", "产品描述", "其他"])
if st.button("抓取并添加内容"):
if not urls:
st.error("请输入至少一个URL")
elif not source_name:
st.error("请提供来源名称")
else:
url_list = [url.strip() for url in urls.split("\n") if url.strip()]
with st.spinner(f"正在处理 {len(url_list)} 个URL..."):
# 在实际应用中调用处理函数
# vector_store_manager.add_urls(url_list, source_name, category)
st.success(f"成功添加 {len(url_list)} 个URL的内容到知识库")
elif add_method == "手动输入":
st.subheader("手动输入内容")
title = st.text_input("标题")
content = st.text_area("内容", height=300)
col1, col2 = st.columns(2)
with col1:
source_name = st.text_input("来源名称")
with col2:
category = st.selectbox("分类", ["技术文档", "学术论文", "新闻文章", "产品描述", "其他"])
if st.button("处理并添加内容"):
if not content:
st.error("请输入内容")
elif not title:
st.error("请输入标题")
elif not source_name:
st.error("请提供来源名称")
else:
with st.spinner("正在处理内容..."):
# 在实际应用中调用处理函数
# vector_store_manager.add_text(title, content, source_name, category)
st.success("成功添加内容到知识库")
# 选项卡3: 更新内容
with tab3:
st.header("更新现有内容")
# 获取可更新的源
sources = vector_store_manager.list_sources()
if not sources:
st.info("没有可更新的内容源")
else:
selected_source = st.selectbox("选择要更新的内容源", [s["name"] for s in sources])
# 获取选定源的详细信息
source_details = next((s for s in sources if s["name"] == selected_source), None)
if source_details:
st.json(source_details)
update_method = st.radio("更新方式", ["替换内容", "增量更新"])
if update_method == "替换内容":
new_content = st.text_area("新内容", height=300)
if st.button("替换内容"):
with st.spinner("正在更新..."):
# 在实际应用中调用更新函数
# vector_store_manager.replace_source(selected_source, new_content)
st.success(f"成功更新 {selected_source}")
elif update_method == "增量更新":
additional_content = st.text_area("附加内容", height=300)
if st.button("添加内容"):
with st.spinner("正在更新..."):
# 在实际应用中调用更新函数
# vector_store_manager.append_to_source(selected_source, additional_content)
st.success(f"成功为 {selected_source} 添加新内容")
# 选项卡4: 删除内容
with tab4:
st.header("删除内容")
# 获取可删除的源
sources = vector_store_manager.list_sources()
if not sources:
st.info("没有可删除的内容源")
else:
deletion_type = st.radio("删除类型", ["按源删除", "按查询删除"])
if deletion_type == "按源删除":
selected_sources = st.multiselect("选择要删除的内容源", [s["name"] for s in sources])
if selected_sources:
st.warning(f"您将删除以下内容源: {', '.join(selected_sources)}")
confirm = st.checkbox("我确认要删除这些内容源")
if confirm and st.button("确认删除"):
with st.spinner("正在删除..."):
# 在实际应用中调用删除函数
# for source in selected_sources:
# vector_store_manager.delete_source(source)
st.success(f"成功删除 {len(selected_sources)} 个内容源")
elif deletion_type == "按查询删除":
query = st.text_input("输入搜索查询")
if query and st.button("搜索"):
with st.spinner("正在搜索..."):
# 在实际应用中调用搜索函数
# results = vector_store_manager.search(query)
# 这里使用模拟数据
results = [
{"id": "1", "content": "这是第一个搜索结果...", "source": "示例来源1"},
{"id": "2", "content": "这是第二个搜索结果...", "source": "示例来源2"}
]
if results:
st.subheader("搜索结果")
selected_docs = []
for i, doc in enumerate(results):
col1, col2 = st.columns([0.1, 0.9])
with col1:
selected = st.checkbox("", key=f"select_{i}")
if selected:
selected_docs.append(doc["id"])
with col2:
st.markdown(f"**来源:** {doc['source']}")
st.text_area(f"内容 {i+1}", doc["content"], height=100, disabled=True)
if selected_docs:
st.warning(f"您将删除 {len(selected_docs)} 个文档")
confirm = st.checkbox("我确认要删除这些文档")
if confirm and st.button("确认删除"):
with st.spinner("正在删除..."):
# 在实际应用中调用删除函数
# vector_store_manager.delete_documents(selected_docs)
st.success(f"成功删除 {len(selected_docs)} 个文档")
else:
st.info("没有找到匹配的内容")
# 向量存储管理器类接口示例
class VectorStoreManager:
"""向量存储管理器"""
def __init__(self, vector_store):
self.vector_store = vector_store
def get_stats(self):
"""获取知识库统计信息"""
# 这里应该实现实际的统计收集逻辑
# 返回模拟数据
return {
"total_documents": 1250,
"total_vectors": 1250,
"source_count": 5,
"recent_updates": [
{"date": "2023-06-01", "source": "技术博客", "action": "添加", "count": 15},
{"date": "2023-06-15", "source": "产品手册", "action": "更新", "count": 8},
{"date": "2023-06-20", "source": "客户反馈", "action": "添加", "count": 12}
],
"source_distribution": [
{"来源": "技术博客", "文档数": 450},
{"来源": "产品手册", "文档数": 300},
{"来源": "客户反馈", "文档数": 200},
{"来源": "学术论文", "文档数": 180},
{"来源": "新闻文章", "文档数": 120}
],
"sources": [
{"name": "技术博客", "document_count": 450, "last_updated": "2023-06-01"},
{"name": "产品手册", "document_count": 300, "last_updated": "2023-06-15"},
{"name": "客户反馈", "document_count": 200, "last_updated": "2023-06-20"},
{"name": "学术论文", "document_count": 180, "last_updated": "2023-05-10"},
{"name": "新闻文章", "document_count": 120, "last_updated": "2023-04-22"}
]
}
def list_sources(self):
"""列出所有内容源"""
# 在实际实现中应该查询向量存储
# 返回模拟数据
return [
{"name": "技术博客", "document_count": 450, "last_updated": "2023-06-01"},
{"name": "产品手册", "document_count": 300, "last_updated": "2023-06-15"},
{"name": "客户反馈", "document_count": 200, "last_updated": "2023-06-20"},
{"name": "学术论文", "document_count": 180, "last_updated": "2023-05-10"},
{"name": "新闻文章", "document_count": 120, "last_updated": "2023-04-22"}
]
7.3 检索结果评估与反馈
RAG系统的质量很大程度上取决于检索的相关性,因此建立评估和反馈机制非常重要:
class RetrievalEvaluator:
"""检索结果评估器"""
def __init__(self, llm):
self.llm = llm
self.feedback_db = []
def evaluate_retrieval(self, query, retrieved_docs, relevance_threshold=0.7):
"""评估检索结果的相关性"""
evaluation_results = []
# 对每个检索结果进行评估
for i, doc in enumerate(retrieved_docs):
# 使用LLM评估相关性
relevance_score = self._assess_relevance(query, doc['content'])
evaluation_results.append({
"doc_id": doc.get('id', f"doc_{i}"),
"relevance_score": relevance_score,
"is_relevant": relevance_score >= relevance_threshold,
"metadata": doc.get('metadata', {})
})
# 计算整体指标
overall_metrics = {
"precision": sum(1 for r in evaluation_results if r['is_relevant']) / len(evaluation_results) if evaluation_results else 0,
"relevant_count": sum(1 for r in evaluation_results if r['is_relevant']),
"irrelevant_count": sum(1 for r in evaluation_results if not r['is_relevant']),
"average_relevance": sum(r['relevance_score'] for r in evaluation_results) / len(evaluation_results) if evaluation_results else 0
}
return {
"query": query,
"results": evaluation_results,
"metrics": overall_metrics
}
def _assess_relevance(self, query, document_content):
"""使用LLM评估查询与文档的相关性"""
prompt = f"""
任务: 评估文档与查询之间的相关性。
查询: {query}
文档内容: {document_content}
请给出相关性评分,范围从0.0到1.0,其中:
- 0.0: 完全不相关
- 0.3: 略微相关
- 0.5: 中等相关
- 0.7: 相关
- 1.0: 高度相关
只返回一个数字作为评分,不要解释。
"""
try:
response = self.llm(prompt).strip()
# 提取数字结果
import re
match = re.search(r'(\d*\.\d+|\d+)', response)
if match:
score = float(match.group(1))
# 确保分数在0-1范围内
return max(0.0, min(1.0, score))
else:
return 0.5 # 如果无法解析,返回中等相关性
except Exception as e:
print(f"评估相关性时出错: {str(e)}")
return 0.5
def record_user_feedback(self, query, doc_id, feedback, user_id=None):
"""记录用户对检索结果的反馈"""
feedback_record = {
"timestamp": datetime.now().isoformat(),
"query": query,
"doc_id": doc_id,
"feedback": feedback, # 可以是"relevant", "irrelevant"等
"user_id": user_id
}
self.feedback_db.append(feedback_record)
# 在实际应用中,应该将反馈保存到数据库
return feedback_record
def analyze_feedback(self, period=None):
"""分析用户反馈数据"""
if not self.feedback_db:
return {"message": "没有收集到反馈数据"}
# 过滤特定时间段的反馈
filtered_feedback = self.feedback_db
if period:
start_time = datetime.now() - timedelta(days=period)
filtered_feedback = [
f for f in self.feedback_db
if datetime.fromisoformat(f["timestamp"]) >= start_time
]
# 计算正负反馈比例
positive_feedback = [f for f in filtered_feedback if f["feedback"] == "relevant"]
negative_feedback = [f for f in filtered_feedback if f["feedback"] == "irrelevant"]
# 按文档ID统计反馈
doc_feedback = {}
for f in filtered_feedback:
doc_id = f["doc_id"]
if doc_id not in doc_feedback:
doc_feedback[doc_id] = {"relevant": 0, "irrelevant": 0}
if f["feedback"] == "relevant":
doc_feedback[doc_id]["relevant"] += 1
elif f["feedback"] == "irrelevant":
doc_feedback[doc_id]["irrelevant"] += 1
# 识别问题文档
problematic_docs = []
for doc_id, counts in doc_feedback.items():
if counts["irrelevant"] > counts["relevant"]:
problematic_docs.append({
"doc_id": doc_id,
"irrelevant_count": counts["irrelevant"],
"relevant_count": counts["relevant"],
"ratio": counts["irrelevant"] / (counts["relevant"] + counts["irrelevant"])
})
return {
"total_feedback": len(filtered_feedback),
"positive_feedback": len(positive_feedback),
"negative_feedback": len(negative_feedback),
"positive_ratio": len(positive_feedback) / len(filtered_feedback) if filtered_feedback else 0,
"problematic_docs": sorted(problematic_docs, key=lambda x: x["ratio"], reverse=True)
}
前端反馈收集界面:
def create_feedback_ui(retrieval_evaluator):
"""创建用户反馈收集界面"""
st.title("检索结果反馈")
# 会话管理
if "feedback_history" not in st.session_state:
st.session_state.feedback_history = []
# 反馈表单
with st.form("feedback_form"):
st.write("请评价最后一次检索结果的相关性")
# 查询信息
last_query = st.text_input("您的查询", disabled=True, value=st.session_state.get("last_query", ""))
# 获取检索结果
if "last_results" in st.session_state:
results = st.session_state.last_results
# 为每个结果添加反馈选项
feedback_values = {}
for i, result in enumerate(results):
st.markdown(f"**文档 {i+1}**")
st.markdown(f"来源: {result.get('source', '未知')}")
st.text_area(f"内容预览 {i+1}", result.get('content', '')[:200] + "...", height=100, disabled=True)
feedback_values[f"feedback_{i}"] = st.radio(
f"文档 {i+1} 的相关性",
options=["相关", "部分相关", "不相关", "不确定"],
horizontal=True,
key=f"feedback_radio_{i}"
)
st.markdown("---")
# 其他反馈
additional_feedback = st.text_area("其他反馈或建议", height=100)
# 提交按钮
submit_button = st.form_submit_button("提交反馈")
if submit_button:
if "last_results" in st.session_state:
# 收集所有反馈
collected_feedback = []
for i, result in enumerate(st.session_state.last_results):
feedback_value = feedback_values.get(f"feedback_{i}")
# 将反馈转换为标准格式
standard_feedback = "unknown"
if feedback_value == "相关":
standard_feedback = "relevant"
elif feedback_value == "不相关":
standard_feedback = "irrelevant"
elif feedback_value == "部分相关":
standard_feedback = "partially_relevant"
# 记录反馈
if feedback_value != "不确定":
feedback_record = retrieval_evaluator.record_user_feedback(
query=last_query,
doc_id=result.get('id', f"doc_{i}"),
feedback=standard_feedback
)
collected_feedback.append(feedback_record)
# 保存到会话历史
st.session_state.feedback_history.extend(collected_feedback)
# 显示感谢消息
st.success("感谢您的反馈!我们将用它来改进检索系统。")
# 清除表单
st.session_state.last_query = ""
st.session_state.last_results = []
else:
st.error("没有检索结果可以评价")
# 显示反馈统计
if st.session_state.feedback_history:
st.subheader("您的反馈历史")
feedback_counts = {
"relevant": 0,
"irrelevant": 0,
"partially_relevant": 0
}
for feedback in st.session_state.feedback_history:
feedback_type = feedback.get("feedback")
if feedback_type in feedback_counts:
feedback_counts[feedback_type] += 1
# 显示反馈统计
col1, col2, col3 = st.columns(3)
with col1:
st.metric("相关", feedback_counts["relevant"])
with col2:
st.metric("部分相关", feedback_counts["partially_relevant"])
with col3:
st.metric("不相关", feedback_counts["irrelevant"])
8. RAG应用优化与扩展
8.1 多模态RAG实现
扩展RAG系统以支持图像和文本混合检索的能力:
from PIL import Image
import torch
from transformers import CLIPProcessor, CLIPModel
class MultimodalRAG:
"""多模态RAG实现"""
def __init__(self, text_embedder, retriever, llm, clip_model_name="openai/clip-vit-base-patch32"):
# 初始化文本嵌入模型
self.text_embedder = text_embedder
# 初始化向量数据库检索器
self.retriever = retriever
# 初始化大语言模型
self.llm = llm
# 初始化CLIP模型用于图像处理
self.clip_model = CLIPModel.from_pretrained(clip_model_name)
self.clip_processor = CLIPProcessor.from_pretrained(clip_model_name)
def encode_image(self, image):
"""将图像编码为向量表示"""
inputs = self.clip_processor(images=image, return_tensors="pt")
with torch.no_grad():
image_features = self.clip_model.get_image_features(**inputs)
return image_features.cpu().numpy()[0]
def encode_text(self, text):
"""将文本编码为向量表示"""
if hasattr(self.text_embedder, 'embed_query'):
# LangChain风格的嵌入模型
return self.text_embedder.embed_query(text)
else:
# 直接使用CLIP的文本编码器
inputs = self.clip_processor(text=text, return_tensors="pt", padding=True)
with torch.no_grad():
text_features = self.clip_model.get_text_features(**inputs)
return text_features.cpu().numpy()[0]
def multimodal_query(self, text_query, image=None, text_weight=0.7, top_k=5):
"""执行多模态查询"""
# 文本查询编码
text_embedding = self.encode_text(text_query)
if image is not None:
# 图像编码
image_embedding = self.encode_image(image)
# 融合嵌入(加权平均)
combined_embedding = text_weight * text_embedding + (1 - text_weight) * image_embedding
# 使用融合嵌入进行检索
# 注意:实际应用中,需要向量存储支持自定义查询向量
results = self.retriever.similarity_search_by_vector(
combined_embedding,
k=top_k
)
else:
# 仅使用文本进行检索
results = self.retriever.similarity_search(
text_query,
k=top_k
)
return results
def generate_response(self, text_query, image=None, text_weight=0.7, top_k=5):
"""生成对多模态查询的回答"""
# 检索相关文档
retrieved_docs = self.multimodal_query(
text_query=text_query,
image=image,
text_weight=text_weight,
top_k=top_k
)
# 构建上下文
context = "\n\n".join([doc.page_content for doc in retrieved_docs])
# 如果有图像,添加图像描述
image_description = ""
if image is not None:
image_description = self._generate_image_description(image)
# 创建提示
prompt = f"""
请基于提供的上下文信息回答用户的问题。
上下文信息:
{context}
{'图像描述: ' + image_description if image_description else ''}
用户问题: {text_query}
请提供详细、准确的回答,仅基于提供的上下文信息。如果上下文中没有足够的信息,请坦诚说明。
"""
# 生成回答
answer = self.llm(prompt)
return {
"answer": answer,
"retrieved_docs": retrieved_docs,
"image_description": image_description if image else None
}
def _generate_image_description(self, image):
"""使用LLM生成图像描述"""
# 此函数假设我们有通过LLM生成图像描述的能力
# 实际应用中可以使用专门的图像说明模型
# 这里使用简化实现
return "图像描述将在实际应用中由图像理解模型生成"
8.2 多语言支持
增强RAG系统以支持多种语言的查询和文档:
from langdetect import detect
from transformers import MarianMTModel, MarianTokenizer
class MultilingualRAG:
"""多语言RAG实现"""
def __init__(self, base_retriever, llm, primary_language="zh", supported_languages=None):
self.base_retriever = base_retriever
self.llm = llm
self.primary_language = primary_language
self.supported_languages = supported_languages or ["zh", "en", "es", "fr", "de", "ja", "ko"]
# 初始化翻译模型
self.translation_models = {}
for lang in self.supported_languages:
if lang != self.primary_language:
# 加载从该语言到主要语言的翻译模型
try:
model_name = f"Helsinki-NLP/opus-mt-{lang}-{self.primary_language}"
self.translation_models[f"{lang}_to_{self.primary_language}"] = {
"tokenizer": MarianTokenizer.from_pretrained(model_name),
"model": MarianMTModel.from_pretrained(model_name)
}
# 加载从主要语言到该语言的翻译模型
reverse_model_name = f"Helsinki-NLP/opus-mt-{self.primary_language}-{lang}"
self.translation_models[f"{self.primary_language}_to_{lang}"] = {
"tokenizer": MarianTokenizer.from_pretrained(reverse_model_name),
"model": MarianMTModel.from_pretrained(reverse_model_name)
}
except Exception as e:
print(f"无法加载翻译模型 {lang}: {str(e)}")
def translate_text(self, text, source_lang, target_lang):
"""翻译文本"""
if source_lang == target_lang:
return text
model_key = f"{source_lang}_to_{target_lang}"
if model_key not in self.translation_models:
raise ValueError(f"不支持的翻译方向: {source_lang} -> {target_lang}")
translator = self.translation_models[model_key]
tokenizer = translator["tokenizer"]
model = translator["model"]
# 编码文本
inputs = tokenizer(text, return_tensors="pt", padding=True, truncation=True, max_length=512)
# 翻译
with torch.no_grad():
outputs = model.generate(**inputs)
# 解码结果
translated_text = tokenizer.decode(outputs[0], skip_special_tokens=True)
return translated_text
def detect_language(self, text):
"""检测文本语言"""
try:
return detect(text)
except:
# 如果检测失败,默认返回主要语言
return self.primary_language
def query(self, query_text, top_k=5):
"""处理多语言查询"""
# 检测查询语言
query_language = self.detect_language(query_text)
# 如果不是主要语言,翻译查询
if query_language != self.primary_language:
translated_query = self.translate_text(
query_text,
query_language,
self.primary_language
)
else:
translated_query = query_text
# 使用翻译后的查询检索文档
retrieved_docs = self.base_retriever.similarity_search(
translated_query,
k=top_k
)
# 如果原始查询不是主要语言,需要翻译回答
need_translation = query_language != self.primary_language
return {
"original_query": query_text,
"translated_query": translated_query if need_translation else None,
"query_language": query_language,
"retrieved_docs": retrieved_docs,
"need_translation": need_translation
}
def generate_response(self, query_text, top_k=5):
"""生成多语言回答"""
# 执行多语言查询
query_result = self.query(query_text, top_k=top_k)
# 从检索结果构建上下文
context = "\n\n".join([doc.page_content for doc in query_result["retrieved_docs"]])
# 使用LLM生成回答
prompt = f"""
请基于以下上下文信息回答问题。
上下文:
{context}
问题:{query_result["translated_query"] or query_result["original_query"]}
回答:
"""
response = self.llm(prompt)
# 如果需要翻译回答
if query_result["need_translation"]:
translated_response = self.translate_text(
response,
self.primary_language,
query_result["query_language"]
)
else:
translated_response = None
return {
"answer": translated_response or response,
"original_answer": response if translated_response else None,
"retrieved_docs": query_result["retrieved_docs"]
}
8.3 个性化推荐
基于用户历史查询和反馈实现个性化推荐:
class PersonalizedRAG:
"""个性化RAG实现"""
def __init__(self, base_retriever, llm, user_db=None):
self.base_retriever = base_retriever
self.llm = llm
self.user_db = user_db or {} # 用户数据库,存储用户偏好和历史
def get_user_profile(self, user_id):
"""获取用户资料,如果不存在则创建"""
if user_id not in self.user_db:
self.user_db[user_id] = {
"preferences": {},
"interests": {},
"history": [],
"favorite_sources": [],
"feedback": []
}
return self.user_db[user_id]
def update_user_interests(self, user_id, query, retrieved_docs, relevance_feedback=None):
"""更新用户兴趣模型"""
user_profile = self.get_user_profile(user_id)
# 提取查询中的关键词/主题
keywords = self._extract_keywords(query)
# 更新兴趣模型
for keyword in keywords:
if keyword not in user_profile["interests"]:
user_profile["interests"][keyword] = 0
user_profile["interests"][keyword] += 1
# 记录查询历史
user_profile["history"].append({
"timestamp": datetime.now().isoformat(),
"query": query,
"doc_ids": [doc.metadata.get("id", "unknown") for doc in retrieved_docs]
})
# 如果有相关性反馈,记录下来
if relevance_feedback:
user_profile["feedback"].append({
"timestamp": datetime.now().isoformat(),
"query": query,
"feedback": relevance_feedback
})
# 更新喜好的信息源
for doc_id, relevance in relevance_feedback.items():
if relevance > 0.7: # 假设高相关性表示用户喜欢
doc_source = next((doc.metadata.get("source") for doc in retrieved_docs
if doc.metadata.get("id") == doc_id), None)
if doc_source and doc_source not in user_profile["favorite_sources"]:
user_profile["favorite_sources"].append(doc_source)
def _extract_keywords(self, text):
"""从文本中提取关键词"""
# 简化实现,实际应用中可以使用TF-IDF, KeyBERT等
import re
words = re.findall(r'\w+', text.lower())
# 过滤停用词
stopwords = {"的", "了", "是", "在", "我", "你", "他", "she", "he", "the", "a", "an", "and", "or", "in", "on", "at"}
return [word for word in words if word not in stopwords and len(word) > 1]
def personalized_retrieval(self, user_id, query, top_k=5):
"""个性化检索"""
user_profile = self.get_user_profile(user_id)
# 基本检索
base_results = self.base_retriever.similarity_search(query, k=top_k*2)
# 个性化重排
if user_profile["interests"] or user_profile["favorite_sources"]:
# 提取查询关键词
query_keywords = self._extract_keywords(query)
# 为每个结果计算个性化分数
personalized_results = []
for doc in base_results:
# 基础分数
base_score = 1.0
# 根据用户兴趣调整分数
interest_boost = 0
doc_keywords = self._extract_keywords(doc.page_content)
for keyword in doc_keywords:
if keyword in user_profile["interests"]:
interest_boost += user_profile["interests"][keyword] * 0.1
# 根据来源偏好调整分数
source_boost = 0
doc_source = doc.metadata.get("source")
if doc_source in user_profile["favorite_sources"]:
source_boost = 0.5
# 计算最终分数
final_score = base_score + interest_boost + source_boost
personalized_results.append({
"doc": doc,
"score": final_score
})
# 按调整后的分数排序
personalized_results.sort(key=lambda x: x["score"], reverse=True)
# 返回前top_k个结果
return [item["doc"] for item in personalized_results[:top_k]]
else:
# 如果没有个性化信息,返回基本检索结果
return base_results[:top_k]
def generate_personalized_response(self, user_id, query, top_k=5):
"""生成个性化回答"""
# 个性化检索
retrieved_docs = self.personalized_retrieval(user_id, query, top_k)
# 获取用户资料
user_profile = self.get_user_profile(user_id)
# 构建上下文
context = "\n\n".join([doc.page_content for doc in retrieved_docs])
# 用户偏好信息
user_preferences = []
for pref, value in user_profile["preferences"].items():
user_preferences.append(f"{pref}: {value}")
# 用户兴趣信息
top_interests = sorted(user_profile["interests"].items(), key=lambda x: x[1], reverse=True)[:5]
user_interests = ", ".join([interest for interest, _ in top_interests])
# 创建提示
prompt = f"""
请基于以下上下文信息回答用户的问题。在回答时,请考虑用户的偏好和兴趣。
上下文信息:
{context}
用户偏好:
{'; '.join(user_preferences) if user_preferences else '未知'}
用户兴趣:
{user_interests if user_interests else '未知'}
用户问题: {query}
请生成一个个性化的、信息丰富的回答。如果上下文中没有足够的信息,请坦诚说明。
"""
# 生成回答
response = self.llm(prompt)
# 更新用户兴趣模型
self.update_user_interests(user_id, query, retrieved_docs)
return {
"answer": response,
"retrieved_docs": retrieved_docs
}
实践项目:构建专业领域RAG问答系统
项目描述
在本项目中,你将构建一个专注于特定领域的RAG知识问答系统。你可以选择自己感兴趣的领域,如医疗健康、法律咨询、技术支持、教育学习等。
项目目标
- 建立一个完整的RAG应用,包括数据收集、向量化、检索和生成环节
- 实现引用溯源和知识库管理功能
- 提供友好的用户界面和反馈机制
- 部署应用并支持持续更新
项目步骤
-
定义应用领域和范围
- 选择特定专业领域
- 确定目标用户群体
- 定义核心功能需求
-
收集和准备数据
- 从公开来源收集领域知识
- 处理和清洗文档数据
- 分块并向量化文本内容
-
实现核心RAG功能
- 设置向量存储和检索机制
- 设计提示模板
- 优化检索和生成参数
-
添加辅助功能
- 实现引用溯源
- 创建知识库管理界面
- 设计用户反馈收集机制
-
构建界面
- 使用Streamlit或其他框架创建前端
- 优化用户交互流程
- 实现会话管理
-
测试和优化
- 评估系统回答质量
- 调整检索参数和生成参数
- 收集用户反馈并迭代改进
-
部署和维护
- 容器化应用
- 设置监控和日志
- 建立知识库更新机制
示例项目:医疗健康咨询助手
这是一个基于RAG技术的医疗健康咨询助手,帮助用户查询常见疾病、症状和健康建议的信息。
关键功能:
- 回答健康和医疗相关问题
- 提供权威医学参考来源
- 明确标示信息限制和免责声明
- 支持多语言查询
- 提供个性化健康建议
技术实现:
- 使用医学文献、指南和权威健康网站作为知识源
- 实现多层次检索策略:向量检索 + 关键词过滤
- 添加医学术语同义词扩展以优化检索
- 使用专业医学模板进行回答生成
- 实现引用溯源,标明信息来源
注意事项:
- 明确系统提供的是参考信息,不能替代专业医疗建议
- 严格过滤不准确或过时的医疗信息
- 医疗类RAG系统对准确性要求极高,需要特别关注评估和验证
拓展资源
学习资源
官方文档与教程
视频教程
- Building RAG Applications with LangChain
- Advanced RAG: Multi-Vector Retrieval Explained
- Full Stack RAG Tutorial with LlamaIndex
博客与文章
开源项目
- LangChain Templates:提供多种RAG应用模板
- privateGPT:本地部署的私有化RAG系统
- llama_index:用于构建RAG应用的开源框架
工具与服务
- Hugging Face Transformers:提供多种嵌入模型
- SentenceTransformers:高效的句子嵌入模型
- Streamlit:快速构建RAG应用界面的工具
- Qdrant:开源向量数据库,适合RAG应用
自测问题
-
RAG系统中引用溯源的主要目的是什么?
答案: 引用溯源的主要目的是提高回答的可信度和透明度,让用户知道回答来源于哪些文档,方便用户验证信息准确性,从而更信任系统生成的回答。引用溯源还有助于用户进一步探索相关内容,了解更多背景信息,同时也为系统开发者提供可追踪性,方便排查问题和改进系统。
-
RAG系统部署时,何种情况下适合选择本地部署,何种情况下适合云端部署?
答案: 本地部署适合个人使用、小型团队或对数据隐私要求极高的场景,以及网络条件有限的环境。本地部署通常具有更低的运行成本和更高的数据安全性。云端部署适合需要高可用性、高并发处理能力的企业级应用,或者需要全球用户访问的场景,以及数据量庞大需要弹性扩展的情况。云端部署通常提供更好的扩展性、可靠性和维护便利性,但可能面临更高的成本和潜在的数据安全问题。
-
多模态RAG系统中,如何处理图像和文本的融合检索?
答案: 多模态RAG系统处理图像和文本融合检索通常采用以下方法:1)使用如CLIP等模型将图像和文本编码到同一向量空间;2)对图像和文本向量进行加权融合生成查询向量;3)使用融合后的查询向量在向量数据库中检索相关内容;4)基于检索结果生成最终回答。这种方法允许用户同时使用图像和文本进行查询,系统能够理解两种模态信息并找到最相关的内容,显著提升了RAG系统的灵活性和应用范围。
-
为什么需要在RAG系统中实现用户反馈机制,它如何帮助提升系统质量?
答案: 用户反馈机制可以:1)收集检索和生成质量的数据指标;2)识别系统的常见问题和弱点;3)发现需要改进的查询类型;4)帮助系统调整检索策略和参数;5)为系统持续优化提供依据。通过分析用户反馈,开发者可以了解哪些查询类型容易失败,哪些文档检索效果不佳,从而有针对性地改进系统。反馈机制还可以实现自动学习,通过强化学习等方法自动调整系统参数,逐步提升RAG系统的整体质量和用户满意度。
-
RAG系统的知识库更新维护有哪些关键步骤?
答案: RAG系统知识库更新维护的关键步骤包括:1)定期爬取或获取新内容;2)验证新内容的质量和权威性;3)处理并分块新内容;4)向量化新内容并添加到向量存储;5)更新或移除过时的内容;6)对内容变更进行版本控制;7)定期重新评估系统性能,包括检索和生成质量;8)维护内容来源和元数据的一致性。理想的更新机制应该是自动化的,能够检测内容变更,并无缝集成新内容,同时保持系统的性能和响应速度。
作业/思考题
- 试比较混合检索策略(如向量检索+关键词检索)与单一向量检索在不同类型查询上的性能差异。哪种情况下混合检索更有优势?
- 如何评估RAG系统的回答质量?设计一个评估框架,包括客观指标和主观评价。
- 探讨在RAG系统中使用本地小型模型与调用大型API模型的权衡,从性能、成本、隐私和部署难度等角度分析。
- 设计一个RAG系统的A/B测试方案,用于比较不同的检索策略或提示模板对用户满意度的影响。
- 对于一个特定领域(如法律、医疗或教育)的RAG系统,讨论该领域可能面临的特殊挑战,以及相应的解决方案。