图书智能检索系统

前言

基于检索增强生成(RAG)技术的智能检索系统完整实现。这个项目展示了如何将传统文学作品与现代AI技术相结合,为用户提供智能化的文学问答体验。

项目效果

请添加图片描述

请添加图片描述

技术栈

  • 核心框架: Python 3.13
  • 编辑器: PyCharm
  • AI模型: DeepSeek API
  • 向量化: TF-IDF + scikit-learn
  • 中文分词: jieba
  • 文档处理: 自定义文本分块算法
  • 缓存机制: pickle序列化
  • 可视化界面:streamlit

整个视频代码讲解:https://www.bilibili.com/video/BV1kr1aBCErf/?spm_id_from=333.337.search-card.all.click&vd_source=590c990e41d7897748699e814ed56603

sklearn中文社区:https://scikit-learn.org.cn/

python零基础入门:https://www.bilibili.com/video/BV1R4sBz1E9q?spm_id_from=333.788.videopod.episodes&vd_source=590c990e41d7897748699e814ed56603&p=2

numpy中文文档:https://numpy.com.cn/

streamlit : https://docs.streamlit.io/

系统架构

用户问题 → 文本预处理 → 向量化 → 相似度检索 → 上下文构建 → AI生成答案

核心类设计SmartLibRAG

初始化设计思路

def __init__(self, api_key: str, docs_dir: str = "docs"):

设计这个类的主要目标是提供一个可配置、可扩展的框架,用于加载文档、预处理文本、构建向量索引以及执行问答任务。

  1. 依赖注入: API密钥通过参数传入,便于测试和部署
  2. 路径抽象: 使用pathlib.Path而非字符串处理路径
  3. 缓存优先: 预设缓存目录,避免重复计算
  4. 组件分离: 将OpenAI客户端、向量化器、文档存储等分别管理

文档加载策略

def load_documents(self) -> None:

设计亮点:

  • 使用生成器模式遍历文件
  • 异常处理确保单个文件错误不影响整体加载
  • 结构化存储文档元信息(文件名、路径、内容)

实际应用建议:
在生产环境中,建议添加文件格式验证和编码检测。

智能文本分块算法

def split_text_into_chunks(self, text: str, chunk_size: int = 300, overlap: int = 50):

这是整个系统的核心算法之一。我采用了基于语义边界的分块策略:

算法特点:

  1. 语义完整性: 按句号、问号、感叹号分句,保持语义完整
  2. 动态调整: 根据句子长度动态调整块大小
  3. 重叠处理: 虽然参数中有overlap,但当前实现更注重语义边界

优化建议:

# 可以考虑添加重叠逻辑
if len(chunks) > 1 and overlap > 0:
    # 添加重叠文本逻辑
    pass

缓存机制设计

def preprocess_documents(self) -> None:

缓存策略:

  • 文档块缓存:避免重复分词和分块

  • 向量缓存:避免重复向量化计算

  • 增量更新:检测文件变化(可扩展)

这种设计在处理大型文档集合时能显著提升性能。

中文NLP处理

def chinese_tokenizer(self, text):

中文处理的挑战:

  1. 分词准确性: 使用jieba处理中文分词
  2. 停用词过滤: 自定义停用词库,提升检索精度
  3. 标点符号处理: 智能过滤标点,保留有意义的词汇,例如:,。!?;:“”()【】《》、
  4. 单字符过滤: 避免无意义的单字符干扰

jieba文档:https://github.com/fxsjy/jieba

常见停用词:

['的', '了', '在', '是', '我', '有', '和', '就', '不', '人', '都', '一', '一个', '上', '也', '很', '到', '说', '要', '去', '你', '会', '着', '没有', '看', '好', '自己', '这']

实现细节

if (word and 
    len(word) > 1 and  # 长度过滤
    word not in self.stopwords and  # 停用词过滤
    not word.isspace() and  # 空白字符过滤
    not all(char in punctuation for char in word)):  # 标点过滤

向量索引构建

请添加图片描述

示例代码

tf = TfidfVectorizer(norm='l2')
train = ["Chinese Beijing Chinese", "Chinese Chinese shanghai", "Chinese Macao", "Tokyo Japan, Chinese"]
# 训练得到语料库
tf.fit(train)
print(tf.get_feature_names_out()) # ['beijing' 'chinese' 'japan' 'macao' 'shanghai' 'tokyo']
test = ["Chinese Chinese Chinese Tokyo Japan"]
tf_fit = tf.transform(test)
print(tf_fit.toarray()) # [[0.        0.7420575 0.4739993 0.        0.        0.4739993]]

公式计算

"""
	TF-IDE = 词频(TF) × 逆文档频率(IDF)
	TF = 某个词在文章中的出现次数 / 文章的总次数
	IDF = log(语料库的文档总数 / 包含该词的文档数 + 1)
"""
def build_vector_index(self) -> None:

技术选择说明:

  • TF-IDF vs 深度学习嵌入: 考虑到部署成本和效果平衡,选择TF-IDF
  • 参数调优:
    • max_features=5000: 控制词汇表大小
    • ngram_range=(1, 2): 包含单词和双词组合
    • min_df=1, max_df=0.95: 过滤极端频率词汇

缓存恢复机制

# 巧妙的缓存恢复设计
self.vectorizer = TfidfVectorizer(
    tokenizer=self.chinese_tokenizer,
    vocabulary=cache_data['vocabulary']
)
self.vectorizer.idf_ = cache_data['idf']

检索算法优化

def search_relevant_chunks(self, query: str, top_k: int = 10, similarity_threshold: float = 0.01):

检索策略

  1. 阈值过滤: 先过滤低相似度结果
  2. 排序优化: 只对有效结果排序
  3. 数量控制: top_k限制返回数量

性能优化点

# 避免对所有结果排序,只处理有效结果
valid_indices = np.where(similarities > similarity_threshold)[0]
sorted_indices = valid_indices[np.argsort(similarities[valid_indices])[::-1]]

AI生成模块

def generate_answer(self, query: str, context_chunks: List[Dict]) -> str:

Prompt工程

  1. 系统角色定义: 明确AI作为红楼梦专家的身份
  2. 回答规范: 详细的回答要求和格式规范
  3. 上下文构建: 结构化的文档片段组织

deepseek提示词库:https://api-docs.deepseek.com/zh-cn/prompt-library/

错误处理策略

try:
    # API调用
except Exception as e:
    error_type = type(e).__name__
    error_details = str(e) if str(e) else "未知错误"
    return f"生成答案时出错: {error_type} - {error_details}"

系统提示词

"""
# Role: 红楼梦研究专家

## Profile
- language: 中文
- description: 精通《红楼梦》文本及红学研究,能够深入解析作品的人物、情节、诗词及文化内涵
- background: 多年从事《红楼梦》研究与教学,参与过多项红学课题研究
- personality: 严谨细致,富有文人气质,善于引经据典
- expertise: 文本分析、人物研究、诗词鉴赏、文化解读
- target_audience: 红学爱好者、文学研究者、学生群体

## Skills

1. 文本解析能力
   - 情节梳理: 能准确还原小说情节脉络
   - 细节把握: 对文本细节有敏锐洞察力
   - 人物分析: 深入剖析人物性格与命运
   - 诗词解读: 精准解析书中诗词内涵

2. 学术研究能力
   - 文献考证: 熟悉各类红学研究成果
   - 文化阐释: 揭示作品背后的文化内涵
   - 比较研究: 能与其他文学作品进行对比
   - 版本鉴别: 了解不同版本差异

## Rules

1. 回答原则:
   - 基于文本: 所有回答必须严格依据原著文本
   - 严谨准确: 不妄加猜测,不传播未经考证的观点
   - 深度解析: 透过表面现象揭示深层含义
   - 客观公正: 避免个人主观臆断

2. 行为准则:
   - 引经据典: 重要观点需引用原文佐证
   - 语言典雅: 保持与原著相称的文雅风格
   - 层次分明: 回答要有逻辑性和条理性
   - 深入浅出: 复杂问题要解释得通俗易懂

3. 限制条件:
   - 不涉争议: 避免介入红学争议性话题
   - 不妄评续作: 对后四十回保持审慎态度
   - 不越文本: 不脱离文本过度解读
   - 不代作者: 不以作者口吻发表观点

## Workflows

- 目标: 提供专业准确的红楼梦解读
- 步骤 1: 仔细理解用户问题,明确询问重点
- 步骤 2: 检索相关文本段落,确认信息准确性
- 步骤 3: 组织回答内容,适当引用原文
- 步骤 4: 以典雅文风呈现完整解答
- 预期结果: 用户获得权威、深入的红楼梦知识

## Initialization
作为红楼梦研究专家,你必须遵守上述Rules,按照Workflows执行任务。
"""

系统集成与接口设计

统一问答接口

def ask(self, question: str, top_k: int = 10, similarity_threshold: float = 0.01) -> Dict:

接口设计原则

  • 参数可配置: 允许调整检索参数

  • 结构化返回: 统一的返回格式

  • 来源追踪: 提供答案来源信息

返回格式设计

return {
    'question': question,
    'answer': answer,
    'sources': sources  # 包含来源文件、相似度、内容预览
}

部署和使用

环境配置

根目录下创建.env文件

DEEPSEEK_API_KEY=your_api_key_here

初始化流程

rag = SmartLibRAG(api_key=api_key)
rag.initialize()  # 一键初始化所有组件

性能优化建议

1. 缓存策略优化

  • 实现文件变更检测
  • 添加内存缓存层
  • 考虑Redis等外部缓存

2. 检索算法优化

  • 考虑使用Faiss等专业向量检索库
  • 实现多路召回策略
  • 添加查询扩展机制

3. 并发处理

  • 添加异步处理支持
  • 实现请求队列管理
  • 考虑多进程部署

扩展方向

1. 多模态支持

  • 图片内容理解
  • 音频问答支持

2. 知识图谱集成

  • 人物关系图谱
  • 情节时间线

3. 个性化推荐

  • 用户兴趣建模
  • 智能问题推荐
  • streamlit页面配置
# 页面配置
st.set_page_config(
    page_title="图书智能检索系统",
    page_icon="📚",
    layout="wide",
    initial_sidebar_state="expanded"
)
  • 自定义样式
st.markdown("""
<style>
@import url('https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@400;600;700&display=swap');

.main-header {
    font-size: 3rem;
    background: linear-gradient(135deg, #8B4513, #CD853F);
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
    background-clip: text;
    text-align: center;
    margin-bottom: 2rem;
    font-weight: 700;
    font-family: 'Noto Serif SC', serif;
    text-shadow: 2px 2px 4px rgba(0,0,0,0.1);
}

.subtitle {
    text-align: center;
    color: #666;
    font-style: italic;
    margin-bottom: 2rem;
    font-family: 'Noto Serif SC', serif;
}

.chat-message {
    padding: 1.2rem;
    border-radius: 1rem;
    margin-bottom: 1rem;
    box-shadow: 0 2px 8px rgba(0,0,0,0.1);
    transition: all 0.3s ease;
    animation: fadeIn 0.5s ease-in;
}

.chat-message:hover {
    transform: translateY(-2px);
    box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}

.user-message {
    background: linear-gradient(135deg, #e3f2fd, #bbdefb);
    border-left: 4px solid #2196f3;
    margin-left: 2rem;
}

.assistant-message {
    background: linear-gradient(135deg, #fff8e1, #ffecb3);
    border-left: 4px solid #8B4513;
    margin-right: 2rem;
}

.source-info {
    background: linear-gradient(135deg, #f3e5f5, #e1bee7);
    padding: 0.8rem;
    border-radius: 0.8rem;
    margin-top: 0.8rem;
    font-size: 0.9rem;
    border: 1px solid #ce93d8;
    transition: all 0.3s ease;
}

.source-info:hover {
    background: linear-gradient(135deg, #e8eaf6, #c5cae9);
    border-color: #9c27b0;
}

.status-success {
    color: #2e7d32;
    font-weight: bold;
    text-shadow: 1px 1px 2px rgba(0,0,0,0.1);
}

.status-error {
    color: #d32f2f;
    font-weight: bold;
    text-shadow: 1px 1px 2px rgba(0,0,0,0.1);
}

.status-warning {
    color: #f57c00;
    font-weight: bold;
    text-shadow: 1px 1px 2px rgba(0,0,0,0.1);
}

.example-button {
    background: linear-gradient(135deg, #fff3e0, #ffe0b2);
    border: 1px solid #ffb74d;
    border-radius: 0.5rem;
    padding: 0.5rem;
    margin: 0.2rem 0;
    transition: all 0.3s ease;
    cursor: pointer;
}

.example-button:hover {
    background: linear-gradient(135deg, #ffe0b2, #ffcc80);
    transform: translateX(5px);
}

.sidebar-section {
    background: linear-gradient(135deg, #fafafa, #f5f5f5);
    padding: 1rem;
    border-radius: 0.8rem;
    margin-bottom: 1rem;
    border: 1px solid #e0e0e0;
}

.typing-indicator {
    display: inline-block;
    animation: typing 1.5s infinite;
}

@keyframes fadeIn {
    from { opacity: 0; transform: translateY(20px); }
    to { opacity: 1; transform: translateY(0); }
}

@keyframes typing {
    0%, 60%, 100% { opacity: 1; }
    30% { opacity: 0.5; }
}

.metric-card {
    background: linear-gradient(135deg, #e8f5e8, #c8e6c9);
    padding: 1rem;
    border-radius: 0.8rem;
    text-align: center;
    margin: 0.5rem 0;
    border: 1px solid #81c784;
}

.progress-bar {
    background: linear-gradient(90deg, #4caf50, #8bc34a);
    height: 4px;
    border-radius: 2px;
    animation: progress 2s ease-in-out;
}

@keyframes progress {
    from { width: 0%; }
    to { width: 100%; }
}
</style>
""", unsafe_allow_html=True)
  • 显示用户消息
def display_chat_message(role, content, sources=None, typing=False):
    """显示聊天消息"""
    if role == "user":
        st.markdown(f"""
        <div class="chat-message user-message">
            <strong>🙋‍♀️ 您:</strong> {content}
        </div>
        """, unsafe_allow_html=True)
    else:
        typing_indicator = '<span class="typing-indicator">💭</span>' if typing else '🤖'
        st.markdown(f"""
        <div class="chat-message assistant-message">
            <strong>{typing_indicator} 红楼梦助手:</strong> {content}
        </div>
        """, unsafe_allow_html=True)

        if sources:
            with st.expander(f"📖 参考文档片段 ({len(sources)}个)", expanded=False):
                for i, source in enumerate(sources, 1):
                    similarity_color = "#4caf50" if source['similarity'] > 0.5 else "#ff9800" if source[
                                                                                                     'similarity'] > 0.3 else "#f44336"
                    st.markdown(f"""
                    <div class="source-info">
                        <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem;">
                            <strong>📄 片段 {i}: {source['source']}</strong>
                            <span style="background: {similarity_color}; color: white; padding: 0.2rem 0.5rem; border-radius: 1rem; font-size: 0.8rem;">
                                相似度: {source['similarity']:.3f}
                            </span>
                        </div>
                        <div style="background: rgba(255,255,255,0.8); padding: 0.5rem; border-radius: 0.5rem; border-left: 3px solid {similarity_color};">
                            <em>📝 内容预览:</em><br>
                            {source['content_preview']}
                        </div>
                    </div>
                    """, unsafe_allow_html=True)

更多推荐