构建基于 Milvus 向量数据库的 RAG 电子书问答系统

0 阅读7分钟

构建基于 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支持图谱+向量,自带语义推理知识图谱融合场景
QdrantRust 编写,性能优异,开源中小型项目、嵌入式部署
FAISS(Facebook)库而非数据库,适合单机研究、离线批处理

Milvus 由 Zilliz 公司开发,是 CNCF(云原生基金会)毕业项目,专为海量向量检索优化,支持亿级向量秒级响应,且提供完善的 SDK(包括 Node.js),非常适合构建 RAG 系统。


二、RAG 是什么?为什么用它做电子书问答?

2.1 RAG 的核心思想

RAG = 检索(Retrieval) + 生成(Generation)

  1. 检索阶段:用户提问 → 转为向量 → 在向量数据库中找最相关的几段原文。
  2. 生成阶段:把原文片段 + 用户问题 一起喂给大模型 → 生成自然、准确的回答。

这样既避免了大模型“胡说八道”(幻觉),又能让回答紧扣原著内容。

2.2 电子书 RAG 的典型流程

以《三体.epub》为例:

EPUB 文件 
   ↓ (用 EPubLoader 加载)
按章节分割的文档 
   ↓ (用 TextSplitter 切片)
500 字左右的文本块(带章节、页码等元数据)
   ↓ (用 OpenAI Embedding 模型)
每个文本块 → 1024 维向量
   ↓ (存入 Milvus)
向量 + 元数据 存入集合(Collection)
   ↓ (用户提问)
问题 → 向量 → Milvus 检索 Top-K 相似片段
   ↓ (送入 ChatGPT)
生成最终答案

整个过程自动化,用户只需问一句“红岸基地是做什么的?”,系统就能精准定位到相关段落并总结回答。


三、动手构建:三步打造电子书 RAG 系统

我们将分三个脚本完成整个流程:

  1. 数据准备:解析 EPUB → 切片 → 存入 Milvus
  2. 语义检索:根据问题查向量库
  3. 智能问答:结合检索结果生成答案

所有代码基于 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();

四、完整工作流演示

假设用户问:“叶文洁向宇宙发送信号后发生了什么?”

  1. 系统将问题转为 1024 维向量

  2. Milvus 返回 3 个最相关片段:

    • 片段1(第7章):“叶文洁在红岸基地按下发射键,向半人马座α星发送了包含地球信息的强信号……”
    • 片段2(第14章):“八年后,她收到回信:‘不要回答!’——来自三体世界监听员的警告。”
    • 片段3(第18章):“但她仍再次回复,邀请三体文明降临地球,认为人类无法自救。”
  3. 大模型整合信息,输出:

叶文洁在红岸基地向宇宙发送了地球的坐标信号。八年后,她收到了来自三体世界的回信,内容是“不要回答!”,警告她暴露地球位置将带来灾难。然而,出于对人类文明的绝望,叶文洁选择再次回复,主动邀请三体文明接管地球。这一行为直接引发了后续三体舰队向地球进发的危机,成为整个“三体危机”的起点。

答案准确、有出处、不虚构,完美体现 RAG 优势。

五、为什么选择 Milvus?优势总结

  1. 高性能:支持每秒百万级向量检索
  2. 可扩展:单机到分布式无缝升级
  3. 多语言 SDK:Node.js、Python、Go 等全面支持
  4. 丰富索引类型:IVF_FLAT、HNSW、ANNOY 等适配不同场景
  5. 开源免费:社区版功能完整,无厂商锁定

相比直接用 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 的发展,你的“电子书”甚至可以包含插图、公式、语音解说——但核心逻辑不变:用向量连接语义,用检索增强智能

现在,去试试问你的《三体》:“智子是什么?它如何锁死地球科技?”吧!