【自学30天掌握AI开发】第19天 - 构建RAG应用

Day19-构建RAG应用

封面图

学习目标

  1. 掌握RAG应用系统的整体架构设计和实现流程
  2. 学会设计和实现智能辅助功能,提升RAG系统交互质量
  3. 掌握RAG系统的部署与运维基本方法
  4. 能够独立构建一个完整的RAG知识问答应用

学习建议

时间规划(共4小时)

  • 理论学习:1小时(RAG辅助功能设计、部署与运维)
  • 代码实践:2小时(辅助功能实现、系统部署)
  • 项目实战:1小时(构建完整RAG应用)

学习方法

  • 先理解RAG系统辅助功能和部署运维的核心概念
  • 边学边做,跟随代码示例动手实践
  • 使用真实数据集测试自己构建的RAG应用
  • 遇到问题及时在线查找解决方案

核心知识点

  1. RAG辅助功能设计

    • 引用溯源与透明度
    • 知识库管理界面
    • 检索结果评估与反馈
  2. RAG系统部署与运维

    • 部署环境选择
    • 系统性能监控
    • 知识库更新维护
    • 安全与隐私保护
  3. RAG应用优化与扩展

    • 多模态RAG实现
    • 多语言支持
    • 个性化推荐

详细学习内容

1. RAG应用开发流程概述

RAG(检索增强生成)应用系统的完整开发流程包括以下关键步骤:

  1. 需求分析与规划

    • 明确应用场景和目标用户
    • 确定知识范围和数据来源
    • 设定性能指标和质量要求
    • 选择适合的技术栈和工具
  2. 系统架构设计

    • 划分核心模块:数据处理、检索、生成
    • 设计数据流和处理流程
    • 规划接口和集成点
    • 考虑系统的可扩展性和可维护性
  3. 开发环境搭建

    • 选择开发框架(如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知识问答系统。你可以选择自己感兴趣的领域,如医疗健康、法律咨询、技术支持、教育学习等。

项目目标

  1. 建立一个完整的RAG应用,包括数据收集、向量化、检索和生成环节
  2. 实现引用溯源和知识库管理功能
  3. 提供友好的用户界面和反馈机制
  4. 部署应用并支持持续更新

项目步骤

  1. 定义应用领域和范围

    • 选择特定专业领域
    • 确定目标用户群体
    • 定义核心功能需求
  2. 收集和准备数据

    • 从公开来源收集领域知识
    • 处理和清洗文档数据
    • 分块并向量化文本内容
  3. 实现核心RAG功能

    • 设置向量存储和检索机制
    • 设计提示模板
    • 优化检索和生成参数
  4. 添加辅助功能

    • 实现引用溯源
    • 创建知识库管理界面
    • 设计用户反馈收集机制
  5. 构建界面

    • 使用Streamlit或其他框架创建前端
    • 优化用户交互流程
    • 实现会话管理
  6. 测试和优化

    • 评估系统回答质量
    • 调整检索参数和生成参数
    • 收集用户反馈并迭代改进
  7. 部署和维护

    • 容器化应用
    • 设置监控和日志
    • 建立知识库更新机制

示例项目:医疗健康咨询助手

这是一个基于RAG技术的医疗健康咨询助手,帮助用户查询常见疾病、症状和健康建议的信息。

关键功能

  • 回答健康和医疗相关问题
  • 提供权威医学参考来源
  • 明确标示信息限制和免责声明
  • 支持多语言查询
  • 提供个性化健康建议

技术实现

  • 使用医学文献、指南和权威健康网站作为知识源
  • 实现多层次检索策略:向量检索 + 关键词过滤
  • 添加医学术语同义词扩展以优化检索
  • 使用专业医学模板进行回答生成
  • 实现引用溯源,标明信息来源

注意事项

  • 明确系统提供的是参考信息,不能替代专业医疗建议
  • 严格过滤不准确或过时的医疗信息
  • 医疗类RAG系统对准确性要求极高,需要特别关注评估和验证

拓展资源

学习资源

官方文档与教程

视频教程

博客与文章

开源项目

工具与服务

自测问题

  1. RAG系统中引用溯源的主要目的是什么?

    答案: 引用溯源的主要目的是提高回答的可信度和透明度,让用户知道回答来源于哪些文档,方便用户验证信息准确性,从而更信任系统生成的回答。引用溯源还有助于用户进一步探索相关内容,了解更多背景信息,同时也为系统开发者提供可追踪性,方便排查问题和改进系统。

  2. RAG系统部署时,何种情况下适合选择本地部署,何种情况下适合云端部署?

    答案: 本地部署适合个人使用、小型团队或对数据隐私要求极高的场景,以及网络条件有限的环境。本地部署通常具有更低的运行成本和更高的数据安全性。云端部署适合需要高可用性、高并发处理能力的企业级应用,或者需要全球用户访问的场景,以及数据量庞大需要弹性扩展的情况。云端部署通常提供更好的扩展性、可靠性和维护便利性,但可能面临更高的成本和潜在的数据安全问题。

  3. 多模态RAG系统中,如何处理图像和文本的融合检索?

    答案: 多模态RAG系统处理图像和文本融合检索通常采用以下方法:1)使用如CLIP等模型将图像和文本编码到同一向量空间;2)对图像和文本向量进行加权融合生成查询向量;3)使用融合后的查询向量在向量数据库中检索相关内容;4)基于检索结果生成最终回答。这种方法允许用户同时使用图像和文本进行查询,系统能够理解两种模态信息并找到最相关的内容,显著提升了RAG系统的灵活性和应用范围。

  4. 为什么需要在RAG系统中实现用户反馈机制,它如何帮助提升系统质量?

    答案: 用户反馈机制可以:1)收集检索和生成质量的数据指标;2)识别系统的常见问题和弱点;3)发现需要改进的查询类型;4)帮助系统调整检索策略和参数;5)为系统持续优化提供依据。通过分析用户反馈,开发者可以了解哪些查询类型容易失败,哪些文档检索效果不佳,从而有针对性地改进系统。反馈机制还可以实现自动学习,通过强化学习等方法自动调整系统参数,逐步提升RAG系统的整体质量和用户满意度。

  5. RAG系统的知识库更新维护有哪些关键步骤?

    答案: RAG系统知识库更新维护的关键步骤包括:1)定期爬取或获取新内容;2)验证新内容的质量和权威性;3)处理并分块新内容;4)向量化新内容并添加到向量存储;5)更新或移除过时的内容;6)对内容变更进行版本控制;7)定期重新评估系统性能,包括检索和生成质量;8)维护内容来源和元数据的一致性。理想的更新机制应该是自动化的,能够检测内容变更,并无缝集成新内容,同时保持系统的性能和响应速度。

作业/思考题

  1. 试比较混合检索策略(如向量检索+关键词检索)与单一向量检索在不同类型查询上的性能差异。哪种情况下混合检索更有优势?
  2. 如何评估RAG系统的回答质量?设计一个评估框架,包括客观指标和主观评价。
  3. 探讨在RAG系统中使用本地小型模型与调用大型API模型的权衡,从性能、成本、隐私和部署难度等角度分析。
  4. 设计一个RAG系统的A/B测试方案,用于比较不同的检索策略或提示模板对用户满意度的影响。
  5. 对于一个特定领域(如法律、医疗或教育)的RAG系统,讨论该领域可能面临的特殊挑战,以及相应的解决方案。

点击链接加入群聊【Aries - AIGC自学交流群】:https://qm.qq.com/q/q88ZpofKLY

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Aries.H

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值