前置篇:手把手用 Milvus + LangChain 搭建小说向量知识库,《天龙八部》EPUB 一键入库

11 阅读6分钟

本系列共四篇,从零到一完成从向量知识库搭建到多跳推理 RAG 的完整演进:

  1. 前置篇:Milvus 向量知识库搭建,《天龙八部》EPUB 一键入库
  2. 入门篇:LangGraph 基础单轮 RAG,检索→生成最简流程
  3. 进阶篇:智能路由分支 RAG,问题自动分流,常识直答、复杂检索
  4. 高级篇:多跳推理 RAG,子问题拆解 + 循环检索 + 智能规划

前置篇:手把手用 Milvus + LangChain 搭建小说向量知识库,《天龙八部》EPUB 一键入库

前言

做 RAG(检索增强生成)的第一步,永远是搭建自己的向量知识库。很多人入门时卡在「怎么把自己的文档变成向量存进去」,尤其是结构化的电子书,直接扔进去不仅检索不准,还浪费算力。

本文就以经典小说《天龙八部》EPUB 为例,从零到一带你完成:

  • EPUB 电子书按章节加载与智能切片
  • 文本向量化生成 Embedding
  • Milvus 向量数据库集合设计与索引创建
  • 流式批量插入,边处理边入库

全程基于 Node.js + LangChain + Milvus 原生 SDK,代码可直接运行,跑完就能拥有一个可用于 RAG 问答的小说向量库。

一、技术栈与前置准备

核心依赖

  • @zilliz/milvus2-sdk-node:Milvus 官方 Node.js SDK,用于集合管理与数据插入
  • @langchain/openai:调用 Embedding 模型生成向量
  • @langchain/community/document_loaders/fs/epub:EPUB 文件加载器,自动按章节拆分
  • @langchain/textsplitters:递归字符文本拆分器,控制切片长度
  • dotenv:环境变量管理

本地环境

  • 本地启动 Milvus 服务(默认端口 19530,推荐 Docker 一键启动)
  • 可用的 OpenAI 格式 Embedding 接口
  • 准备好《天龙八部》EPUB 格式电子书

二、核心设计思路

1. 为什么要做文本切片?

大段整章文本直接向量化会导致:

  • 向量语义过于宽泛,检索命中率低
  • 单次检索上下文过长,干扰生成答案
  • 超出 Embedding 模型最大输入长度

我们采用按章节拆分 + 二次固定长度切片的策略,每片 500 字符,保留 50 字符重叠,保证上下文连贯性。

2. Milvus 集合字段设计

表格

字段名类型作用
idVarChar(100)主键,由「书籍 ID_章节号_切片序号」组成,全局唯一
book_idVarChar(100)书籍 ID,支持多书籍混合入库
book_nameVarChar(200)书籍名称
chapter_numInt32章节号,便于定位原文
indexInt32章节内切片序号
contentVarChar(10000)切片原文内容,用于生成答案
vectorFloatVector(1024)文本向量,用于相似度检索

3. 索引选择

采用 IVF_FLAT 索引 + 余弦相似度(COSINE) ,把向量聚类分桶,检索时只比对最相关的桶,在保证准确率的同时大幅提升检索速度。

三、完整代码逐段解析

1. 依赖导入与常量定义

首先导入所需模块,定义全局常量,方便后续统一调整参数。

import "dotenv/config";
import { parse } from 'node:path';
import { 
  MilvusClient, 
  DataType,
  MetricType,
  IndexType 
} from '@zilliz/milvus2-sdk-node';
import { OpenAIEmbeddings } from "@langchain/openai";
import { EPubLoader } from "@langchain/community/document_loaders/fs/epub";
import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters";

// 全局配置
const COLLECTION_NAME = 'ebook_collection';
const VECTOR_DIM = 1024;          // 向量维度,与 Embedding 模型对应
const CHUNK_SIZE = 500;           // 单切片字符数
const EPUB_FILE = './天龙八部.epub';
const BOOK_NAME = parse(EPUB_FILE).name; // 从文件名提取书名

2. 初始化 Embedding 与 Milvus 客户端

Embedding 模型负责把文本转成 1024 维向量;Milvus 客户端连接本地向量数据库。

// 初始化向量模型
const embeddings = new OpenAIEmbeddings({
  apiKey: process.env.OPENAI_API_KEY,
  model: process.env.EMBEDDINGS_MODEL_NAME,
  configuration: {
    baseURL: process.env.OPENAI_BASE_URL
  },
  dimensions: VECTOR_DIM
});

// 初始化 Milvus 客户端
const client = new MilvusClient({
  address: 'localhost:19530'
});

// 封装向量生成方法
async function getEmbedding(text) {
  const result = await embeddings.embedQuery(text);
  return result;
}

3. 创建集合与索引

ensureCollection 函数负责幂等性创建集合:不存在则创建,存在则跳过,最后保证集合处于加载状态。

async function ensureCollection(bookId) {
  try {
    // 检查集合是否已存在
    const hasCollection = await client.hasCollection({
      collection_name: COLLECTION_NAME
    });

    if (!hasCollection.value) {
      console.log('创建集合...');
      // 创建集合,定义所有字段
      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: 200 },
          { 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: VECTOR_DIM }
        ]
      });
      console.log('✓ 集合创建成功');

      // 为向量字段创建索引
      console.log('创建索引...');
      await client.createIndex({
        collection_name: COLLECTION_NAME,
        field_name: 'vector',
        index_type: IndexType.IVF_FLAT,
        metric_type: MetricType.COSINE,
        params: { nlist: 1024 } // 聚类分桶数量
      });
      console.log('✓ 索引创建成功');
    } 
    
    // 加载集合到内存,保证检索性能
    try {
      await client.loadCollection({ collection_name: COLLECTION_NAME });
      console.log('✓ 集合已加载');
    } catch (error) {
      console.log('✓ 集合已处于加载状态');
    }
  } catch (error) {
    console.error('创建集合时出错:', error.message);
    throw error;
  }
}

4. 批量插入数据

insertChunksBatch 接收一个章节的所有切片,并行生成向量,组装成 Milvus 要求的数据格式,批量插入。

async function insertChunksBatch(chunks, bookId, chapterNum) {
  try {
    if (chunks.length === 0) return 0;

    // 并行生成每个切片的向量
    const insertData = await Promise.all(
      chunks.map(async (chunk, chunkIndex) => {
        const vector = await getEmbedding(chunk);
        return {
          id: `${bookId}_${chapterNum}_${chunkIndex}`,
          book_id: bookId,
          book_name: BOOK_NAME,
          chapter_num: chapterNum,
          index: chunkIndex,
          content: chunk,
          vector: vector
        };
      })
    );

    // 批量插入
    const insertResult = await client.insert({
      collection_name: COLLECTION_NAME,
      data: insertData
    });

    return Number(insertResult.insert_cnt) || 0;
  } catch (error) {
    console.error(`插入章节 ${chapterNum} 的数据时出错:`, error.message);
    throw error;
  }
}

5. EPUB 流式处理

核心处理逻辑:加载 EPUB → 按章节拆分 → 逐章二次切片 → 边处理边插入。 采用流式处理的好处是不需要把整本书都加载到内存,处理大文件更稳定。

async function loadAndProcessEPubStreaming(bookId) {
  try {
    console.log(`\n开始加载 EPUB 文件: ${EPUB_FILE}`);
    
    // 加载 EPUB,自动按章节拆分
    const loader = new EPubLoader(EPUB_FILE, { splitChapters: true });
    const documents = await loader.load();
    console.log(`✓ 加载完成,共 ${documents.length} 个章节\n`);

    // 初始化文本拆分器
    const textSplitter = new RecursiveCharacterTextSplitter({
      chunkSize: CHUNK_SIZE,
      chunkOverlap: 50, // 相邻切片重叠 50 字符,避免语义断裂
    });

    let totalInserted = 0;

    // 逐章处理
    for (let chapterIndex = 0; chapterIndex < documents.length; chapterIndex++) {
      const chapter = documents[chapterIndex];
      const chapterContent = chapter.pageContent;
      
      console.log(`处理第 ${chapterIndex + 1}/${documents.length} 章...`);
      
      // 二次切片
      const chunks = await textSplitter.splitText(chapterContent);
      console.log(`  拆分为 ${chunks.length} 个片段`);
      
      if (chunks.length === 0) {
        console.log(`  跳过空章节\n`);
        continue;
      }

      console.log(`  生成向量并插入中...`);
      const insertedCount = await insertChunksBatch(chunks, bookId, chapterIndex + 1);
      totalInserted += insertedCount;
      
      console.log(`  ✓ 已插入 ${insertedCount} 条记录(累计: ${totalInserted})\n`);
    }

    console.log(`\n总共插入 ${totalInserted} 条记录\n`);
    return totalInserted;
  } catch (error) {
    console.error('加载 EPUB 文件时出错:', error.message);
    throw error;
  }
}

6. 主函数入口

串联整个流程:连接 Milvus → 确保集合存在 → 处理 EPUB 并入库。

async function main() {
  try {
    console.log('='.repeat(80));
    console.log('电子书向量知识库构建工具');
    console.log('='.repeat(80));

    console.log('\n连接 Milvus...');
    await client.connectPromise;
    console.log('✓ 已连接\n');

    const bookId = 1;
    await ensureCollection(bookId);
    await loadAndProcessEPubStreaming(bookId);

    console.log('='.repeat(80));
    console.log('处理完成!向量知识库已就绪');
    console.log('='.repeat(80));
  } catch (error) {
    console.error('\n错误:', error.message);
    console.error(error.stack);
    process.exit(1);
  }
}

main();

四、运行效果

执行脚本后,控制台会输出完整的处理进度:

================================================================================
电子书向量知识库构建工具
================================================================================

连接 Milvus...
✓ 已连接

创建集合...
✓ 集合创建成功
创建索引...
✓ 索引创建成功
✓ 集合已加载

开始加载 EPUB 文件: ./天龙八部.epub
✓ 加载完成,共 50 个章节

处理第 1/50 章...
  拆分为 12 个片段
  生成向量并插入中...
  ✓ 已插入 12 条记录(累计: 12)

...(中间章节省略)

总共插入 1286 条记录

================================================================================
处理完成!向量知识库已就绪
================================================================================

五、总结与预告

至此,我们已经把《天龙八部》整本书变成了 1000+ 条带向量的文本片段,存入了 Milvus 向量库。这是所有 RAG 应用的地基,接下来的问答系统都将基于这个知识库运行。

下一篇,我们将基于这个向量库,用 LangGraph 搭建最简单的单轮 RAG 问答系统,实现「提问 → 向量检索 → LLM 生成答案」的完整流程。