🏯 从零搭建《天龙八部》RAG 智能问答系统 — 完整实战学习日志

🏯 从零搭建《天龙八部》RAG 智能问答系统 --- 完整实战学习日志

阅读本文,你将收获:

  • 一套完整的 RAG(检索增强生成)流程实践
  • LangChain + Milvus + OpenAI 技术栈的落地经验
  • EPUB 电子书加载、分块、向量化、检索的全链路代码解析
  • 向量数据库设计与 IVP_FLAT 索引调优思路

📖 一、项目概览

这个项目做了什么?

我们把金庸经典武侠小说《天龙八部》(约 150 万字的 EPUB 电子书)变成了一本"可对话的书"------你可以像和一位资深金庸迷聊天一样,问它:

  • "鸠摩智会什么武功?"
  • "段誉的六脉神剑是怎么练成的?"
  • "乔峰的身世秘密是什么?"

底层采用 RAG(Retrieval-Augmented Generation,检索增强生成) 架构:先检索小说中最相关的片段,再交给大模型基于这些片段生成准确回答,从根本上解决了大模型"幻觉"和知识截止日期的问题。


🧱 二、技术栈全景图

层级 技术选型 作用
文档加载 @langchain/community EPubLoader 解析 EPUB 格式,按章节拆分
文本分割 @langchain/textsplitters RecursiveCharacterTextSplitter 将长文本切分为语义连贯的块
向量化 @langchain/openai OpenAIEmbeddings 将文本块转为 1024 维向量
向量存储 @zilliz/milvus2-sdk-node Milvus 分布式向量数据库,存储 + 检索
大模型 @langchain/openai ChatOpenAI 基于检索到的上下文生成回答
scss 复制代码
┌──────────────┐    ┌──────────────┐    ┌──────────────┐    ┌──────────────┐
│  EPUB 电子书  │ → │  文本分块器   │ → │  Embedding   │ → │  Milvus 向量库 │
│  (150万字)   │    │  (500字/块)  │    │  (1024维)    │    │  (IVF_FLAT)   │
└──────────────┘    └──────────────┘    └──────────────┘    └──────┬───────┘
                                                                    │
                                                            ┌───────▼───────┐
                                                            │   相似度检索    │
                                                            │  (COSINE)     │
                                                            └───────┬───────┘
                                                                    │
┌──────────────┐    ┌──────────────┐    ┌──────────────┐    ┌───────▼───────┐
│   用户提问     │ → │  问题向量化   │ → │  拼接 Prompt  │ → │   LLM 生成回答 │
└──────────────┘    └──────────────┘    └──────────────┘    └──────────────┘

🔧 三、项目结构

bash 复制代码
tlbb/
├── .env                    # 环境变量(API Key、数据库地址等)
├── package.json            # 依赖管理
├── 天龙八部.epub            # 约 15MB 的小说源文件
├── readme.md               # 项目说明
└── src/
    ├── main.mjs            # 数据预处理:加载 → 分块 → 向量化 → 入库
    ├── query.mjs           # 向量检索:输入问题 → 返回最相似的 Top-K 片段
    └── rag.mjs             # 完整 RAG 流程:检索 + LLM 生成回答

三个核心文件,职责分明:

文件 一句话职责
main.mjs 把电子书"喂"进向量数据库
query.mjs 从向量数据库中"找"相关内容
rag.mjs "检索 + 生成"完整对话流程

🗄️ 四、深入 main.mjs:数据预处理管道

这是整个系统的"地基"------把一本 150 万字的电子书变成向量数据库中可检索的向量。

4.1 向量数据库 Collection 设计

js 复制代码
const COLLECTION_NAME = 'ebook';      // 集合名
const VECTOR_DIM = 1024;              // 向量维度
const CHUNK_SIZE = 500;               // 每个分块 500 字符

Milvus Collection 的表结构(Schema):

字段名 类型 说明
id VarChar(100) 主键,格式:{bookId}_{章节号}_{块序号}
book_id VarChar(100) 书籍 ID(支持多书扩展)
book_name VarChar(200) 书名,如"天龙八部"
chapter_num Int32 章节编号,标记内容来源
index Int32 块在章节内的序号
content VarChar(10000) 文本内容(最多 10000 字符)
vector FloatVector(1024) 文本的向量表示,核心检索字段

💡 设计亮点 :保留 chapter_numindex 字段,检索时能精确定位到原文的章节位置,方便溯源。

4.2 核心函数解析

ensureCollection(bookId) --- 确保集合就绪
js 复制代码
async function ensureCollection(bookId) {
    // 1. 检查集合是否存在
    const hasCollection = await client.hasCollection({
        collection_name: COLLECTION_NAME,
    });

    // 2. 不存在 → 创建集合 + 创建索引
    if (!hasCollection.value) {
        await client.createCollection({ /* 定义字段结构 */ });
        await client.createIndex({
            field_name: 'vector',
            index_type: IndexType.IVF_FLAT,   // IVF 倒排索引
            metric_type: MetricType.COSINE,    // 余弦相似度
            params: { nlist: 1024 },           // 聚类中心数
        });
    }

    // 3. 加载集合到内存(已加载时捕获重复加载异常)
    try {
        await client.loadCollection({ collection_name: COLLECTION_NAME });
    } catch (err) {
        console.log('集合已处于加载状态,无需重复加载');
    }
}

🎯 关键决策:nlist = 1024

IVF_FLAT 使用 K-Means 聚类将向量空间划分为 1024 个簇。检索时先定位最近的簇,再在簇内精确搜索,大幅减少计算量。1024 是一个经验值,适合百万级数据量。

loadAndProcessEPubStreaming(bookId) --- 流式处理管道
js 复制代码
async function loadAndProcessEPubStreaming(bookId) {
    // Step 1: 加载 EPUB
    const loader = new EPubLoader(EPUB_FILE, {
        splitChapters: true,  // 按章节生成多个 Document
    });
    const documents = await loader.load();
    // → 得到 N 个 Document,每个代表一章

    // Step 2: 创建文本分割器
    const textSplitters = new RecursiveCharacterTextSplitter({
        chunkSize: 500,        // 每块 500 字符
        chunkOverlap: 50,      // 块间重叠 50 字符
    });

    // Step 3: 逐章处理(流式,边切边存)
    for (let chapterIndex = 0; chapterIndex < documents.length; chapterIndex++) {
        const chapter = documents[chapterIndex];
        const chunks = await textSplitters.splitText(chapter.pageContent);
        await insertChunksBatch(chunks, bookId, chapterIndex + 1);
    }
}

🧠 为什么用流式而不是一次性? 一本 150 万字的书会产生约 3000+ 个分块,每个块都要调 Embedding API 生成 1024 维向量。流式处理逐章切割、逐章入库,内存友好,失败时不会全盘重来。

4.3 关键参数的选择哲学

参数 取值 为什么
chunkSize 500 小说语义密度适中,500 字足够覆盖一个完整情节片段
chunkOverlap 50 10% 重叠率,保证上下文连贯,避免关键信息落在切割边界
VECTOR_DIM 1024 匹配使用的 Embedding 模型输出维度
separator 默认(\n\n RecursiveCharacterTextSplitter\n\n → \n → 。→ !→ ? 优先级递归切割

🔍 五、深入 query.mjs:纯向量检索

这是最"纯粹"的一步------给定问题文本,在向量数据库中找到语义最相似的 Top-K 个片段。

js 复制代码
async function main() {
    await client.loadCollection({ collection_name: COLLECTION_NAME });

    const query = '段誉会什么武功';
    const queryVector = await getEmbedding(query);

    const searchResult = await client.search({
        collection_name: COLLECTION_NAME,
        vector: queryVector,
        limit: 3,                              // Top-3
        metric_type: MetricType.COSINE,         // 余弦相似度
        output_fields: ['id', 'book_id', 'chapter_num', 'index', 'content'],
    });

    // 打印结果,带上相似度分数
    searchResult.results.forEach((item, index) => {
        console.log(`${index + 1}. [Score: ${item.score.toFixed(4)}]`);
        console.log(`   Content: ${item.content}`);
    });
}

检索流程拆解:

css 复制代码
用户问题 "段誉会什么武功"
        │
        ▼
  Embedding 模型 → 1024 维向量 [0.023, -0.451, ..., 0.187]
        │
        ▼
  Milvus COSINE 相似度检索 → Top-3 最相似文本块
        │
        ▼
  结果 1: Score 0.9234 --- "段誉学会了六脉神剑..."
  结果 2: Score 0.8912 --- "段誉的凌波微步..."
  结果 3: Score 0.8456 --- "北冥神功是段誉..."

💡 余弦相似度(COSINE) 衡量的是向量方向的接近程度,范围为 -1, 1。值越接近 1,语义越相似。它天然适合文本语义相似度计算,因为文本向量的方向长度更有意义。


🤖 六、深入 rag.mjs:检索 + 生成的完整闭环

query.mjs 只做了"找",rag.mjs 加上了"答"------这才是用户感知到的完整体验。

6.1 检索函数

js 复制代码
async function retrieveRelevantContent(question, k = 3) {
    const queryVector = await getEmbeddings(question);
    const searchResult = await client.search({
        collection_name: COLLECTION_NAME,
        vector: queryVector,
        limit: k,                               // 默认返回 Top-3
        output_fields: ['id', 'book_id', 'chapter_num', 'index', 'content'],
    });
    return searchResult.results;
}

6.2 RAG 核心:Prompt 工程设计

js 复制代码
async function answerEbookQuestion(question, k = 3) {
    // 1. 检索相关片段
    const retrievedContent = await retrieveRelevantContent(question, k);

    // 2. 构建上下文
    const context = retrievedContent.map((item, index) => `
        片段${index + 1},章节${item.chapter_num},内容:
            ${item.content}
    `).join('\n');

    // 3. 组装 Prompt
    const prompt = `
        你是个专业的天龙八部小说助手。
        基于小说回答问题,用准确详细的语言。
        请根据以下小说片段内容回答问题:

        ${context}

        用户问题:${question}

        回答要求:
        1. 如果片段中有相关信息,请结合小说内容给出详细准确的回答
        2. 如果没有,请说不知道
        3. 可以综合多个片段的内容,提供完整的答案
        4. 如果片段中没有相关信息,请如实告知用户
        5. 回答要准确,符合小说的情节和人物设定
        6. 可以引用原文内容来支持你的回答

        AI回答:
    `;

    // 4. 调用 LLM
    const response = await model.invoke(prompt);
    return response.content;
}

6.3 Prompt 设计的五个关键约束

约束 目的
"基于小说回答问题" 限定知识来源,拒绝瞎编
"如果没有,请说不知道" 防止 LLM 用自己的预训练知识"脑补"
"综合多个片段" 利用 Top-K 检索的冗余信息拼出完整答案
"符合小说情节和人物设定" 保持角色一致性
"引用原文内容" 增强可信度,读者可以溯源

🧠 这就是 RAG 的本质 :用检索到的外部知识约束大模型的生成,让它只在你提供的"参考资料"范围内作答。没有 RAG,大模型可能把段誉的武功说成"降龙十八掌";有了 RAG,它只会回答"六脉神剑、凌波微步、北冥神功"。


⚙️ 七、Embedding 配置详解

三个文件共用的 Embedding 配置:

js 复制代码
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,                      // 指定输出维度
});
配置项 说明
apiKey OpenAI API 密钥(兼容任何 OpenAI 格式的服务)
model Embedding 模型名称,如 text-embedding-3-small
baseURL 自定义 API 端点,支持中转代理或国产模型
dimensions 显式指定向量维度,确保与 Milvus 的 Schema 一致

🗃️ 八、Milvus 向量数据库关键操作速查

操作 代码 说明
创建客户端 new MilvusClient({ address, token, ssl: true }) Milvus Cloud 使用 TLS 连接
检查集合 client.hasCollection({ collection_name }) 返回 { value: boolean }
创建集合 client.createCollection({ fields: [...] }) 定义主键、标量字段、向量字段
创建索引 client.createIndex({ index_type: 'IVF_FLAT', metric_type: 'COSINE' }) 必须对向量字段建索引才能检索
加载集合 client.loadCollection({ collection_name }) 将集合加载到内存,检索的前提
插入数据 client.insert({ data: [...] }) 批量插入,返回 insert_cnt
向量检索 client.search({ vector, limit, output_fields }) 返回 { results: [{ score, ... }] }

🚀 九、完整运行流程

Step 1:环境准备

env 复制代码
# .env 文件
OPENAI_API_KEY=sk-xxxxxxxx
OPENAI_BASE_URL=https://api.openai.com/v1
EMBEDDINGS_MODEL_NAME=text-embedding-3-small
MODEL_NAME=gpt-4o
MILVUS_ADDRESS=https://your-instance.milvus.cloud:19530
MILVUS_TOKEN=your-milvus-api-token

Step 2:数据入库(首次运行)

bash 复制代码
node src/main.mjs

输出示例:

erlang 复制代码
================================================================================
开始加载集合:
================================================================================
连接数据库...
数据库连接成功
加载集合...
创建集合....
集合创建成功
创建索引
索引创建成功
集合加载成功

开始加载EPUB文件:./天龙八部.epub
加载完成,共50个章节
处理章节1: 50
章节1共12个数据切片
生成向量并插入中...
章节1共12条数据插入完成
处理章节2: 50
...
共插入3156条数据

Step 3:问答交互

bash 复制代码
node src/rag.mjs
markdown 复制代码
集合加载成功
鸠摩智是吐蕃国师,精通多种武功绝学,主要包括:

1. **火焰刀** --- 鸠摩智的成名绝技,以掌力发出炽热刀气...
2. **小无相功** --- 鸠摩智偷学自逍遥派...
3. **少林七十二绝技** --- 鸠摩智在天龙寺一战中展现了多种少林武功...

------ 以上内容综合自第10章、第14章、第18章

🎯 十、核心知识点总结

10.1 RAG 工作流的四个阶段

css 复制代码
[文档加载] → [文本分割] → [向量化存储] → [检索生成]
  EPUB         Chunk          Embedding      Search + LLM

10.2 为什么需要文本分块?

  1. Embedding 模型有输入长度限制(通常是 8192 token)
  2. 语义聚焦:太长的文本包含混杂语义,检索精度下降
  3. 检索粒度:小块更精确地定位到答案所在的具体段落

10.3 为什么需要 chunkOverlap(重叠)?

假设一句话恰好被切在两块的边界:

复制代码
块1:...段誉施展                  ← 缺了下半句
块2:六脉神剑,击退了鸠摩智...      ← 缺了上半句

有了 50 字符的重叠:

复制代码
块1:...段誉施展六脉神剑,击退      ← 完整保留
块2:段誉施展六脉神剑,击退了鸠摩智... ← 完整保留

10.4 IVF_FLAT 索引原理

scss 复制代码
┌─────────────────────────────────────────┐
│              IVF_FLAT 索引               │
│                                          │
│  全量向量                                │
│     │                                    │
│     ▼  K-Means 聚类 (nlist=1024)        │
│  ┌──┴──┐ ┌─────┐ ┌─────┐    ┌─────┐    │
│  │簇 0 │ │簇 1 │ │簇 2 │ ...│簇1023│    │
│  └─────┘ └─────┘ └─────┘    └─────┘     │
│                                          │
│  检索时:定位最近的 N 个簇 → 簇内暴力搜索  │
│  复杂度:O(nlist) + O(N/nlist)           │
└─────────────────────────────────────────┘

10.5 为什么选 COSINE 而不是 L2(欧氏距离)?

  • COSINE:只看方向,不看长度。「乔峰」和「乔峰乔峰」向量方向一致,余弦相似度接近 1
  • L2:计算绝对距离。同一段话重复两遍,L2 距离会变大,即使语义完全相同
  • 文本语义相似度 = 方向相似度,COSINE 是天然选择

🏗️ 十一、架构设计的可扩展性

当前设计已经为扩展留好了口子:

扩展方向 实现方式
多书支持 book_id 字段已预留,存不同 ID + 检索时加过滤条件
更大规模 nlist 从 1024 提升到 4096 或更高
混合检索 结合关键词检索(BM25)+ 向量检索,弥补专有名词的向量盲区
流式回答 ChatOpenAI 的 streaming: true 参数实现逐字输出
对话记忆 引入 LangChain 的 ConversationBufferMemory 支持多轮对话

💡 十二、踩坑笔记 & 最佳实践

坑 1:重复加载集合报错

js 复制代码
// ❌ 直接加载,集合已加载时抛异常
await client.loadCollection({ collection_name: COLLECTION_NAME });

// ✅ 用 try-catch 包裹,优雅处理
try {
    await client.loadCollection({ collection_name: COLLECTION_NAME });
} catch (err) {
    console.log('集合已处于加载状态,无需重复加载');
}

坑 2:EPUB 解析为空

部分 EPUB 文件的分章方式特殊,splitChapters: true 可能失效。解决思路:先用 splitChapters: false 加载全文,再用正则按"第X章"手动切分。

坑 3:Embedding API 速率限制

3000+ 个分块连续调用 Embedding API 可能触发限流。解决方案:

  • insertChunksBatch 中加 await sleep(100) 控制并发
  • 或使用批量 Embedding API(embeddings.embedDocuments(chunks)

最佳实践清单

csharp 复制代码
✅ 函数单一职责:一个函数只做一件事
✅ 返回值可预测:insertChunksBatch 始终返回数字
✅ 错误处理分层:每个 async 函数有自己的 try-catch
✅ 配置集中管理:所有常量在文件顶部统一定义
✅ 流式处理思想:边加载边处理,不堆积数据
✅ 命名即文档:retrieveRelevantContent 函数名就是说明书

📝 十三、写在最后

这个项目虽然代码量不大(三个文件总共约 400 行),但它完整覆盖了 RAG 系统的全链路

  1. 数据层:EPUB 解析 + 递归文本分割
  2. 向量层:OpenAI Embedding + Milvus IVF_FLAT 索引
  3. 检索层:余弦相似度 Top-K 检索
  4. 生成层:Prompt 工程 + LLM 调用

掌握了这套流程,你可以把任何文本知识库------技术文档、法律条款、医学指南、公司内部 Wiki------都变成可对话的智能助手。

🔗 相关资源:


相关推荐
Sciencemio1 小时前
五源紧耦合厘米级定位:无需外部标识的全域机器人部署技术方案
人工智能·计算机视觉·机器人
zoytown1 小时前
Vibe Coding 从 Demo 到交付:12 个必须回答的工程问题
人工智能
无忧.芙桃1 小时前
MySQL数据库原理与实践(四):基本查询
大数据·数据库·mysql
石榴1 小时前
SQL Workbench 0.3.0:给数据库插件加 AI,难的不是接上模型
人工智能
Revolution611 小时前
长命令运行时,Agent 怎样继续处理其他工作
人工智能·llm·claude
橘子星1 小时前
RAG 实战:3 步将整本《天龙八部》存入向量数据库(一)
javascript·人工智能
廋到被风吹走1 小时前
【AI】本周AI领域三大重磅事件
人工智能
秋田君1 小时前
Qt_常用控件使用学习
数据库·qt·学习
萌动的小火苗1 小时前
深度学习中的损失函数与优化算法基础知识
人工智能·深度学习·算法