🐉 天龙八部 RAG 知识库实战:从零构建你的武侠 AI 助手

"以彼之道,还施彼身" ------ 用 RAG 技术让 AI 真正读懂金庸武侠世界


📖 项目简介

本项目以金庸经典武侠小说《天龙八部》为数据源,完整实现了一个 RAG(Retrieval-Augmented Generation) 系统。系统能够:

  • 📚 加载 EPUB 电子书并按章节拆分
  • ✂️ 智能分块文本,保持上下文连贯
  • 🧮 将文本向量化并存入 Milvus 向量数据库
  • 🔍 基于语义相似度检索相关内容
  • 🤖 结合 LLM 生成准确、详细的回答

🏗️ 系统架构

css 复制代码
┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│  main.mjs   │────▶│  query.mjs  │────▶│   rag.mjs   │
│  数据入库    │     │  向量检索    │     │  RAG问答     │
└─────────────┘     └─────────────┘     └─────────────┘
      │                    │                    │
      ▼                    ▼                    ▼
┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│ EPUB加载     │     │ Milvus查询   │     │ LLM生成回答  │
│ 文本分块     │     │ 相似度匹配   │     │ 上下文整合   │
│ 向量化存储    │     │             │     │             │
└─────────────┘     └─────────────┘     └─────────────┘

🛠️ 技术栈

技术 用途 说明
Milvus 向量数据库 存储和检索高维向量
LangChain AI 框架 提供文档加载、文本分割、Embedding 等工具
OpenAI Embeddings 文本向量化 将文本转换为 1024 维向量
ChatOpenAI LLM 对话模型 基于检索内容生成回答

📦 依赖安装

json 复制代码
{
  "dependencies": {
    "@langchain/community": "^1.1.29",
    "@langchain/core": "^1.2.9",
    "@langchain/openai": "^1.5.10",
    "@langchain/textsplitters": "^1.0.1",
    "@zilliz/milvus2-sdk-node": "^3.0.4",
    "dotenv": "^17.4.2"
  }
}

📁 项目结构

bash 复制代码
tlbb/
├── src/
│   ├── main.mjs      # 📥 数据入库:加载EPUB → 分块 → 向量化 → 存储
│   ├── query.mjs      # 🔍 向量检索:测试相似度搜索
│   └── rag.mjs        # 🤖 RAG问答:检索 + LLM = 智能回答
├── 天龙八部.epub       # 📚 原始数据源
├── .env               # 🔐 环境变量配置
└── package.json

🚀 模块一:main.mjs ------ 数据入库流水线

核心职责:将 EPUB 电子书转换为向量数据,存入 Milvus 数据库

📋 配置常量

javascript 复制代码
const COLLECTION_NAME = 'ebook2';    // 集合名称
const VECTOR_DIM = 1024;             // 向量维度
const CHUNK_SIZE = 500;              // 每个分块的字符数
const EPUB_FILE = './天龙八部.epub'  // 源文件路径

💡 设计思考

  • VECTOR_DIM = 1024:与 Embedding 模型输出维度一致
  • CHUNK_SIZE = 500:平衡检索精度与上下文完整性

🔧 初始化 Embedding 模型

javascript 复制代码
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
});

async function getEmbedding(text) {
  const result = await embeddings.embedQuery(text);
  return result;
}

📌 函数说明

  • getEmbedding(text):将任意文本转换为 1024 维浮点数组
  • 内部调用 OpenAI 的 embedQuery 接口
  • 返回值:number[](长度 1024 的向量)

🗄️ Milvus 集合管理

javascript 复制代码
const client = new MilvusClient({
  address: ADDRESS,    // Milvus 服务地址
  token: TOKEN         // 认证令牌
});

async function ensureCollection(bookId) {
  // 检查集合是否存在
  const hasCollection = await client.hasCollection({
    collection_name: COLLECTION_NAME
  });

  if (!hasCollection.value) {
    // 创建集合,定义字段结构
    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: 200 },
        { 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 }
      ]
    });

    // 创建 IVF_FLAT 索引,使用余弦相似度
    await client.createIndex({
      collection_name: COLLECTION_NAME,
      field_name: 'vector',
      index_type: IndexType.IVF_FLAT,
      metric_type: MetricType.COSINE,
      params: { nlist: 1024 }
    });
  }

  // 加载集合到内存(每次都需要)
  await client.loadCollection({
    collection_name: COLLECTION_NAME
  });
}

📊 字段设计

字段 类型 说明
id VarChar 主键,格式:{bookId}_{chapter}_{index}
book_id VarChar 书籍编号
book_name VarChar 书籍名称
chapter_num Int32 章节序号
index Int32 分块在章节内的序号
content VarChar 原始文本内容
vector FloatVector 1024 维向量

🔍 索引类型说明

  • IVF_FLAT:倒排文件索引 + 暴力搜索
  • COSINE:余弦相似度,适合文本语义匹配
  • nlist = 1024:聚类簇数,影响检索速度与精度

📥 EPUB 加载与分块处理

javascript 复制代码
async function loadAndProcessEPubStreaming(bookId) {
  // 1. 加载 EPUB 文件,按章节拆分
  const loader = new EPubLoader(EPUB_FILE, {
    splitChapters: true  // 按章节生成多个 Document
  });
  const documents = await loader.load();

  // 2. 初始化文本分割器
  const textsplitters = new RecursiveCharacterTextSplitter({
    chunkSize: CHUNK_SIZE,      // 每块 500 字符
    chunkOverlap: 50            // 重叠 50 字符,保持上下文连贯
  });

  let totalInserted = 0;

  // 3. 逐章节处理
  for (let chapterIndex = 0; chapterIndex < documents.length; chapterIndex++) {
    const chapter = documents[chapterIndex];
    const chunks = await textsplitters.splitText(chapter.pageContent);

    if (chunks.length === 0) continue;  // 跳过空章节

    // 4. 批量插入数据库
    const insertedCount = await insertChunksBatch(chunks, bookId, chapterIndex + 1);
    totalInserted += insertedCount;
  }

  return totalInserted;
}

⚙️ 分块策略

  • chunkSize = 500:每个文本块约 500 字符
  • chunkOverlap = 50:相邻块重叠 50 字符,避免语义断裂

💾 批量插入数据库

javascript 复制代码
async function insertChunksBatch(chunks, bookId, chapterNum) {
  // 并行生成所有分块的向量
  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
      };
    })
  );

  // 批量插入 Milvus
  const insertResult = await client.insert({
    collection_name: COLLECTION_NAME,
    data: insertData
  });

  return Number(insertResult.insert_cnt) || 0;
}

💡 性能优化

  • 使用 Promise.all 并行调用 Embedding API
  • 一次性批量插入,减少数据库交互次数

🏁 主函数入口

javascript 复制代码
const main = async () => {
  console.log('='.repeat(80));
  console.log('电子书处理程序');
  console.log('='.repeat(80));

  // 连接 Milvus
  await client.connectPromise;
  console.log('已连接');

  const bookId = 1;

  // 确保集合已创建并加载
  await ensureCollection(bookId);

  // 加载 EPUB → 分块 → 向量化 → 存储
  await loadAndProcessEPubStreaming(bookId);
};

main().catch(err => console.error(err));

🔄 处理流程

scss 复制代码
EPUB文件
   │
   ▼
┌──────────────────┐
│ EPubLoader.load() │  按章节拆分
└──────────────────┘
   │
   ▼
┌──────────────────────────────┐
│ RecursiveCharacterTextSplitter│  每章再分块
└──────────────────────────────┘
   │
   ▼
┌──────────────────┐
│ getEmbedding()   │  文本 → 1024维向量
└──────────────────┘
   │
   ▼
┌──────────────────┐
│ client.insert()  │  存入 Milvus
└──────────────────┘

🔍 模块二:query.mjs ------ 向量检索测试

核心职责:验证向量检索功能,测试相似度搜索

🔧 初始化配置

javascript 复制代码
import { MilvusClient, MetricType, IndexType, DataType } from '@zilliz/milvus2-sdk-node';
import { OpenAIEmbeddings } from '@langchain/openai';

const COLLECTION_NAME = 'ebook2';
const VECTOR_DIM = 1024;

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
});

const client = new MilvusClient({
  address: ADDRESS,
  token: TOKEN
});

const getEmbedding = async (text) => {
  return await embeddings.embedQuery(text);
};

🔎 执行向量搜索

javascript 复制代码
async function main() {
  // 1. 连接并加载集合
  await client.connectPromise;
  await client.loadCollection({ collection_name: COLLECTION_NAME });

  // 2. 准备查询
  const query = '段誉会什么武功?';
  const queryVector = await getEmbedding(query);  // 查询文本 → 向量

  // 3. 执行向量搜索
  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"]
  });

  // 4. 格式化输出结果
  searchResult.results.forEach((item, index) => {
    console.log(`
    ${index + 1}.[Score:${item.score.toFixed(4)}]
    ID: ${item.id}
    BookId: ${item.book_id}
    Content: ${item.content}
    `);
  });
}

📊 搜索结果示例

makefile 复制代码
1.[Score:0.8923]
ID: 1_5_12
BookId: 1
Content: 段誉身具"北冥神功",能够吸取他人内力...

2.[Score:0.8567]
ID: 1_8_3
BookId: 1
Content: 段誉又学会了"凌波微步",身形飘忽不定...

3.[Score:0.8234]
ID: 1_12_7
BookId: 1
Content: "六脉神剑"乃是大理段氏的至高武学...

🧮 相似度评分说明

  • Score 范围:0 ~ 1(余弦相似度)
  • 越接近 1:语义越相似
  • COSINE 指标:计算向量夹角的余弦值

🤖 模块三:rag.mjs ------ RAG 智能问答

核心职责:结合检索结果与 LLM,生成准确、详细的回答

🔧 完整初始化

javascript 复制代码
import { MilvusClient, MetricType } from '@zilliz/milvus2-sdk-node';
import { ChatOpenAI, OpenAIEmbeddings } from '@langchain/openai';

// Embedding 模型
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
});

// LLM 对话模型
const model = new ChatOpenAI({
  temperature: 0.1,           // 低温度,更确定性的回答
  model: process.env.MODEL_NAME,
  apiKey: process.env.OPENAI_API_KEY,
  configuration: { baseURL: process.env.OPENAI_BASE_URL }
});

// Milvus 客户端
const client = new MilvusClient({
  address: ADDRESS,
  token: TOKEN
});

🌡️ temperature 参数

  • 0.1:接近确定性输出,适合事实性问答
  • 0.7 ~ 1.0:更有创造性,适合创意写作

🔍 核心函数:retrieveRelevantContent

javascript 复制代码
/**
 * 从向量数据库中检索相关内容
 * @param {string} question - 用户问题
 * @param {number} k - 返回结果数量(默认 3)
 * @returns {Array} 检索结果列表
 */
async function retrieveRelevantContent(question, k = 3) {
  try {
    // 1. 将问题转换为向量
    const queryVector = await getEmbedding(question);

    // 2. 执行向量搜索
    const searchResult = await client.search({
      collection_name: COLLECTION_NAME,
      vector: queryVector,
      metric_type: MetricType.COSINE,
      limit: k,
      output_fields: [
        'id', 'book_id', 'book_name',
        'chapter_num', 'index', 'content'
      ]
    });

    return searchResult.results;
  } catch (err) {
    console.error('检索相关内容时出错');
    return [];
  }
}

📌 函数设计原则

  • 单一职责:只负责检索,不处理生成
  • 可配置k 参数控制返回数量
  • 健壮性:异常时返回空数组

🤖 核心函数:answerEbookQuestion

javascript 复制代码
/**
 * 基于 RAG 回答电子书相关问题
 * @param {string} question - 用户问题
 * @param {number} k - 检索结果数量(默认 3)
 * @returns {string} LLM 生成的回答
 */
async function answerEbookQuestion(question, k = 3) {
  // 1. 检索相关内容
  const retrievedContent = await retrieveRelevantContent(question, k);

  if (retrievedContent.length === 0) {
    return '抱歉,我没有找到相关的《天龙八部》内容。';
  }

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

  // 3. 构建 Prompt
  const prompt = `
    你是一个专业的《天龙八部》小说助手。基于小说回答问题,用准确、详细的语言。
    请根据以下小说片段内容回答问题:
    ${context}
    用户问题:${question}

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

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

📝 Prompt 设计要点

  1. 角色设定:专业的《天龙八部》小说助手
  2. 上下文注入:将检索结果格式化后嵌入 Prompt
  3. 回答规范:5 条明确的指令约束
  4. 引用要求:鼓励引用原文增强可信度

🏁 主函数入口

javascript 复制代码
async function main() {
  // 1. 连接 Milvus
  await client.connectPromise;
  console.log('连接成功');

  // 2. 加载集合
  await client.loadCollection({ collection_name: COLLECTION_NAME });
  console.log('集合加载成功');

  // 3. 提问并获取回答
  const result = await answerEbookQuestion('鸠摩智会什么武功?', 5);
  console.log(result);
}

main().catch((err) => console.error('error:', err));

🎯 调用示例

markdown 复制代码
问题:鸠摩智会什么武功?

回答:鸠摩智是吐蕃国师,精通多种绝学:

1. **火焰刀法**:鸠摩智的成名绝技,以深厚内力催动...
2. **小无相功**:逍遥派绝学,鸠摩智曾偷学...
3. **少林七十二绝技**:鸠摩智凭借小无相功催动...

(根据检索到的片段综合回答)

🔄 完整 RAG 流程图

scss 复制代码
                        ┌─────────────────────────────────────┐
                        │           用户提问                   │
                        │    "鸠摩智会什么武功?"              │
                        └──────────────┬──────────────────────┘
                                       │
                                       ▼
                        ┌──────────────────────────────────────┐
                        │     ① 文本向量化 (Embedding)         │
                        │     getEmbedding(question)           │
                        └──────────────┬───────────────────────┘
                                       │
                                       ▼
                        ┌──────────────────────────────────────┐
                        │     ② 向量相似度检索                  │
                        │     client.search()                  │
                        │     metric: COSINE, limit: k         │
                        └──────────────┬───────────────────────┘
                                       │
                                       ▼
                        ┌──────────────────────────────────────┐
                        │     ③ 获取 Top-K 相关片段             │
                        │     retrieveRelevantContent()        │
                        └──────────────┬───────────────────────┘
                                       │
                                       ▼
                        ┌──────────────────────────────────────┐
                        │     ④ 构建 Prompt                    │
                        │     注入上下文 + 用户问题              │
                        └──────────────┬───────────────────────┘
                                       │
                                       ▼
                        ┌──────────────────────────────────────┐
                        │     ⑤ LLM 生成回答                   │
                        │     model.invoke(prompt)             │
                        └──────────────┬───────────────────────┘
                                       │
                                       ▼
                        ┌──────────────────────────────────────┐
                        │     ⑥ 返回最终答案                    │
                        │     "鸠摩智精通火焰刀法..."           │
                        └──────────────────────────────────────┘

🧠 核心概念解析

📐 什么是 Embedding?

arduino 复制代码
"段誉会什么武功?"
        │
        ▼
┌─────────────────────────────────────┐
│        Embedding Model              │
│    (text-embedding-3-small)         │
└─────────────────────────────────────┘
        │
        ▼
[0.023, -0.156, 0.089, ..., 0.234]   ← 1024 维浮点数组
  • 输入:任意长度的文本
  • 输出:固定长度的浮点数组(向量)
  • 特性:语义相似的文本,向量距离更近

📏 什么是余弦相似度?

css 复制代码
向量 A: [0.1, 0.3, 0.5]
向量 B: [0.2, 0.4, 0.6]

余弦相似度 = (A · B) / (|A| × |B|)
         = cos(θ)
         ≈ 0.98 (非常相似)
  • 范围:-1 到 1
  • 1:完全相同方向(语义一致)
  • 0:正交(无关)
  • -1:完全相反

🔗 RAG vs 纯 LLM

对比项 纯 LLM RAG
知识来源 训练数据(可能过时) 实时检索(最新数据)
准确性 可能产生幻觉 基于真实文档
可追溯性 无法验证 可引用原文
领域知识 通用但浅显 深度垂直领域

⚙️ 环境配置

.env 文件

bash 复制代码
# Milvus 配置
MILVUS_ADDRESS=https://your-milvus-instance.zillizcloud.com
MILVUS_TOKEN=your-milvus-token

# OpenAI 配置
OPENAI_API_KEY=your-openai-api-key
OPENAI_BASE_URL=https://api.openai.com/v1

# 模型配置
EMBEDDINGS_MODEL_NAME=text-embedding-3-small
MODEL_NAME=gpt-4o-mini

🚀 运行步骤

bash 复制代码
# 1. 安装依赖
pnpm install

# 2. 数据入库(首次运行)
node src/main.mjs

# 3. 测试检索
node src/query.mjs

# 4. RAG 问答
node src/rag.mjs

💡 最佳实践

✅ 代码设计原则

  1. 单一职责:每个函数只做一件事
  2. 函数命名可读retrieveRelevantContent 清晰表达意图
  3. 单一返回值:函数只返回一种类型的数据
  4. 错误处理:try-catch 包裹,返回安全的默认值

⚠️ 注意事项

  1. 向量维度一致:Embedding 模型输出维度必须与 Milvus 字段定义匹配
  2. 索引创建时机:必须在插入数据前创建索引
  3. 集合加载 :每次查询前需要 loadCollection
  4. API 限流:并行调用 Embedding API 时注意速率限制

🎯 总结

通过本项目,我们完整实现了 RAG 的三大核心流程:

阶段 文件 功能
数据入库 main.mjs EPUB → 分块 → 向量化 → Milvus
向量检索 query.mjs 查询文本 → 向量 → 相似度搜索
智能问答 rag.mjs 检索 + Prompt + LLM → 回答

"侠之大者,为国为民" ------ RAG 技术让 AI 真正成为领域专家的得力助手!


📝 本教程基于 LangChain + Milvus + OpenAI 构建,代码简洁清晰,适合入门学习 RAG 技术。

相关推荐
hh95015 分钟前
Agent Plan × DeepSeek Harness:角色 Prompt 驱动的 Agent 分工优化与协作质量实验
java·前端·人工智能·prompt·adg·agent plan·adg成都社区
阿基拉de_Akir15 分钟前
② 跨层禁止:机器如何拦截非法语义绑定
人工智能
科技小E18 分钟前
把人从百米高空拉下来:自动化AI算法训练服务器DLTM+无人机巡检让风机光伏缺陷无所遁形
人工智能·自动化·无人机
武子康19 分钟前
一次 Agent 失败后,到底该改模型、Prompt 还是 Router?
人工智能·llm·agent
小K讲AI营销19 分钟前
固态电池战局拆解:机器人为何先于汽车吃到红利
大数据·人工智能·区块链
beiju19 分钟前
别急着埋 SaaS:Agent 时代真正被压缩的是人工胶水层
人工智能
让学习成为一种生活方式19 分钟前
黄花蒿LHC基因家族的串联重复驱动扩张及其UV-B胁迫适应性--BMC Plant Biology
人工智能·算法·机器学习
小小测试开发20 分钟前
RAG应用评测:从指标体系到LLM-as-a-Judge的自动化落地
android·运维·人工智能·自动化
lhldsg21 分钟前
课程排课实战指南:从需求分析到系统落地全流程
数据库·数据挖掘·需求分析