🏯 从零搭建《天龙八部》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------都变成可对话的智能助手。

🔗 相关资源:


相关推荐
七夜zippoe1 分钟前
为什么 2026 年每个 Java 团队都该懂 AI Agent
java·开发语言·人工智能
举个栗子。3 分钟前
SwarmForge:AI 智能体协同编程框架,让多个 Agent 在隔离工作区并行协作
人工智能·开源·ai编程
天天进步20155 分钟前
Pixelle-Video 源码解析 #18:声音克隆功能:参考音频如何影响解说效果?
数据库·音视频
AIGC小尼14 分钟前
Windows 本地 AI 漫剧全自动生产线部署完整教程(零基础、全指令、带源码、模型配置、排错方案)
人工智能·windows·ai漫剧
合米AI SOP系统18 分钟前
传统产线如何快速上马落地 AI 防错?合米科技 AI SOP 7天即可上线。
大数据·人工智能·科技
思录Echo18 分钟前
什么决定具身智能的最终走向?多技术路线与落地现实辨析
大数据·人工智能
不懂的浪漫34 分钟前
ToDesk 连接 Linux 后分辨率过低的解决方法
linux·运维·数据库
ShallWeL36 分钟前
Orin 上多模型常驻与显存预算
人工智能·嵌入式硬件·nvidia·orin
xiaohaiAIgeo38 分钟前
【2026年】AI监控加行为分析守护实验室安全
大数据·人工智能·科普知识
IT_陈寒40 分钟前
Python的多线程就是个假把式,我算是体验到了
前端·人工智能·后端