RAG系统深度优化与多会话管理

在上一篇文章中,我们完成了基础的智能知识库系统,实现了文档上传、管理和基本的问答功能。但基础RAG系统在实际使用中还存在不少问题:检索结果不够精准、多轮对话上下文理解能力弱、缺乏会话管理等。这篇文章,我们将对RAG系统进行深度优化,实现检索结果重排序、多查询检索、对话历史管理和多会话支持,大幅提升系统的回答质量和用户体验。

摘要

  • 理解基础RAG系统的局限性与核心优化方向
  • 集成Cohere Rerank实现检索结果重排序,显著提升回答准确性
  • 实现多查询检索与父文档检索,解决上下文缺失问题
  • 完善对话历史管理,优化多轮对话上下文理解
  • 开发多会话支持功能,实现会话创建、切换与删除
  • 优化用户体验细节,包括引用来源跳转与问题建议

1. 基础RAG系统的局限性与优化方向

虽然我们的基础RAG系统已经能够工作,但在实际测试中你可能会发现以下问题:

1.1 基础RAG的常见问题

  • 检索精度不足:经常返回一些语义相关但实际不相关的文档片段
  • 上下文缺失:小块检索导致上下文不完整,大模型无法理解完整含义
  • 多轮对话能力弱:无法有效利用之前的对话历史,需要用户重复说明
  • 回答质量不稳定:同样的问题可能得到质量差异很大的回答
  • 缺乏会话管理:只有一个全局会话,无法同时处理多个主题

1.2 我们的优化方案

针对这些问题,我们将实施以下优化措施:

问题 解决方案 预期效果
检索精度不足 检索结果重排序(Cohere Rerank) 回答准确率提升30-50%
上下文缺失 父文档检索 + 更大的上下文窗口 回答更完整、更准确
召回率低 多查询检索(Multi-Query Retrieval) 找到更多相关的文档片段
多轮对话能力弱 对话历史压缩 + 上下文感知检索 更好地理解多轮对话上下文
缺乏会话管理 前端多会话支持 + 后端会话持久化 支持同时处理多个主题的对话

2. RAG检索效果核心优化

2.1 检索结果重排序(Reranking)

检索结果重排序是提升RAG系统效果最有效的方法之一。基础的向量相似度搜索只能找到语义上相似的文档,但无法判断这些文档与问题的实际相关性。重排序模型会对初步检索到的结果进行重新打分,只保留最相关的前几个片段。

我们选择Cohere Rerank作为重排序模型,它是目前效果最好的商用重排序模型之一,使用简单且价格便宜。

2.1.1 安装依赖与配置

进入backend目录,安装Cohere SDK:

cd backend
source venv/bin/activate  # Windows 使用 venv\Scripts\activate
pip install cohere

.env文件中添加Cohere API Key:

OPENAI_API_KEY=your-openai-api-key-here
CHROMA_PERSIST_DIRECTORY=./chroma_db
COHERE_API_KEY=your-cohere-api-key-here  # 新增

2.1.2 实现重排序功能

修改services/vector_service.py,添加重排序功能:

import os
import chromadb
import cohere
from chromadb.utils import embedding_functions
from dotenv import load_dotenv
from utils.text_splitter import split_text

load_dotenv()

class VectorService:
    def __init__(self):
        self.persist_directory = os.getenv("CHROMA_PERSIST_DIRECTORY", "./chroma_db")
        self.client = chromadb.PersistentClient(path=self.persist_directory)
        
        # 使用 OpenAI 嵌入模型
        self.embedding_function = embedding_functions.OpenAIEmbeddingFunction(
            api_key=os.getenv("OPENAI_API_KEY"),
            model_name="text-embedding-3-small"
        )
        
        # 初始化 Cohere 重排序客户端
        self.cohere_client = cohere.Client(os.getenv("COHERE_API_KEY"))
        
        # 获取或创建集合
        self.collection = self.client.get_or_create_collection(
            name="knowledge_base",
            embedding_function=self.embedding_function,
            metadata={"description": "企业级智能知识库向量集合"}
        )
    
    # ... 保留之前的 add_document 和 delete_document 方法 ...
    
    def search(self, query: str, top_k: int = 10, rerank_top_k: int = 3) -> list[dict]:
        """
        搜索与查询最相关的文档片段,并进行重排序
        """
        # 第一步:初步检索,获取较多的结果
        results = self.collection.query(
            query_texts=[query],
            n_results=top_k
        )
        
        if not results["ids"][0]:
            return []
        
        # 提取文档内容
        documents = results["documents"][0]
        metadatas = results["metadatas"][0]
        ids = results["ids"][0]
        distances = results["distances"][0] if results["distances"] else [None] * len(documents)
        
        # 第二步:使用 Cohere Rerank 进行重排序
        rerank_response = self.cohere_client.rerank(
            model="rerank-english-v3.0",  # 中文使用 rerank-multilingual-v3.0
            query=query,
            documents=documents,
            top_n=rerank_top_k
        )
        
        # 格式化重排序后的结果
        formatted_results = []
        for rerank_result in rerank_response.results:
            index = rerank_result.index
            formatted_results.append({
                "id": ids[index],
                "document": documents[index],
                "metadata": metadatas[index],
                "original_distance": distances[index],
                "rerank_score": rerank_result.relevance_score
            })
        
        # 按重排序分数降序排列
        formatted_results.sort(key=lambda x: x["rerank_score"], reverse=True)
        
        return formatted_results

2.2 多查询检索(Multi-Query Retrieval)

用户的提问方式往往比较随意,单一查询可能无法覆盖所有相关的文档。多查询检索技术会让大模型根据用户的原始问题,生成多个不同角度的查询,然后分别进行检索,最后合并结果。

修改services/vector_service.py,添加多查询检索功能:

from openai import OpenAI

class VectorService:
    def __init__(self):
        # ... 保留之前的初始化代码 ...
        self.openai_client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
    
    # ... 保留之前的方法 ...
    
    def generate_queries(self, original_query: str, num_queries: int = 3) -> list[str]:
        """
        根据原始问题生成多个不同角度的查询
        """
        prompt = f"""
        你是一个AI助手,你的任务是根据用户的原始问题,生成{num_queries}个不同角度的查询,用于从知识库中检索相关信息。
        
        原始问题:{original_query}
        
        请生成{num_queries}个不同的查询,每个查询应该从不同的角度表达相同的信息需求。查询应该简洁明了,直接针对核心问题。
        请只返回查询内容,每行一个,不要添加任何其他解释或编号。
        """
        
        response = self.openai_client.chat.completions.create(
            model="gpt-3.5-turbo",
            messages=[{"role": "user", "content": prompt}],
            temperature=0.7
        )
        
        queries = response.choices[0].message.content.strip().split('\n')
        queries = [q.strip() for q in queries if q.strip()]
        
        # 确保包含原始查询
        if original_query not in queries:
            queries.insert(0, original_query)
        
        return queries[:num_queries]
    
    def multi_query_search(self, query: str, top_k_per_query: int = 5, rerank_top_k: int = 3) -> list[dict]:
        """
        多查询检索:生成多个查询,分别检索后合并结果并重排序
        """
        # 生成多个查询
        queries = self.generate_queries(query)
        
        # 对每个查询进行检索
        all_results = []
        seen_ids = set()
        
        for q in queries:
            results = self.collection.query(
                query_texts=[q],
                n_results=top_k_per_query
            )
            
            for i in range(len(results["ids"][0])):
                doc_id = results["ids"][0][i]
                if doc_id not in seen_ids:
                    seen_ids.add(doc_id)
                    all_results.append({
                        "id": doc_id,
                        "document": results["documents"][0][i],
                        "metadata": results["metadatas"][0][i],
                        "original_distance": results["distances"][0][i] if results["distances"] else None
                    })
        
        if not all_results:
            return []
        
        # 对合并后的结果进行重排序
        documents = [r["document"] for r in all_results]
        rerank_response = self.cohere_client.rerank(
            model="rerank-multilingual-v3.0",
            query=query,
            documents=documents,
            top_n=rerank_top_k
        )
        
        # 格式化结果
        formatted_results = []
        for rerank_result in rerank_response.results:
            index = rerank_result.index
            result = all_results[index]
            result["rerank_score"] = rerank_result.relevance_score
            formatted_results.append(result)
        
        # 按重排序分数降序排列
        formatted_results.sort(key=lambda x: x["rerank_score"], reverse=True)
        
        return formatted_results

2.3 父文档检索(Parent Document Retrieval)

基础的小块检索虽然能提高检索精度,但会导致上下文缺失。父文档检索技术将文档拆分为较小的子块用于检索,但在生成回答时返回包含该子块的较大父块,从而提供更完整的上下文。

修改utils/text_splitter.py,实现父子分块:

from langchain_text_splitters import RecursiveCharacterTextSplitter

def split_text_with_parent(
    text: str, 
    parent_chunk_size: int = 2000, 
    parent_chunk_overlap: int = 200,
    child_chunk_size: int = 512, 
    child_chunk_overlap: int = 50
) -> list[dict]:
    """
    将文本拆分为父子块结构
    """
    # 首先拆分为较大的父块
    parent_splitter = RecursiveCharacterTextSplitter(
        chunk_size=parent_chunk_size,
        chunk_overlap=parent_chunk_overlap,
        length_function=len,
        is_separator_regex=False,
    )
    
    parent_chunks = parent_splitter.split_text(text)
    
    # 然后将每个父块拆分为较小的子块
    child_splitter = RecursiveCharacterTextSplitter(
        chunk_size=child_chunk_size,
        chunk_overlap=child_chunk_overlap,
        length_function=len,
        is_separator_regex=False,
    )
    
    all_chunks = []
    
    for parent_index, parent_chunk in enumerate(parent_chunks):
        child_chunks = child_splitter.split_text(parent_chunk)
        
        for child_index, child_chunk in enumerate(child_chunks):
            all_chunks.append({
                "parent_index": parent_index,
                "child_index": child_index,
                "parent_content": parent_chunk,
                "child_content": child_chunk
            })
    
    return all_chunks

修改services/vector_service.pyadd_document方法,支持父子分块:

def add_document(self, document_id: str, content: str, metadata: dict = None) -> dict:
    """
    添加文档到向量数据库(使用父子分块)
    """
    # 使用父子分块策略
    chunks = split_text_with_parent(content)
    
    # 生成块 ID
    chunk_ids = [f"{document_id}_chunk_{c['parent_index']}_{c['child_index']}" for c in chunks]
    
    # 准备元数据
    chunk_metadatas = []
    for i, chunk in enumerate(chunks):
        chunk_metadata = {
            "document_id": document_id,
            "parent_index": chunk["parent_index"],
            "child_index": chunk["child_index"],
            "parent_content": chunk["parent_content"],
            "content_preview": chunk["child_content"][:100] + "..." if len(chunk["child_content"]) > 100 else chunk["child_content"]
        }
        if metadata:
            chunk_metadata.update(metadata)
        chunk_metadatas.append(chunk_metadata)
    
    # 添加到向量数据库(存储子块内容用于检索)
    self.collection.add(
        ids=chunk_ids,
        documents=[c["child_content"] for c in chunks],
        metadatas=chunk_metadatas
    )
    
    return {
        "document_id": document_id,
        "chunk_count": len(chunks),
        "success": True
    }

修改search方法,返回父块内容用于生成回答:

def search(self, query: str, top_k: int = 10, rerank_top_k: int = 3) -> list[dict]:
    """
    搜索与查询最相关的文档片段,并进行重排序(返回父块内容)
    """
    # 第一步:初步检索,获取较多的结果
    results = self.collection.query(
        query_texts=[query],
        n_results=top_k
    )
    
    if not results["ids"][0]:
        return []
    
    # 提取文档内容和元数据
    metadatas = results["metadatas"][0]
    ids = results["ids"][0]
    distances = results["distances"][0] if results["distances"] else [None] * len(metadatas)
    
    # 提取父块内容用于重排序
    documents = [m["parent_content"] for m in metadatas]
    
    # 第二步:使用 Cohere Rerank 进行重排序
    rerank_response = self.cohere_client.rerank(
        model="rerank-multilingual-v3.0",
        query=query,
        documents=documents,
        top_n=rerank_top_k
    )
    
    # 格式化重排序后的结果
    formatted_results = []
    seen_parent_indices = set()
    
    for rerank_result in rerank_response.results:
        index = rerank_result.index
        metadata = metadatas[index]
        
        # 避免重复返回同一个父块
        parent_key = f"{metadata['document_id']}_{metadata['parent_index']}"
        if parent_key in seen_parent_indices:
            continue
        seen_parent_indices.add(parent_key)
        
        formatted_results.append({
            "id": ids[index],
            "document": metadata["parent_content"],  # 返回父块内容
            "child_document": metadata["content_preview"],
            "metadata": metadata,
            "original_distance": distances[index],
            "rerank_score": rerank_result.relevance_score
        })
    
    # 按重排序分数降序排列
    formatted_results.sort(key=lambda x: x["rerank_score"], reverse=True)
    
    return formatted_results[:rerank_top_k]

2.4 更新API接口

修改main.py,更新搜索接口以使用优化后的搜索方法:

@app.post("/api/vector/search")
async def search_documents(request: SearchRequest):
    try:
        results = vector_service.multi_query_search(
            query=request.query,
            top_k_per_query=5,
            rerank_top_k=request.top_k
        )
        return {"results": results, "count": len(results)}
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

3. 对话历史管理与多轮对话优化

3.1 对话历史压缩

随着对话的进行,历史消息会越来越长,不仅会消耗更多的token,还可能导致大模型分心。我们可以使用大模型对对话历史进行压缩,只保留关键信息。

backend目录下创建services/chat_service.py

from openai import OpenAI
import os
from dotenv import load_dotenv

load_dotenv()

class ChatService:
    def __init__(self):
        self.openai_client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
    
    def compress_history(self, messages: list[dict], max_tokens: int = 1000) -> list[dict]:
        """
        压缩对话历史,只保留关键信息
        """
        if len(messages) <= 4:  # 保留最近的2轮对话
            return messages
        
        # 保留系统提示和最近的2轮对话
        recent_messages = messages[-4:]
        
        # 压缩更早的历史
        older_messages = messages[:-4]
        
        if not older_messages:
            return recent_messages
        
        prompt = f"""
        请将以下对话历史压缩成一段简洁的摘要,保留所有关键信息:
        
        {older_messages}
        
        摘要应该简洁明了,不超过{max_tokens}个token。
        """
        
        response = self.openai_client.chat.completions.create(
            model="gpt-3.5-turbo",
            messages=[{"role": "user", "content": prompt}],
            temperature=0.3
        )
        
        compressed_summary = response.choices[0].message.content
        
        # 构建压缩后的历史
        compressed_history = [
            {"role": "system", "content": f"之前的对话摘要:{compressed_summary}"}
        ] + recent_messages
        
        return compressed_history
    
    def generate_context_aware_query(self, query: str, history: list[dict]) -> str:
        """
        根据对话历史生成上下文感知的查询
        """
        if len(history) < 2:  # 没有足够的历史
            return query
        
        prompt = f"""
        给定以下对话历史和用户的最新问题,请生成一个上下文感知的查询,用于从知识库中检索相关信息。
        
        对话历史:
        {history[-6:]}  # 保留最近的3轮对话
        
        用户最新问题:{query}
        
        请生成一个完整的查询,包含所有必要的上下文信息。查询应该简洁明了,直接针对核心问题。
        请只返回查询内容,不要添加任何其他解释。
        """
        
        response = self.openai_client.chat.completions.create(
            model="gpt-3.5-turbo",
            messages=[{"role": "user", "content": prompt}],
            temperature=0.3
        )
        
        context_aware_query = response.choices[0].message.content.strip()
        
        return context_aware_query

# 创建全局实例
chat_service = ChatService()

3.2 更新OpenClaw系统提示词

修改openclaw/.claw/config.yaml的系统提示词,优化多轮对话处理:

system_prompt: |
  你是一个专业的企业知识库助手。你的任务是基于企业内部知识库的内容,准确、清晰地回答用户的问题。
  
  重要规则:
  1. 当用户询问任何与企业内部知识、产品信息、规章制度、流程规范相关的问题时,**必须先调用 rag-retrieval 技能**获取相关信息。
  2. 你的回答必须严格基于检索到的文档内容,不得编造信息。
  3. 如果检索结果中没有相关信息,请明确告诉用户"知识库中没有找到相关信息",不要猜测。
  4. 回答时要引用来源,格式为:[来源: 文件名]
  5. 保持回答简洁明了,重点突出。
  6. 在多轮对话中,要结合之前的对话上下文理解用户的问题。如果用户的问题指代不明确,可以适当追问。

4. 多会话支持实现

4.1 前端多会话管理

首先,我们需要在前端状态管理中添加会话相关的状态。修改src/store/index.ts

import { create } from 'zustand';

interface Message {
  id: string;
  role: 'user' | 'assistant';
  content: string;
  sources?: any[];
  timestamp: Date;
}

interface Session {
  id: string;
  title: string;
  createdAt: Date;
}

interface AppState {
  // 会话状态
  sessions: Session[];
  currentSessionId: string | null;
  messages: Message[];
  isLoading: boolean;
  
  // 文档状态
  documents: any[];
  isUploading: boolean;
  uploadProgress: number;
  
  // 方法
  setSessions: (sessions: Session[]) => void;
  addSession: (session: Session) => void;
  deleteSession: (sessionId: string) => void;
  setCurrentSessionId: (id: string | null) => void;
  addMessage: (message: Message) => void;
  setMessages: (messages: Message[]) => void;
  setIsLoading: (loading: boolean) => void;
  setDocuments: (documents: any[]) => void;
  setIsUploading: (uploading: boolean) => void;
  setUploadProgress: (progress: number) => void;
  clearMessages: () => void;
}

export const useAppStore = create<AppState>((set) => ({
  sessions: [],
  currentSessionId: null,
  messages: [],
  isLoading: false,
  documents: [],
  isUploading: false,
  uploadProgress: 0,
  
  setSessions: (sessions) => set({ sessions }),
  addSession: (session) => set((state) => ({ 
    sessions: [...state.sessions, session] 
  })),
  deleteSession: (sessionId) => set((state) => ({ 
    sessions: state.sessions.filter(s => s.id !== sessionId),
    currentSessionId: state.currentSessionId === sessionId ? null : state.currentSessionId,
    messages: state.currentSessionId === sessionId ? [] : state.messages
  })),
  setCurrentSessionId: (id) => set({ currentSessionId: id }),
  addMessage: (message) => set((state) => ({ 
    messages: [...state.messages, message] 
  })),
  setMessages: (messages) => set({ messages }),
  setIsLoading: (loading) => set({ isLoading: loading }),
  setDocuments: (documents) => set({ documents }),
  setIsUploading: (uploading) => set({ isUploading: uploading }),
  setUploadProgress: (progress) => set({ uploadProgress: progress }),
  clearMessages: () => set({ messages: [] }),
}));

4.2 会话列表组件

创建src/components/SessionList.tsx

import { useState } from 'react';
import { List, Button, Popconfirm, message } from 'antd';
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons';
import { openclawApiService } from '../api';
import { useAppStore } from '../store';

const SessionList = () => {
  const { sessions, setSessions, addSession, deleteSession, currentSessionId, setCurrentSessionId, setMessages } = useAppStore();
  const [isCreating, setIsCreating] = useState(false);
  
  // 创建新会话
  const handleCreateSession = async () => {
    setIsCreating(true);
    try {
      const response = await openclawApiService.createSession('新会话');
      const newSession = {
        id: response.data.data.id,
        title: response.data.data.title,
        createdAt: new Date(response.data.data.createdAt)
      };
      
      addSession(newSession);
      setCurrentSessionId(newSession.id);
      setMessages([]);
      message.success('新会话创建成功');
    } catch (error: any) {
      message.error(`创建会话失败: ${error.message}`);
    } finally {
      setIsCreating(false);
    }
  };
  
  // 切换会话
  const handleSwitchSession = async (sessionId: string) => {
    setCurrentSessionId(sessionId);
    setMessages([]);
    
    try {
      const response = await openclawApiService.getSessionHistory(sessionId);
      const messages = response.data.data.messages.map((msg: any) => ({
        id: msg.id,
        role: msg.role,
        content: msg.content,
        timestamp: new Date(msg.createdAt)
      }));
      setMessages(messages);
    } catch (error: any) {
      message.error(`加载会话历史失败: ${error.message}`);
    }
  };
  
  // 删除会话
  const handleDeleteSession = async (sessionId: string, e: React.MouseEvent) => {
    e.stopPropagation();
    
    try {
      // 这里可以调用后端API删除会话
      deleteSession(sessionId);
      message.success('会话删除成功');
    } catch (error: any) {
      message.error(`删除会话失败: ${error.message}`);
    }
  };
  
  return (
    <div style={{ padding: '16px 0' }}>
      <Button 
        type="primary" 
        icon={<PlusOutlined />} 
        onClick={handleCreateSession}
        loading={isCreating}
        style={{ marginBottom: 16, width: '100%' }}
      >
        新建会话
      </Button>
      
      <List
        dataSource={sessions}
        renderItem={(session) => (
          <List.Item
            onClick={() => handleSwitchSession(session.id)}
            style={{ 
              cursor: 'pointer',
              padding: '8px 12px',
              borderRadius: 4,
              backgroundColor: session.id === currentSessionId ? '#e6f7ff' : 'transparent',
              borderLeft: session.id === currentSessionId ? '3px solid #1890ff' : '3px solid transparent'
            }}
            actions={[
              <Popconfirm
                key="delete"
                title="确定要删除这个会话吗?"
                onConfirm={(e) => handleDeleteSession(session.id, e!)}
                okText="确定"
                cancelText="取消"
              >
                <Button type="text" danger size="small" icon={<DeleteOutlined />} />
              </Popconfirm>
            ]}
          >
            <List.Item.Meta
              title={session.title}
              description={session.createdAt.toLocaleString()}
            />
          </List.Item>
        )}
      />
    </div>
  );
};

export default SessionList;

4.3 更新聊天页面

修改src/pages/ChatPage.tsx,集成会话列表:

import { useState, useEffect, useRef } from 'react';
import { Layout, Card, Input, Button, List, Avatar, Spin, Typography, Tag, Empty } from 'antd';
import { SendOutlined, UserOutlined, RobotOutlined } from '@ant-design/icons';
import { openclawApiService } from '../api';
import { useAppStore } from '../store';
import SessionList from '../components/SessionList';
import './ChatPage.css';

const { Sider, Content } = Layout;
const { Text, Paragraph } = Typography;

const ChatPage = () => {
  const { 
    currentSessionId, 
    messages, 
    addMessage, 
    setMessages, 
    isLoading, 
    setIsLoading 
  } = useAppStore();
  
  const [inputValue, setInputValue] = useState('');
  const messagesEndRef = useRef<HTMLDivElement>(null);
  
  // 自动滚动到底部
  useEffect(() => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
  }, [messages]);
  
  // 处理发送消息
  const handleSend = async () => {
    if (!inputValue.trim() || !currentSessionId || isLoading) return;
    
    const userMessage = {
      id: Date.now().toString(),
      role: 'user' as const,
      content: inputValue.trim(),
      timestamp: new Date(),
    };
    
    addMessage(userMessage);
    setInputValue('');
    setIsLoading(true);
    
    try {
      // 发送消息并获取流式响应
      const response = await fetch(`http://localhost:18789/api/sessions/${currentSessionId}/messages`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          role: 'user',
          content: userMessage.content,
          stream: true,
        }),
      });
      
      if (!response.ok) {
        throw new Error('请求失败');
      }
      
      const reader = response.body?.getReader();
      const decoder = new TextDecoder();
      let assistantContent = '';
      
      // 创建一个空的助手消息
      const assistantMessageId = (Date.now() + 1).toString();
      addMessage({
        id: assistantMessageId,
        role: 'assistant',
        content: '',
        timestamp: new Date(),
      });
      
      // 读取流式响应
      while (reader) {
        const { done, value } = await reader.read();
        if (done) break;
        
        const chunk = decoder.decode(value);
        const lines = chunk.split('\n').filter(line => line.trim() !== '');
        
        for (const line of lines) {
          if (line.startsWith('data: ')) {
            const data = line.slice(6);
            if (data === '[DONE]') continue;
            
            try {
              const parsed = JSON.parse(data);
              if (parsed.content) {
                assistantContent += parsed.content;
                
                // 更新助手消息
                setMessages(prev => prev.map(msg => 
                  msg.id === assistantMessageId 
                    ? { ...msg, content: assistantContent }
                    : msg
                ));
              }
            } catch (e) {
              console.error('解析响应失败:', e);
            }
          }
        }
      }
      
    } catch (error: any) {
      console.error('发送消息失败:', error);
      addMessage({
        id: Date.now().toString(),
        role: 'assistant',
        content: `抱歉,发生了一个错误: ${error.message}`,
        timestamp: new Date(),
      });
    } finally {
      setIsLoading(false);
    }
  };
  
  // 处理回车键发送
  const handleKeyPress = (e: React.KeyboardEvent) => {
    if (e.key === 'Enter' && !e.shiftKey) {
      e.preventDefault();
      handleSend();
    }
  };
  
  return (
    <Layout style={{ height: 'calc(100vh - 120px)' }}>
      <Sider width={250} style={{ background: '#fff', borderRight: '1px solid #f0f0f0' }}>
        <div style={{ padding: '16px', borderBottom: '1px solid #f0f0f0' }}>
          <h3 style={{ margin: 0 }}>会话列表</h3>
        </div>
        <SessionList />
      </Sider>
      
      <Content>
        <Card 
          title="智能问答" 
          style={{ height: '100%', display: 'flex', flexDirection: 'column', borderRadius: 0 }}
        >
          <div className="chat-messages" style={{ flex: 1, overflowY: 'auto', marginBottom: 16 }}>
            {!currentSessionId ? (
              <Empty 
                description="请创建或选择一个会话开始对话" 
                style={{ marginTop: 100 }}
              />
            ) : messages.length === 0 ? (
              <Empty 
                description="开始与知识库对话吧!" 
                style={{ marginTop: 100 }}
              />
            ) : (
              <List
                dataSource={messages}
                renderItem={(message) => (
                  <div 
                    className={`message-item ${message.role}`}
                    style={{ 
                      display: 'flex', 
                      marginBottom: 16,
                      justifyContent: message.role === 'user' ? 'flex-end' : 'flex-start'
                    }}
                  >
                    {message.role === 'assistant' && (
                      <Avatar icon={<RobotOutlined />} style={{ marginRight: 12, backgroundColor: '#1890ff' }} />
                    )}
                    
                    <div 
                      className="message-content"
                      style={{ 
                        maxWidth: '70%',
                        padding: '12px 16px',
                        borderRadius: 8,
                        backgroundColor: message.role === 'user' ? '#1890ff' : '#f0f0f0',
                        color: message.role === 'user' ? '#fff' : '#000'
                      }}
                    >
                      <Paragraph style={{ margin: 0, whiteSpace: 'pre-wrap' }}>
                        {message.content}
                      </Paragraph>
                      
                      {message.sources && message.sources.length > 0 && (
                        <div style={{ marginTop: 8 }}>
                          <Text type="secondary" style={{ fontSize: 12 }}>引用来源:</Text>
                          {message.sources.map((source: any, index: number) => (
                            <Tag key={index} size="small" style={{ marginLeft: 4 }}>
                              {source.metadata?.fileName || '未知'}
                            </Tag>
                          ))}
                        </div>
                      )}
                    </div>
                    
                    {message.role === 'user' && (
                      <Avatar icon={<UserOutlined />} style={{ marginLeft: 12, backgroundColor: '#52c41a' }} />
                    )}
                  </div>
                )}
              />
            )}
            
            {isLoading && messages.length > 0 && messages[messages.length - 1].role === 'user' && (
              <div style={{ display: 'flex', marginBottom: 16 }}>
                <Avatar icon={<RobotOutlined />} style={{ marginRight: 12, backgroundColor: '#1890ff' }} />
                <div style={{ padding: '12px 16px', borderRadius: 8, backgroundColor: '#f0f0f0' }}>
                  <Spin size="small" />
                </div>
              </div>
            )}
            
            <div ref={messagesEndRef} />
          </div>
          
          <div className="chat-input" style={{ display: 'flex', gap: 8 }}>
            <Input.TextArea
              value={inputValue}
              onChange={(e) => setInputValue(e.target.value)}
              onKeyPress={handleKeyPress}
              placeholder="输入你的问题..."
              autoSize={{ minRows: 1, maxRows: 4 }}
              disabled={isLoading || !currentSessionId}
            />
            <Button 
              type="primary" 
              icon={<SendOutlined />} 
              onClick={handleSend}
              loading={isLoading}
              disabled={!inputValue.trim() || !currentSessionId}
            >
              发送
            </Button>
          </div>
        </Card>
      </Content>
    </Layout>
  );
};

export default ChatPage;

5. 用户体验细节优化

5.1 引用来源可点击跳转

修改src/pages/ChatPage.tsx,让引用来源可以点击跳转到对应的文档:

{message.sources && message.sources.length > 0 && (
  <div style={{ marginTop: 8 }}>
    <Text type="secondary" style={{ fontSize: 12 }}>引用来源:</Text>
    {message.sources.map((source: any, index: number) => (
      <Tag 
        key={index} 
        size="small" 
        style={{ marginLeft: 4, cursor: 'pointer' }}
        onClick={() => {
          // 跳转到文档管理页面并高亮该文档
          window.location.href = `/?documentId=${source.metadata?.document_id}`;
        }}
      >
        {source.metadata?.fileName || '未知'}
      </Tag>
    ))}
  </div>
)}

5.2 问题建议

在聊天页面添加常见问题建议,帮助用户快速开始:

{!currentSessionId ? (
  <Empty 
    description="请创建或选择一个会话开始对话" 
    style={{ marginTop: 100 }}
  />
) : messages.length === 0 ? (
  <div style={{ textAlign: 'center', marginTop: 100 }}>
    <Typography.Title level={4}>开始与知识库对话吧!</Typography.Title>
    <div style={{ marginTop: 24, display: 'flex', flexWrap: 'wrap', justifyContent: 'center', gap: 12 }}>
      <Button 
        onClick={() => setInputValue('公司有哪些请假制度?')}
      >
        公司有哪些请假制度?
      </Button>
      <Button 
        onClick={() => setInputValue('如何申请报销?')}
      >
        如何申请报销?
      </Button>
      <Button 
        onClick={() => setInputValue('新员工入职流程是什么?')}
      >
        新员工入职流程是什么?
      </Button>
    </div>
  </div>
) : (
  // ... 消息列表 ...
)}

6. 测试优化效果

现在我们来测试优化后的系统效果:

  1. 重启所有服务

    • FastAPI 后端:cd backend && uvicorn main:app --reload --port 8000
    • OpenClaw 网关:cd openclaw && pnpm run start
    • React 前端:cd frontend && pnpm run dev
  2. 测试多轮对话

    • 创建一个新会话
    • 先问:“公司有哪些请假制度?”
    • 然后问:“病假工资怎么算?”
    • 系统应该能够理解上下文,不需要你重复说明"公司请假制度中的病假"
  3. 测试复杂问题

    • 上传一个包含多个主题的长文档
    • 问一个需要综合多个文档片段才能回答的问题
    • 系统应该能够检索到所有相关的片段,并给出完整准确的回答
  4. 测试多会话支持

    • 创建多个会话,分别讨论不同的主题
    • 在不同会话之间切换,验证会话历史是否正确保存

7. 项目结构更新

至此,我们的项目目录已经更新为:

enterprise-knowledge-base/
├── .gitignore
├── backend/
│   ├── venv/
│   ├── .env
│   ├── main.py
│   ├── requirements.txt
│   ├── uploads/
│   ├── utils/
│   │   ├── text_splitter.py
│   │   └── document_parser.py
│   └── services/
│       ├── vector_service.py
│       ├── document_service.py
│       └── chat_service.py
├── openclaw/
│   ├── node_modules/
│   ├── package.json
│   ├── .claw/
│   │   └── config.yaml
│   └── skills/
│       ├── hello-world/
│       ├── document-parser/
│       └── rag-retrieval/
└── frontend/
    ├── node_modules/
    ├── src/
    │   ├── api/
    │   │   └── index.ts
    │   ├── components/
    │   │   └── SessionList.tsx
    │   ├── pages/
    │   │   ├── DocumentPage.tsx
    │   │   ├── ChatPage.tsx
    │   │   ├── SettingsPage.tsx
    │   │   └── ChatPage.css
    │   ├── store/
    │   │   └── index.ts
    │   ├── utils/
    │   ├── App.tsx
    │   ├── App.css
    │   └── main.tsx
    ├── index.html
    ├── package.json
    └── tsconfig.json

8. 小结

本篇文章我们完成了以下事情:

  • 分析了基础RAG系统的局限性,制定了全面的优化方案
  • 集成了Cohere Rerank实现检索结果重排序,显著提升了回答准确性
  • 实现了多查询检索和父文档检索,解决了上下文缺失问题
  • 开发了对话历史压缩和上下文感知查询功能,优化了多轮对话体验
  • 实现了完整的多会话支持,包括会话创建、切换和删除
  • 优化了用户体验细节,包括可点击的引用来源和问题建议