本系列共四篇,从零到一完成从向量知识库搭建到多跳推理 RAG 的完整演进:
- 前置篇:Milvus 向量知识库搭建,《天龙八部》EPUB 一键入库
- 入门篇:LangGraph 基础单轮 RAG,检索→生成最简流程
- 进阶篇:智能路由分支 RAG,问题自动分流,常识直答、复杂检索
- 高级篇:多跳推理 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 集合字段设计
表格
| 字段名 | 类型 | 作用 |
|---|---|---|
| id | VarChar(100) | 主键,由「书籍 ID_章节号_切片序号」组成,全局唯一 |
| book_id | VarChar(100) | 书籍 ID,支持多书籍混合入库 |
| book_name | VarChar(200) | 书籍名称 |
| chapter_num | Int32 | 章节号,便于定位原文 |
| index | Int32 | 章节内切片序号 |
| content | VarChar(10000) | 切片原文内容,用于生成答案 |
| vector | FloatVector(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 生成答案」的完整流程。