实战:用Milvus+LangChain搭建电子书RAG问答系统
一、前言:什么是RAG?为什么要做电子书RAG?
在大模型时代,直接提问大模型经常会出现 “幻觉” ——回答不符合事实、胡编乱造,针对特定领域/专属内容(比如一本小众电子书、内部文档)更是无法精准作答。这时候RAG(检索增强生成,Retrieval-Augmented Generation) 就派上用场了。
简单来说,RAG的核心逻辑是:先从专属知识库中检索相关内容,再把检索结果交给大模型,让模型基于真实素材生成回答,既保证回答的准确性,又能让模型适配专属数据,完美解决大模型“知识盲区”和“幻觉”问题。
本次实战,我们就以《天龙八部》EPUB电子书为例,借助LangChain做文档加载与拆分、Milvus向量数据库做向量存储与检索、OpenAI嵌入模型+对话模型做向量化与问答生成,从零搭建一套完整的电子书RAG智能问答系统,实现“问小说内容,得精准回答”的效果。
二、RAG核心流程拆解(电子书专属)
针对电子书的RAG开发,核心分为 “知识库构建(离线)” 和 “问答检索(在线)” 两大阶段,整体流程清晰,新手也能快速上手:
📚 知识库构建阶段(数据入库)
- 加载电子书:借助LangChain社区的Loader,读取EPUB格式电子书,按章节拆分;
- 文本分片:用文本分割器,把长章节拆分成短文本块(避免文本过长,影响向量精度);
- 文本向量化:通过Embedding嵌入模型,将文本块转为计算机可识别的向量数据;
- 向量存储:将向量+原文文本+元数据(章节、序号)存入Milvus向量数据库。
🤖 问答检索阶段(在线服务)
- 问题向量化:将用户的自然语言问题,转为和知识库同维度的向量;
- 语义检索:在Milvus中检索与问题向量最相似的文本片段;
- Prompt构造:把检索到的小说原文作为上下文,拼接成规范的提示词;
- 大模型回答:将Prompt交给对话大模型,生成贴合原著的精准回答。
🗂️ 核心技术栈
- 向量数据库:Milvus(轻量、高效,适配向量检索场景)
- 文档处理:LangChain(EPubLoader加载电子书、RecursiveCharacterTextSplitter文本分片)
- 模型服务:OpenAI Embedding(文本向量化)、OpenAI Chat(问答生成)
- 开发语言:Node.js
三、前期准备:环境配置与依赖安装
1. 环境依赖
- 安装Node.js环境(推荐v16+)
- 部署Milvus服务(本地/云端均可,极简版可使用Zilliz Cloud托管服务)
- 准备OpenAI API Key(需配置可用的代理地址,避免访问受限)
- 准备《天龙八部》EPUB格式电子书,放入项目根目录
2. 项目初始化与依赖安装
# 初始化项目
npm init -y
# 安装核心依赖
npm install dotenv @zilliz/milvus2-sdk-node @langchain/openai @langchain/community @langchain/textsplitters
3. 环境变量配置(.env文件)
在项目根目录创建.env文件,配置Milvus和OpenAI相关参数:
# Milvus 配置
MILVUS_ADDRESS=你的Milvus服务地址:端口
MILVUS_TOKEN=你的Milvus认证令牌
# OpenAI 配置
OPENAI_API_KEY=你的OpenAI API Key
EMBEDDING_MODEL_NAME=text-embedding-3-large
MODEL_NAME=gpt-3.5-turbo
OPENAI_BASE_URL=OpenAI代理地址
四、实战开发:分模块实现RAG系统
我们将代码拆分为知识库构建和智能问答两大模块,先完成数据入库,再实现问答功能,同时修正代码中的拼写错误、逻辑bug,保证代码可直接运行。
模块一:知识库构建(电子书入库)
核心目标:读取EPUB电子书,拆分文本、生成向量,存入Milvus,包含集合初始化、电子书处理、批量入库三大核心函数。
import "dotenv/config";
import { parse } from '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';
const VECTOR_DIM = 1024; // 修正拼写错误:VECTION_DIM → VECTOR_DIM
const CHUNK_SIZE = 500; // 文本块大小
const CHUNK_OVERLAP = 50; // 文本块重叠长度,保证上下文连贯
const EPUB_FILE = './天龙八部.epub';
// Milvus连接配置
const ADDRESS = process.env.MILVUS_ADDRESS;
const TOKEN = process.env.MILVUS_TOKEN;
// 解析书名
const BOOK_NAME = parse(EPUB_FILE).name;
console.log('待处理书籍:', BOOK_NAME);
// 初始化嵌入模型(文本转向量)
const embeddings = new OpenAIEmbeddings({
apiKey: process.env.OPENAI_API_KEY,
model: process.env.EMBEDDING_MODEL_NAME,
configuration: { baseURL: process.env.OPENAI_BASE_URL },
dimensions: VECTOR_DIM,
});
// 初始化Milvus客户端
const client = new MilvusClient({ address: ADDRESS, token: TOKEN });
/**
* 生成单条文本向量
* @param {string} text - 待向量化的文本
* @returns {number[]} 向量数组
*/
async function getEmbedding(text) {
const result = await embeddings.embedQuery(text);
return result;
}
/**
* 1. 初始化Milvus集合:判断是否存在,不存在则创建+建索引+加载
* @param {number} bookId - 书籍ID
*/
async function ensureBookCollection(bookId) {
try {
// 判断集合是否存在(修正:hasCollection返回布尔值,无需.value)
const hasCollection = await client.hasCollection({ collection_name: COLLECTION_NAME });
console.log('集合是否存在:', hasCollection);
// 集合不存在,创建集合+向量索引
if (!hasCollection) {
console.log(`${COLLECTION_NAME} 集合不存在,开始创建`);
// 创建集合,定义数据表结构
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: VECTOR_DIM },
]
});
console.log('集合创建成功');
// 为向量字段创建索引,加速检索
await client.createIndex({
collection_name: COLLECTION_NAME,
field_name: 'vector',
index_type: IndexType.IVF_FLAT,
metric_type: MetricType.COSINE,
params: { nlist: VECTOR_DIM },
});
console.log('向量索引创建成功');
}
// 加载集合到内存(Milvus检索/入库前提,修正拼写错误)
try {
await client.loadCollection({ collection_name: COLLECTION_NAME });
console.log('集合加载成功');
} catch (err) {
console.log('集合已处于加载状态');
}
} catch (err) {
console.error('集合初始化失败:', err.message);
throw err;
}
}
/**
* 2. 批量插入文本块向量数据
* @param {string[]} chunks - 文本块数组
* @param {number} bookId - 书籍ID
* @param {number} chapterNum - 章节号
* @returns {number} 插入成功的数量
*/
async function insertChunksBatch(chunks, bookId, chapterNum) {
try {
if (!chunks.length) 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
};
})
);
// 批量插入Milvus
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;
}
}
/**
* 3. 加载EPUB电子书并处理入库
* @param {number} bookId - 书籍ID
*/
async function loadAndProcessEPubStreaming(bookId) {
try {
console.log('开始加载EPUB电子书');
// 加载电子书,按章节拆分
const loader = new EPubLoader(EPUB_FILE, { splitChapters: true });
const documents = await loader.load();
console.log(`电子书加载完成,共${documents.length}章`);
// 初始化文本分片器,适配中文拆分
const textSplitter = new RecursiveCharacterTextSplitter({
chunkSize: CHUNK_SIZE,
chunkOverlap: CHUNK_OVERLAP,
separators: ['\n\n', '\n', '。', '!', '?', ',', '、']
});
let totalInserted = 0;
// 遍历章节,逐章处理
for (let chapterIndex = 0; chapterIndex < documents.length; chapterIndex++) {
const chapter = documents[chapterIndex];
const chapterContent = chapter.pageContent;
console.log(`\n处理第 ${chapterIndex + 1}/${documents.length} 章`);
// 拆分章节为短文本块
const chunks = await textSplitter.splitText(chapterContent);
console.log(`本章节拆分为 ${chunks.length} 个文本块`);
if (!chunks.length) {
console.log('跳过空章节');
continue;
}
// 批量插入当前章节文本块
const insertedCount = await insertChunksBatch(chunks, bookId, chapterIndex + 1);
totalInserted += insertedCount;
console.log(`本章节插入成功 ${insertedCount} 个,累计插入 ${totalInserted} 个`);
}
console.log(`\n✅ 电子书处理完成,总计插入 ${totalInserted} 个文本块`);
return totalInserted;
} catch (err) {
console.error('电子书处理失败:', err.message);
throw err;
}
}
// 执行入库主函数
async function buildKnowledgeBase() {
try {
console.log('🚀 开始构建电子书知识库');
const bookId = 1;
// 初始化集合
await ensureBookCollection(bookId);
// 处理电子书并入库
await loadAndProcessEPubStreaming(bookId);
} catch (err) {
console.error('知识库构建失败:', err.message);
}
}
// 执行入库(首次运行执行此函数)
// buildKnowledgeBase();
模块二:智能问答系统(RAG核心)
核心目标:接收用户问题,检索相关小说片段,结合大模型生成精准回答,实现真正的RAG问答。
import 'dotenv/config';
import { MilvusClient, MetricType } from '@zilliz/milvus2-sdk-node';
import { ChatOpenAI, OpenAIEmbeddings } from '@langchain/openai';
// 基础配置
const ADDRESS = process.env.MILVUS_ADDRESS;
const TOKEN = process.env.MILVUS_TOKEN;
const COLLECTION_NAME = 'ebook';
const VECTOR_DIM = 1024;
// 初始化模型与客户端
const embeddings = new OpenAIEmbeddings({
apiKey: process.env.OPENAI_API_KEY,
model: process.env.EMBEDDING_MODEL_NAME,
configuration: { baseURL: process.env.OPENAI_BASE_URL },
dimensions: VECTOR_DIM,
});
// 对话大模型,temperature设为0.7,兼顾准确性与表达流畅度
const model = new ChatOpenAI({
temperature: 0.7,
model: process.env.MODEL_NAME,
apiKey: process.env.OPENAI_API_KEY,
configuration: { baseURL: process.env.OPENAI_BASE_URL },
});
const client = new MilvusClient({ address: ADDRESS, token: TOKEN });
/**
* 文本转向量
*/
async function getEmbedding(text) {
return await embeddings.embedQuery(text);
}
/**
* 1. 语义检索:根据问题检索相关小说片段
* @param {string} question - 用户问题
* @param {number} k - 返回结果数量
* @returns {object[]} 检索结果
*/
async function retrieveRelevantContent(question, k = 3) {
try {
// 问题向量化
const queryVector = await getEmbedding(question);
// Milvus向量检索
const searchResult = await client.search({
collection_name: COLLECTION_NAME,
vector: queryVector,
limit: k,
metric_type: MetricType.COSINE,
output_fields: ['id', 'book_name', 'chapter_num', 'content']
});
return searchResult.results || [];
} catch (err) {
console.log('语义检索失败:', err.message);
return [];
}
}
/**
* 2. RAG问答核心:基于检索内容生成回答
* @param {string} question - 用户问题
* @param {number} k - 检索结果数量
* @returns {string} 最终回答
*/
async function answerEbookQuestion(question, k = 3) {
try {
console.log('🤔 开始处理问题:', question);
// 检索相关内容
const retrievedContent = await retrieveRelevantContent(question, k);
console.log(`📄 检索到 ${retrievedContent.length} 条相关内容`);
// 无相关内容,直接返回提示
if (!retrievedContent.length) {
return '抱歉,未在《天龙八部》中找到相关内容';
}
// 拼接检索结果为上下文
const context = retrievedContent.map((item, i) => `
[片段${i+1}]
章节:第${item.chapter_num}章
内容:${item.content}
`).join('\n\n----\n\n');
// 构造Prompt,约束大模型行为
const prompt = `
你是专业的《天龙八部》小说答疑助手,严格基于以下小说原文片段回答问题,严禁编造内容。
### 参考原文片段
${context}
### 用户问题
${question}
### 回答要求
1. 必须结合参考片段内容,贴合原著情节与人物设定;
2. 可整合多个片段信息,给出完整、详细的回答;
3. 若无相关信息,如实告知用户,不胡乱作答;
4. 语言通顺,可适当引用原文支撑回答。
### 助手回答:
`;
// 调用大模型生成回答
const response = await model.invoke(prompt);
return response.content;
} catch (err) {
console.error('问答处理失败:', err.message);
return '抱歉,处理问题时出现错误,请稍后重试';
}
}
/**
* 3. 问答主函数
*/
async function chatWithEbook() {
try {
console.log('🔗 连接Milvus服务中');
// 加载集合
try {
await client.loadCollection({ collection_name: COLLECTION_NAME });
} catch (err) { /* 忽略已加载报错 */ }
console.log('✅ Milvus连接正常,可开始提问');
// 测试提问
const question = "谁的武功最厉害?";
const answer = await answerEbookQuestion(question);
console.log('\n🗨️ 用户问题:', question);
console.log('\n🤖 助手回答:\n', answer);
} catch (err) {
console.error('问答系统启动失败:', err.message);
}
}
// 启动问答系统
chatWithEbook();
五、代码运行与效果演示
1. 第一步:构建电子书知识库
取消buildKnowledgeBase()的注释,执行代码,等待电子书拆分、向量化、入库完成,控制台会打印入库进度,直至提示“电子书处理完成”。
2. 第二步:启动智能问答
注释buildKnowledgeBase(),执行问答模块代码,测试效果如下:
用户问题:谁的武功最厉害?
助手回答:在《天龙八部》原著中,武功顶尖的人物众多,其中扫地僧堪称战力天花板,他轻松化解萧峰与慕容复的争斗,硬抗降龙十八掌与斗转星移;此外,乔峰(萧峰)的降龙十八掌、段誉的六脉神剑、虚竹的北冥神功与天山折梅手,也都是绝顶武功,而扫地僧的武学修为与境界,远超其他角色,是小说中公认的武功最强者。
可以看到,模型完全基于小说原文生成回答,无任何幻觉,精准贴合原著内容。
六、关键避坑指南(实战踩坑总结)
- Milvus集合判断错误:
hasCollection返回布尔值,无需加.value,否则会重复创建集合报错; - 拼写错误:注意
collection_name、VECTOR_DIM的拼写,错写会导致加载/入库失败; - 集合必须加载:Milvus集合创建后,必须执行
loadCollection加载到内存,否则无法检索/入库; - 文本块大小适配:EPUB电子书拆分时,
CHUNK_SIZE不宜过大或过小,500字符左右适配中文场景; - 向量维度一致:嵌入模型的
dimensions必须和Milvus集合中向量字段的dim保持一致,否则检索失效。
七、总结与扩展
1. 核心总结
本次实战完整实现了电子书RAG问答系统,从文档加载、文本拆分、向量化、向量存储,到语义检索、大模型问答,覆盖RAG全流程,不仅解决了大模型针对专属内容的问答痛点,也为文档问答、知识库搭建等场景提供了可复用的实战方案。
2. 扩展方向
- 适配多格式文件:新增PDF、Word、TXT等Loader,实现多类型文档RAG;
- 优化检索效果:调整向量索引类型、增加检索过滤条件;
- 部署上线:封装为接口,搭配前端页面,做成可对外使用的问答工具;
- 替换模型:改用国产大模型(如通义千问、文心一言),实现本地化部署。
RAG的核心是 “检索” 与 “生成” 的结合,掌握这套电子书RAG开发逻辑,就能轻松适配各类专属知识库场景,再也不用担心大模型幻觉啦