构建基于 Milvus向量数据库的 RAG 电子书问答系统
在人工智能快速发展的今天,检索增强生成(Retrieval-Augmented Generation, RAG)已成为构建智能问答系统的核心技术之一。它将外部知识库与大语言模型(LLM)结合,既能利用模型的语言能力,又能确保回答内容基于真实、准确的数据源——比如你手中的那本《三体》。
本文将手把手教你如何使用 Milvus 向量数据库 + LangChain + OpenAI Embedding/Chat 模型,构建一个能回答“叶文洁向宇宙发送信号后发生了什么?”这类问题的电子书智能问答系统。全文面向编程小白,无需向量数据库或深度学习背景,只需具备基础 JavaScript/Node.js 知识即可理解。
一、什么是向量数据库?为什么需要 Milvus?
1.1 传统数据库 vs 向量数据库
传统数据库(如 MySQL、PostgreSQL)擅长存储结构化数据(姓名、年龄、订单号),并通过关键词精确匹配或模糊查询(如 LIKE '%叶文洁%')来检索信息。但这种“文本匹配”方式无法理解语义。
例如:
- 用户问:“谁最先联系外星文明?”
- 数据库里有句子:“叶文洁在红岸基地按下按钮,向半人马座α星发送了地球坐标。”
- 但因为没有“联系外星文明”这几个字,传统搜索可能找不到这条信息。
而向量数据库解决的是语义相似性问题。它把文字转化为高维数字向量(embedding),然后通过计算向量之间的距离(如余弦相似度)来判断语义是否相近。
“谁最先联系外星文明?” 和 “叶文洁在红岸基地按下按钮,向半人马座α星发送了地球坐标” 在语义空间中非常接近,即使字面完全不同。
1.2 常见向量数据库对比
目前主流向量数据库包括:
| 数据库 | 特点 | 适用场景 |
|---|---|---|
| Milvus | 开源、高性能、支持分布式、专为 AI 设计 | 大规模生产环境、企业级应用 |
| Pinecone | 全托管 SaaS,易用但收费 | 快速原型、中小项目 |
| Weaviate | 支持图谱+向量,自带语义推理 | 知识图谱融合场景 |
| Qdrant | Rust 编写,性能优异,开源 | 中小型项目、嵌入式部署 |
| FAISS(Facebook) | 库而非数据库,适合单机 | 研究、离线批处理 |
Milvus 由 Zilliz 公司开发,是 CNCF(云原生基金会)毕业项目,专为海量向量检索优化,支持亿级向量秒级响应,且提供完善的 SDK(包括 Node.js),非常适合构建 RAG 系统。
二、RAG 是什么?为什么用它做电子书问答?
2.1 RAG 的核心思想
RAG = 检索(Retrieval) + 生成(Generation)
- 检索阶段:用户提问 → 转为向量 → 在向量数据库中找最相关的几段原文。
- 生成阶段:把原文片段 + 用户问题 一起喂给大模型 → 生成自然、准确的回答。
这样既避免了大模型“胡说八道”(幻觉),又能让回答紧扣原著内容。
2.2 电子书 RAG 的典型流程
以《三体.epub》为例:
EPUB 文件
↓ (用 EPubLoader 加载)
按章节分割的文档
↓ (用 TextSplitter 切片)
500 字左右的文本块(带章节、页码等元数据)
↓ (用 OpenAI Embedding 模型)
每个文本块 → 1024 维向量
↓ (存入 Milvus)
向量 + 元数据 存入集合(Collection)
↓ (用户提问)
问题 → 向量 → Milvus 检索 Top-K 相似片段
↓ (送入 ChatGPT)
生成最终答案
整个过程自动化,用户只需问一句“红岸基地是做什么的?”,系统就能精准定位到相关段落并总结回答。
三、动手构建:三步打造电子书 RAG 系统
我们将分三个脚本完成整个流程:
- 数据准备:解析 EPUB → 切片 → 存入 Milvus
- 语义检索:根据问题查向量库
- 智能问答:结合检索结果生成答案
所有代码基于 Node.js + TypeScript 风格,使用 @zilliz/milvus2-sdk-node 和 @langchain/* 库。
3.1 连接 Milvus 并定义基础配置
import "dotenv/config";
import { parse } from "path";
import {
MilvusClient,
DataType,
MetricType,
IndexType,
} from "@zilliz/milvus2-sdk-node";
import { OpenAIEmbeddings } from "@langchain/openai";
// 配置
const ADDRESS = process.env.MILVUS_ADDRESS;
const TOKEN = process.env.MILVUS_TOKEN;
const COLLECTION_NAME = "ebook";
const VECTION_DIM = 1024; // OpenAI text-embedding-3-large 的维度
const CHUNK_SIZE = 500;
const CHUNK_OVERLAP = 50;
const EPUB_FILE = "./三体.epub"; // 替换为你的电子书路径
const BOOK_NAME = parse(EPUB_FILE).name; // 自动解析为 "三体"
// 初始化 Embedding 模型
const embeddings = new OpenAIEmbeddings({
apiKey: process.env.OPENAI_API_KEY,
model: process.env.EMBEDDING_MODEL_NAME,
configuration: {
baseURL: process.env.OPENAI_BASE_URL,
},
dimensions: VECTION_DIM,
});
// 初始化 Milvus 客户端
const client = new MilvusClient({
address: ADDRESS,
token: TOKEN,
});
// 生成单条文本的 embedding
async function getEmbedding(text) {
const result = await embeddings.embedQuery(text);
return result;
}
3.2 确保集合存在并创建索引(ensureBookCollection)
async function ensureBookCollection(bookId) {
try {
const hasCollection = await client.hasCollection({
collection_name: COLLECTION_NAME,
});
if (!hasCollection.value) {
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: VECTION_DIM },
]
});
console.log('✅ 集合创建成功');
// 创建向量索引
await client.createIndex({
collection_name: COLLECTION_NAME,
field_name: 'vector',
index_type: IndexType.IVF_FLAT,
metric_type: MetricType.COSINE,
params: { nlist: 128 }, // 建议设为 sqrt(总数据量),此处用128作为示例
});
console.log('✅ 向量索引创建成功');
}
// 加载集合到内存(Milvus 查询前必须加载)
await client.loadCollection({ collection_name: COLLECTION_NAME });
console.log('✅ 集合已加载到内存');
} catch (err) {
console.error('❌ 集合初始化失败:', err.message);
throw err;
}
}
3.3 加载 EPUB 并批量插入 Milvus(loadAndProcessEPubStreaming + insertChunksBatch)
import { EPubLoader } from "@langchain/community/document_loaders/fs/epub";
import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters";
async function insertChunksBatch(chunks, bookId, chapterNum) {
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}_${chapterNum}_${chunkIndex}`,
book_id: bookId,
book_name: BOOK_NAME,
chapter_num: chapterNum,
index: chunkIndex,
content: chunk,
vector: vector,
};
})
);
const result = await client.insert({
collection_name: COLLECTION_NAME,
data: insertData,
});
return Number(result.insert_cnt) || 0;
}
async function loadAndProcessEPubStreaming(bookId) {
console.log(`📂 正在加载电子书: ${EPUB_FILE}`);
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,
});
let totalInserted = 0;
for (let i = 0; i < documents.length; i++) {
const chapter = documents[i];
console.log(`챕️ 处理第 ${i + 1}/${documents.length} 章`);
const chunks = await textSplitter.splitText(chapter.pageContent);
if (chunks.length === 0) continue;
console.log(`✂️ 切分为 ${chunks.length} 个片段`);
const count = await insertChunksBatch(chunks, bookId, i + 1);
totalInserted += count;
console.log(`✅ 插入 ${count} 个片段,累计: ${totalInserted}`);
}
return totalInserted;
}
3.4 主函数:执行数据注入
async function main() {
try {
console.log("🔌 连接 Milvus...");
await client.connectPromise;
console.log("✅ 连接成功");
const bookId = "san_ti"; // 可自定义书 ID
await ensureBookCollection(bookId);
await loadAndProcessEPubStreaming(bookId);
console.log("🎉 《三体》电子书已成功注入 Milvus 向量数据库!");
} catch (err) {
console.error("💥 数据注入失败:", err);
}
}
main();
3.5 RAG 问答核心函数(用于检索+生成)
import { ChatOpenAI } from "@langchain/openai";
const model = new ChatOpenAI({
temperature: 0.7,
apiKey: process.env.OPENAI_API_KEY,
modelName: process.env.MODEL_NAME,
configuration: {
baseURL: process.env.OPENAI_BASE_URL,
},
});
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,
metric_type: MetricType.COSINE,
output_fields: ['book_name', 'chapter_num', 'content'],
});
return searchResult.results;
} catch (err) {
console.error("🔍 向量检索失败:", err.message);
return [];
}
}
async function answerEbookQuestion(question, k = 3) {
console.log(`❓ 用户问题: ${question}`);
const results = await retrieveRelevantContent(question, k);
if (results.length === 0) {
return "抱歉,未在《三体》中找到相关信息。";
}
const context = results
.map((item, i) => `
[片段${i + 1}]
章节:第${item.chapter_num}章
内容:${item.content}
`)
.join("\n\n----\n\n");
const prompt = `
你是一个专业的《三体》科幻小说助手。请严格基于以下提供的小说原文片段回答问题。
【提供的《三体》原文】
${context}
【用户问题】
${question}
【回答要求】
1. 仅使用上述片段中的信息作答;
2. 若片段中无相关信息,请明确说明“未找到相关内容”;
3. 回答应准确、简洁,并可引用原文;
4. 不得编造情节或添加书中未提及的内容。
AI助手的回答:
`;
const response = await model.invoke(prompt);
return response.content;
}
6. 测试问答主函数
async function main() {
try {
console.log("🔌 连接 Milvus...");
await client.connectPromise;
await client.loadCollection({ collection_name: COLLECTION_NAME });
console.log("✅ 准备就绪,开始问答...");
const answer = await answerEbookQuestion("智子是如何锁死地球基础科学的?");
console.log("🤖 最终回答:\n", answer);
} catch (err) {
console.error("❌ 问答过程出错:", err);
}
}
main();
四、完整工作流演示
假设用户问:“叶文洁向宇宙发送信号后发生了什么?”
-
系统将问题转为 1024 维向量
-
Milvus 返回 3 个最相关片段:
- 片段1(第7章):“叶文洁在红岸基地按下发射键,向半人马座α星发送了包含地球信息的强信号……”
- 片段2(第14章):“八年后,她收到回信:‘不要回答!’——来自三体世界监听员的警告。”
- 片段3(第18章):“但她仍再次回复,邀请三体文明降临地球,认为人类无法自救。”
-
大模型整合信息,输出:
叶文洁在红岸基地向宇宙发送了地球的坐标信号。八年后,她收到了来自三体世界的回信,内容是“不要回答!”,警告她暴露地球位置将带来灾难。然而,出于对人类文明的绝望,叶文洁选择再次回复,主动邀请三体文明接管地球。这一行为直接引发了后续三体舰队向地球进发的危机,成为整个“三体危机”的起点。
答案准确、有出处、不虚构,完美体现 RAG 优势。
五、为什么选择 Milvus?优势总结
- 高性能:支持每秒百万级向量检索
- 可扩展:单机到分布式无缝升级
- 多语言 SDK:Node.js、Python、Go 等全面支持
- 丰富索引类型:IVF_FLAT、HNSW、ANNOY 等适配不同场景
- 开源免费:社区版功能完整,无厂商锁定
相比直接用 FAISS(内存库)或 Pinecone(付费),Milvus 在成本、性能、可控性上取得最佳平衡。
六、常见问题与优化建议
Q1:切片大小怎么选?
- 小说类:500~800 字较合适(一段情节)
- 技术文档:200~300 字(一个知识点)
- 可通过
chunkOverlap避免关键信息被切断
Q2:Embedding 模型选哪个?
- OpenAI:
text-embedding-3-large(1024维,效果最好) - 开源替代:
BAAI/bge-large-zh-v1.5(中文优化)
Q3:如何提升回答准确性?
- 增加检索数量(k=5)
- 在 Prompt 中强调“仅根据以下内容回答”
- 对检索结果做重排序(Rerank)
总结:RAG 让每个人拥有“私人知识助手”
通过本文,你已掌握构建电子书 RAG 系统的完整链路:从 EPUB 解析、文本切片、向量化存储,到语义检索与智能生成。这套架构不仅适用于《三体》,还可用于:
- 公司内部文档问答
- 法律/医疗知识库
- 个人笔记智能检索
而 Milvus 作为强大的向量引擎,为这一系统提供了坚实的数据底座。未来,随着多模态(图像、音频)RAG 的发展,你的“电子书”甚至可以包含插图、公式、语音解说——但核心逻辑不变:用向量连接语义,用检索增强智能。
现在,去试试问你的《三体》:“智子是什么?它如何锁死地球科技?”吧!