当前位置: 首页
AI教程
从零开始搭建电子书RAG问答系统:Milvus与LangChain实战指南

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

热心网友 时间:2026-06-26
转载

前言

大语言模型火起来以后,AI幻觉问题成为难以回避的挑战。如何让模型给出可信的答案,特别是基于私有数据作答?RAG(检索增强生成)技术可以说是当前最务实的解决方案。本文通过搭建一个完整的电子书问答系统,系统梳理RAG的核心流程——文档向量化、语义检索、智能问答,同时揭示实际开发中的关键细节。

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

一、什么是RAG?

RAG的核心思想

传统大语言模型存在两个天然短板:一是训练数据有知识截止日期,无法知晓新事件;二是只学习公开语料,企业内部文档、专业书籍等私有知识一概不懂。

RAG 的思路直截了当:不再让模型“凭记忆作答”,而是先到知识库中“查找资料”,再将检索到的材料连同问题一起交给模型生成答案。整个流程如下:

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

本文案例场景

为了让你直观理解,我们以《天龙八部》为例构建问答系统。当用户问“段誉会什么武功?”,系统并非依赖模型猜测,而是真正从小说原文中检索相关段落,提供有据可查的答案。

二、技术栈选型

核心组件

这套系统主要使用以下组件:

  • 向量数据库:@zilliz/milvus2-sdk-node——Milvus 官方 Node.js SDK
  • LangChain 生态:@langchain/openai(负责 Embedding 和 LLM 调用)、@langchain/community(加载 EPUB 文档)、@langchain/textsplitters(文本分割工具)

为什么选择Milvus?

市面上向量数据库众多,Milvus 具备几个硬核优势:专为向量检索而设计,十亿级规模也能轻松应对;支持 COSINE、L2 等多种相似度算法;索引类型丰富,从 IVF_FLAT 到 HNSW 按需选择;云原生架构,扩展性极佳。

三、系统架构设计

整体流程

系统分为数据导入和实时查询两大模块。导入流程:电子书文件 → 文档加载(按章节拆分) → 文本分块(每块 500 字) → 向量化(Embedding) → 存入 Milvus 并建立索引。查询流程:用户问题 → 向量化 → 语义检索 → LLM 生成答案。

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

四、核心代码实现

步骤1:创建向量集合

先来看第一步:在 Milvus 中创建一个集合,用于存储数据 Schema 和向量索引。

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 文件

加载 EPUB 文件后,先按章节拆分,再进一步切分成小块。为什么要切块?下面会详细说明。

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;
  }
}

分块的原因很朴素:首先,Embedding 模型有长度限制(通常为 8192 tokens),超过则被截断;其次,小块检索精度更高,能减少噪音干扰;此外,块与块之间设置一定重叠,避免关键信息恰好被切在边界上。

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

分好块后,需要批量生成向量并写入数据库。这里使用并发调用,是性能优化的关键手段。

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:语义检索实现

数据准备好后,用户提问时,系统需将问题转为向量,再到数据库中寻找最相似的几个片段。

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 [];
  }
}

检索过程简单来说:问题→Embedding 成向量→与数据库中所有片段向量计算余弦相似度→返回最相似的 3 个片段。

步骤5:RAG问答核心逻辑

最后一步,将检索到的上下文和问题拼接起来,交给 LLM 生成答案。

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并发控制

并发固然好,但需注意 API 限流风险。建议分批处理:

// 避免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. 索引选择策略

数据规模不同,索引方案也应调整:

// 小规模数据(<100万): IVF_FLAT
index_type: IndexType.IVF_FLAT

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

3. 分块参数调优

分块大小需根据场景灵活调整:技术文档内容连贯性强,适合较大分块(1000 字,重叠 100 字);问答场景更关注精度,较小分块(500 字,重叠 50 字)更合适。

总结

到这里,一套完整的 RAG 电子书问答系统便搭建完成。回顾核心流程:从 EPUB 文件加载、章节拆分、文本分块,到并发调用 Embedding 进行向量化、批量插入 Milvus;查询时,问题转为向量、余弦相似度检索、Top K 召回;最后结合精心设计的 Prompt,交付给 LLM 生成有据可查的答案。

这套架构扩展性极强,稍作调整即可适用于企业知识库问答、法律文档检索、技术文档助手、客服智能问答等场景。掌握 RAG 技术后,你的 AI 应用才能真正从“聊天玩具”进化为“生产力工具”。

来源:https://juejin.cn/post/7616681500683583515

游乐网为非赢利性网站,所展示的游戏/软件/文章内容均来自于互联网或第三方用户上传分享,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系youleyoucom@outlook.com。

同类文章
更多
科研人员必读:多肽、蛋白质、重组蛋白区别及定制指南

科研人员必读:多肽、蛋白质、重组蛋白区别及定制指南

Section 01 多肽 VS 蛋白质 VS 重组蛋白 多肽、蛋白质和重组蛋白,本质上是同宗同源的东西——都是氨基酸串起来的生物大分子。三者的核心区别,说到底无非是三个维度:分子大小、折叠形态,以及生产方式。 接下来是一张清晰的对比图,帮你快速建立直觉: ![对比图1](https:

时间:2026-07-07 17:55
知识图谱与本体语义建模的核心区别解析

知识图谱与本体语义建模的核心区别解析

谈到人工智能如何“理解”知识,有两个概念常被放在一起讨论:知识图谱与本体语义建模。不少人以为它们是同一事物,或者认为后者是前者的进化版。实际上,两者的分工完全不同——打个比方,一个是“记事的本子”,另一个是“写本子之前先定好的规矩”。 1 本体语义建模:先绘制一张“通用分类蓝图” 设想一下,你要整

时间:2026-07-07 17:55
强烈推荐工作搭子WorkBuddy

强烈推荐工作搭子WorkBuddy

一次偶然的机会,从朋友那里了解到WorkBuddy这个工具。说实话,在AI产品扎堆的今天,能遇到一个下载即用的助手,确实值得推荐给每一个被日常琐事缠身的人。 安装过程没什么难度,双击安装包默认安装即可。需要留意的是,如果在Windows7上折腾了半天没反应,别慌——这工具在高版本Windows下运行

时间:2026-07-07 17:55
跨境电商系统自动化测试与CI/CD流水线构建指南

跨境电商系统自动化测试与CI/CD流水线构建指南

技术方向:自动化测试与DevOps实践关键词:日本代购、一站式日淘、雅虎代拍系统、煤炉自动代拍 一、测试分层策略详解 不少人刚开始就想直接搞E2E测试,觉得跑通完整流程才够“真实”。然而,测试金字塔这么多年仍不过时,原因很简单——不同层级的测试各有分工,缺少任何一层都会不稳。来看看这张金字塔图: ┌

时间:2026-07-07 17:54
中小企业AI营销矩阵工具推荐:赛诺贝斯智域蒲公英

中小企业AI营销矩阵工具推荐:赛诺贝斯智域蒲公英

天天刷着别人的爆款内容,自己却“有心无力”——这才是2026年绝大多数中小企业运营社交媒体的真实写照。说白了,社交媒体如今早已不是“要不要做”的选择题,而是“怎么做才能真正见效”的生存考验。现实情况是,团队人力就那么几个,预算也紧巴巴,却要同时运营抖音、小红书、知乎、头条、百家号等多个阵地……文案、

时间:2026-07-07 17:54
热门专题
更多
刀塔传奇破解版无限钻石下载大全 刀塔传奇破解版无限钻石下载大全
洛克王国正式正版手游下载安装大全 洛克王国正式正版手游下载安装大全
思美人手游下载专区 思美人手游下载专区
好玩的阿拉德之怒游戏下载合集 好玩的阿拉德之怒游戏下载合集
不思议迷宫手游下载合集 不思议迷宫手游下载合集
百宝袋汉化组游戏最新合集 百宝袋汉化组游戏最新合集
jsk游戏合集30款游戏大全 jsk游戏合集30款游戏大全
宾果消消消原版下载大全 宾果消消消原版下载大全