在上一篇文章中,我们完成了基础的智能知识库系统,实现了文档上传、管理和基本的问答功能。但基础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.py的add_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. 测试优化效果
现在我们来测试优化后的系统效果:
-
重启所有服务:
- FastAPI 后端:
cd backend && uvicorn main:app --reload --port 8000 - OpenClaw 网关:
cd openclaw && pnpm run start - React 前端:
cd frontend && pnpm run dev
- FastAPI 后端:
-
测试多轮对话:
- 创建一个新会话
- 先问:“公司有哪些请假制度?”
- 然后问:“病假工资怎么算?”
- 系统应该能够理解上下文,不需要你重复说明"公司请假制度中的病假"
-
测试复杂问题:
- 上传一个包含多个主题的长文档
- 问一个需要综合多个文档片段才能回答的问题
- 系统应该能够检索到所有相关的片段,并给出完整准确的回答
-
测试多会话支持:
- 创建多个会话,分别讨论不同的主题
- 在不同会话之间切换,验证会话历史是否正确保存
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实现检索结果重排序,显著提升了回答准确性
- 实现了多查询检索和父文档检索,解决了上下文缺失问题
- 开发了对话历史压缩和上下文感知查询功能,优化了多轮对话体验
- 实现了完整的多会话支持,包括会话创建、切换和删除
- 优化了用户体验细节,包括可点击的引用来源和问题建议