从零搭建电子书RAG问答系统:Milvus + LangChain实战指南

前言

随着大语言模型的普及,RAG(Retrieval-Augmented Generation,检索增强生成)技术成为解决AI幻觉问题的关键方案。本文将通过一个完整的电子书问答系统,带你掌握RAG的核心实现流程:文档向量化、语义检索、智能问答。

一、什么是RAG?

RAG的核心思想

传统LLM存在两大问题:

makefile 复制代码
问题1: 知识截止日期后的信息无法获取
问题2: 私有领域知识(如企业文档、专业书籍)无法回答

RAG通过检索+生成的方式解决这些问题:

scss 复制代码
用户提问
   ↓
将问题转为向量(Embedding)
   ↓
在向量数据库中检索相似内容
   ↓
将检索结果+问题一起发给LLM
   ↓
LLM基于检索内容生成答案

本文案例场景

我们将构建一个《天龙八部》小说问答系统,用户可以提问"段誉会什么武功?",系统能够从小说原文中检索相关章节,并给出准确答案。

二、技术栈选型

核心组件

less 复制代码
向量数据库
@zilliz/milvus2-sdk-node  // Milvus官方Node.js SDK


LangChain生态
@langchain/openai          // OpenAI集成(Embedding + LLM)
@langchain/community       // 文档加载器(EPUB)
@langchain/textsplitters   // 文本分割工具

为什么选择Milvus?

makefile 复制代码
优势1: 专为向量检索优化,支持十亿级规模
优势2: 支持COSINE/L2等多种相似度算法
优势3: 提供丰富的索引类型(IVF_FLAT、HNSW等)
优势4: 云原生架构,易于扩展

三、系统架构设计

整体流程

scss 复制代码
┌─────────────────┐
│  电子书文件      │
│  (EPUB格式)     │
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│  文档加载        │
│  按章节拆分      │
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│  文本分块        │
│  (500字/块)     │
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│  向量化          │
│  (Embedding)    │
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│  存入Milvus      │
│  建立索引        │
└─────────────────┘

查询流程:
问题 → 向量化 → 语义检索 → LLM生成答案

四、核心代码实现

步骤1:创建向量集合

javascript

php 复制代码
async function ensureBookCollection(bookId) {
  try {
    const hasCollection = await client.hasCollection({
      collection_name: COLLECTION_NAME,
    });
    
    if (!hasCollection.value) {
      console.log(`${COLLECTION_NAME} 集合不存在,创建集合`);
      
      // 定义数据Schema
      await client.createCollection({
        collection_name: COLLECTION_NAME,
        fields: [
          { name: 'id', data_type: DataType.VarChar, max_length: 100, is_primary_key: true },
          { name: 'book_id', data_type: DataType.VarChar, max_length: 100 },
          { name: 'book_name', data_type: DataType.VarChar, max_length: 100 },
          { name: 'chapter_num', data_type: DataType.Int32 },
          { name: 'index', data_type: DataType.Int32 },
          { name: 'content', data_type: DataType.VarChar, max_length: 10000 },
          { name: 'vector', data_type: DataType.FloatVector, dim: VECTION_DIM },
        ]
      });
      
      // 创建向量索引
      await client.createIndex({
        collection_name: COLLECTION_NAME,
        field_name: 'vector',
        index_type: IndexType.IVF_FLAT,
        metric_type: MetricType.COSINE,  // 余弦相似度
        params: {
          nlist: VECTION_DIM,
        }
      });
    }
    
    // 加载集合到内存
    await client.loadCollection({
      collection_name: COLLECTION_NAME,
    });
  } catch (err) {
    console.error('集合创建失败', err.message);
    throw err;
  }
}

核心要点:

  • FloatVector字段存储1024维向量
  • COSINE度量适合文本语义相似度计算
  • IVF_FLAT索引平衡了速度和精度

步骤2:加载并处理EPUB文件

javascript

javascript 复制代码
async function loadAndProcessEPubStreaming(bookId) {
  try {
    // 加载EPUB文件
    const loader = new EPubLoader(
      EPUB_FILE,
      { splitChapters: true }  // 按章节拆分
    );
    const documents = await loader.load();
    
    // 文本分割器配置
    const textSplitter = new RecursiveCharacterTextSplitter({
      chunkSize: 500,      // 每块500字
      chunkOverlap: 50,    // 块间重叠50字,保持上下文连贯
    });
    
    let totalInserted = 0;
    for (let chapterIndex = 0; chapterIndex < documents.length; chapterIndex++) {
      const chapter = documents[chapterIndex];
      console.log(`处理第 ${chapterIndex + 1}/${documents.length} 章`);
      
      // 章节内容进一步切块
      const chunks = await textSplitter.splitText(chapter.pageContent);
      console.log(`拆分为 ${chunks.length} 个片段`);
      
      if (chunks.length === 0) continue;
      
      const insertedCount = await insertChunksBatch(chunks, bookId, chapterIndex + 1);
      totalInserted += insertedCount;
    }
    
    console.log(`累计插入 ${totalInserted} 个片段`);
    return totalInserted;
  } catch (err) {
    console.error('加载EPUB文件失败', err.message);
    throw err;
  }
}

为什么要分块?

makefile 复制代码
原因1: Embedding模型有长度限制(通常8192 tokens)
原因2: 小块检索精度更高,减少噪音
原因3: 重叠设计避免关键信息被切断

步骤3:批量向量化并插入

javascript

javascript 复制代码
async function insertChunksBatch(chunks, bookId, chapterIndex) {
  try {
    if (chunks.length === 0) return 0;
    
    // 并发生成Embedding(性能优化关键)
    const insertData = await Promise.all(
      chunks.map(async (chunk, chunkIndex) => {
        const vector = await getEmbedding(chunk);
        return {
          id: `${bookId}_${chapterIndex}_${chunkIndex}`,
          book_id: bookId,
          book_name: BOOK_NAME,
          chapter_num: chapterIndex,
          index: chunkIndex,
          content: chunk,
          vector
        };
      })
    );
    
    const insertResult = await client.insert({
      collection_name: COLLECTION_NAME,
      data: insertData,
    });
    
    return Number(insertResult.insert_cnt) || 0;
  } catch (err) {
    console.error('插入数据失败', err.message);
    throw err;
  }
}

性能优化亮点:

  • 使用Promise.all并发调用Embedding API,大幅提升处理速度
  • 批量插入减少网络往返次数

步骤4:语义检索实现

javascript

javascript 复制代码
async function retrieveRelevantContent(question, k=3) {
  try {
    // 将问题转为向量
    const queryVector = await getEmbedding(question);
    
    // 向量相似度检索
    const searchResult = await client.search({
      collection_name: COLLECTION_NAME,
      vector: queryVector,
      limit: k,  // 返回Top K个最相似结果
      metric_type: MetricType.COSINE,
      output_fields: ['id', 'content', 'book_id', 'chapter_num', 'index', 'book_name'],
    });
    
    return searchResult.results;
  } catch (err) {
    console.log('检索相关内容失败', err.message);
    return [];
  }
}

检索原理:

makefile 复制代码
问题: "段誉会什么武功?"
   ↓
Embedding: [0.23, -0.45, 0.67, ...]  (1024维向量)
   ↓
与数据库中所有片段向量计算余弦相似度
   ↓
返回相似度最高的3个片段

步骤5:RAG问答核心逻辑

javascript

javascript 复制代码
async function answerEbookQuestion(question, k=3) {
  try {
    console.log('开始回答问题: ', question);
    
    // 1. 检索相关内容
    const retrievedContent = await retrieveRelevantContent(question, k);
    
    if (retrievedContent.length === 0) {
      return '抱歉,没有找到相关内容';
    }
    
    // 2. 构建上下文
    const context = retrievedContent.map((item, i) => `
      [片段 ${i+1}]
      章节:第${item.chapter_num}章
      内容:${item.content}
    `).join('\n\n---\n\n');
    
    // 3. 构建Prompt
    const prompt = `
      你是一个专业的《天龙八部》小说助手,基于小说内容回答问题。

      请根据以下小说片段内容回答问题:
      ${context}

      用户问题:${question}

      回答要求:
      1. 如果片段中有相关信息,请结合小说内容给出详情
      2. 可以综合多个片段的内容,提供完整的答案
      3. 如果片段中没有相关信息,请如实告知
      4. 回答要准确,符合小说的情节和人物设定
      5. 可以引用原文内容来支持你的回答

      AI助手的回答:
    `;
    
    // 4. 调用LLM生成答案
    const response = await model.invoke(prompt);
    console.log(response.content);
    return response.content;
  } catch (err) {
    return '抱歉,处理您的问题时出现了错误';
  }
}

Prompt工程要点:

  • 明确角色定位("小说助手")
  • 提供结构化上下文(章节信息+内容)
  • 设定回答规范(准确性、引用原文)
  • 处理边界情况(无相关信息时的应对)

五、性能优化建议

1. Embedding并发控制

javascript

ini 复制代码
// 避免API限流
const BATCH_SIZE = 10;
for (let i = 0; i < chunks.length; i += BATCH_SIZE) {
  const batch = chunks.slice(i, i + BATCH_SIZE);
  await Promise.all(batch.map(chunk => getEmbedding(chunk)));
}

2. 索引选择策略

javascript

yaml 复制代码
// 小规模数据(<100万): IVF_FLAT
index_type: IndexType.IVF_FLAT

// 大规模数据(>100万): HNSW
index_type: IndexType.HNSW
params: { M: 16, efConstruction: 200 }

3. 分块参数调优

javascript

yaml 复制代码
// 技术文档: 较大块保持完整性
chunkSize: 1000, chunkOverlap: 100

// 问答场景: 较小块提升精度
chunkSize: 500, chunkOverlap: 50

总结

通过本文,我们完整实现了一个RAG电子书问答系统,核心要点回顾:

  1. 文档处理: EPUB加载 → 章节拆分 → 文本分块
  2. 向量化: 并发调用Embedding API → 批量插入Milvus
  3. 语义检索: 问题向量化 → COSINE相似度计算 → Top K检索
  4. 智能问答: 检索上下文 + Prompt工程 → LLM生成答案

这套架构可以轻松扩展到:

  • 企业知识库问答
  • 法律文档检索
  • 技术文档助手
  • 客服智能问答

掌握RAG技术,让你的AI应用从"聊天玩具"进化为"生产力工具"。

相关推荐
FriendshipT1 小时前
评估指标:AP(Average Precision)、mAP(Mean Average Precision)
人工智能·python·计算机视觉·map·ap
limx76321 小时前
AstronClaw实战:邮箱
人工智能
新缸中之脑1 小时前
Autoresearch 快速上手
人工智能
Jackson@ML1 小时前
自然语言处理概要
人工智能·自然语言处理·nlp
庭前云落1 小时前
Compound 3| COMP的作用和价值
人工智能·区块链
zhangfeng11332 小时前
国产GPU与ROCm架构的关系 国产GPU架构总结 ROCm 7.1 在 PyTorch 官网上被划掉(横线)直接支持
人工智能·pytorch·架构
咚咚王者2 小时前
人工智能之语言领域 自然语言处理 第十一章 注意力机制
人工智能·自然语言处理
AI科技星2 小时前
基于v≡c空间光速螺旋量子几何归一化统一场论第一性原理的时间势差本源理论
人工智能·线性代数·算法·机器学习·平面
王的宝库2 小时前
AI 学习笔记:AI学习模式 Transformer、RAG、Skill、MCP
人工智能·笔记·学习