把百万字小说装进向量数据库:EPUB向量化 + RAG全流程实战

2 阅读8分钟

把百万字小说装进向量数据库:EPUB向量化 + RAG全流程实战

当一本百万字的武侠小说遇向量化 + RAG,会发生什么?本文以金庸经典《天龙八部》EPUB 电子书为例,带你跑通 文档加载 → 文本分块 → 向量嵌入 → Milvus 存储 → 语义检索 → LLM 生成 的完整 RAG 链路。全文代码可直接运行,建议收藏后动手实践。


一、前言:让 AI 真正"读"完一本书

大语言模型虽然强大,但它有一个致命弱点——它没有读过你的私有文档

你问 ChatGPT:"段誉会什么武功?"它能回答,因为它训练数据里可能包含了相关百科。但你问"鸠摩智在天龙寺用了什么招式",它可能就开始"幻觉"了——因为它并没有真正读过小说原文。

RAG(Retrieval-Augmented Generation) 就是解决方案:先把整本书"切碎"存进向量数据库,用户提问时先检索最相关的片段,再把片段喂给大模型生成回答。

                    RAG = 检索 + 生成
                 ┌─────────────────────┐
  用户提问 ─────▶│  向量检索相关文本片段  │
                 │  注入 Prompt 上下文   │
                 │  LLM 基于片段生成回答  │
                 └─────────────────────┘

本文将以《天龙八部》EPUB 电子书为数据源,完整实现这条链路。


二、RAG 全流程架构设计

在动手之前,先从宏观上理解 RAG 的五个核心环节:

┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐
│  Loader  │───▶│ Splitter │───▶│ Embedding│───▶│  Milvus  │───▶│   RAG    │
│ 文档加载  │    │ 文本分块  │    │ 向量嵌入  │    │ 向量存储  │    │ 检索+生成 │
└──────────┘    └──────────┘    └──────────┘    └──────────┘    └──────────┘
    │                │                │                │                │
    ▼                ▼                ▼                ▼                ▼
 EPubLoader      按章节拆分      text-embedding    IVF_FLAT索引     cosine相似度
 CSVLoader       chunk_size=500   1024维向量        COSINE度量       top-k检索
 PDFLoader       overlap=50       百万字向量化      批量插入          LLM生成

2.1 五大环节详解

环节作用技术选型关键参数
Loader从各种来源加载原始文档EPubLoadersplitChapters: true
Splitter将长文本切分为合适大小的块RecursiveCharacterTextSplitterchunkSize: 500, chunkOverlap: 50
Embedding将文本块转为高维向量OpenAIEmbeddingsdimensions: 1024
Milvus存储向量并支持高效检索@zilliz/milvus2-sdk-nodeIVF_FLAT + COSINE
RAG检索相关内容 + LLM 生成回答ChatOpenAItemperature: 0.1

2.2 为什么需要 Splitter?

一本《天龙八部》有上百万字,直接整本丢给 Embedding 模型?不行——模型有输入长度限制,而且整本书变成一个向量,语义信息会被"稀释"成一团模糊。

正确做法是切成小块,每块 500 字左右,这样每个向量都承载一段聚焦的语义信息,检索时才能精准命中。

2.3 为什么需要 Overlap?

切分时如果块与块之间完全独立,一句话可能被拦腰截断,丢失上下文。设置 chunkOverlap: 50 表示相邻块有 50 个字符的重叠,保证语义连贯性。


三、环境准备

3.1 项目结构

ebook-rag/
├── .env                  # 环境变量
├── package.json
├── 天龙八部.epub          # 电子书源文件
├── main.mjs              # EPUB加载 + 分块 + 向量化 + 存储
├── query.mjs             # 语义搜索
└── rag.mjs               # 完整 RAG 问答

3.2 安装依赖

npm init -y
npm install @zilliz/milvus2-sdk-node @langchain/openai @langchain/community @langchain/textsplitters dotenv

3.3 环境变量配置

# Zilliz Cloud 连接信息
MILVUS_ADDRESS=https://your-cluster-url.zillizcloud.com
MILVUS_TOKEN=your_api_key

# OpenAI 配置
OPENAI_API_KEY=your_openai_api_key
OPENAI_BASE_URL=https://api.openai.com/v1
EMBEDDINGS_MODEL_NAME=text-embedding-3-large
MODEL_NAME=gpt-4o-mini

四、第一步:EPUB 加载 + 分块 + 向量化 + 存储

main.mjs 是整个系统的"数据入口",负责将 EPUB 电子书处理成向量并存入 Milvus。

4.1 完整代码

import "dotenv/config";
import { parse } from 'path';
import {
  MilvusClient,
  DataType,
  MetricType,
  IndexType
} from '@zilliz/milvus2-sdk-node';
import { OpenAIEmbeddings } from '@langchain/openai';
import { EPubLoader } from '@langchain/community/document_loaders/fs/epub';
import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters";

// ===== 配置 =====
const COLLECTION_NAME = 'ebook2';
const VECTOR_DIM = 1024;
const CHUNK_SIZE = 500;
const EPUB_FILE = './天龙八部.epub';
const ADDRESS = process.env.MILVUS_ADDRESS;
const TOKEN = process.env.MILVUS_TOKEN;

// 从文件路径中提取书名(如 "天龙八部")
const { name: BOOK_NAME } = parse(EPUB_FILE);
console.log(BOOK_NAME);

// ===== 初始化 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
});

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

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

// ===== 确保集合存在(没有就创建)=====
async function ensureCollection(bookId) {
  try {
    const hasCollection = await client.hasCollection({
      collection_name: COLLECTION_NAME
    });

    if (!hasCollection.value) {
      console.log('创建集合...');
      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
          }
        ]
      });
      console.log('集合创建成功');

      console.log('创建索引...');
      await client.createIndex({
        collection_name: COLLECTION_NAME,
        field_name: 'vector',
        index_type: IndexType.IVF_FLAT,
        metric_type: MetricType.COSINE,
        params: { nlist: 1024 }   // K-Means 聚类的簇数
      });
      console.log('索引创建成功');
    }

    // 每次运行都需要加载集合到内存
    try {
      await client.loadCollection({
        collection_name: COLLECTION_NAME
      });
      console.log('集合加载成功');
    } catch (err) {
      console.error('集合已经处于加载状态');
    }
  } catch (err) {
    console.error('创建集合时出错:', err.message);
  }
}

// ===== 加载 EPUB 并按章节处理 =====
async function loadAndProcessEPubStreaming(bookId) {
  try {
    console.log(`\n开始加载 EPUB 文件: ${EPUB_FILE}`);

    // EPubLoader 加载后,按章节生成多个 Document
    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: 50   // 重叠50个字符,保持上下文连贯性
    });

    let totalInserted = 0;
    const documentLen = documents.length;

    // 逐章节处理:分块 → 向量化 → 插入
    for (let chapterIndex = 0; chapterIndex < documentLen; chapterIndex++) {
      const chapter = documents[chapterIndex];
      const chapterContent = chapter.pageContent;
      console.log(`处理第 ${chapterIndex + 1} / ${documentLen} 章...`);

      const chunks = await textSplitter.splitText(chapterContent);
      console.log(`拆分为 ${chunks.length} 个片段`);

      if (chunks.length === 0) {
        console.log('跳过空章节\n');
        continue;
      }

      console.log('生成向量并插入中...');
      const insertedCount = await insertChunksBatch(
        chunks,
        bookId,
        chapterIndex + 1
      );

      totalInserted += insertedCount;
      console.log(`已插入 ${insertedCount} 条记录\n`);
    }

    console.log(`\n总共插入 ${totalInserted} 条记录\n`);
    return totalInserted;
  } catch (err) {
    console.error('加载 EPUB 文件时出错:', err.message);
  }
}

// ===== 批量插入分块数据 =====
async function insertChunksBatch(chunks, bookId, chapterNum) {
  try {
    if (chunks.length === 0) {
      return 0;
    }

    // 为每个 chunk 生成向量
    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 insertResult = await client.insert({
      collection_name: COLLECTION_NAME,
      data: insertData
    });

    return Number(insertResult.insert_cnt) || 0;
  } catch (err) {
    console.error(`插入章节 ${chapterNum} 的数据时出错:`, err.message);
    throw err;
  }
}

// ===== 主函数 =====
const main = async () => {
  try {
    console.log('='.repeat(80));
    console.log('电子书处理程序');
    console.log('='.repeat(80));
    console.log('\n连接 Milvus...');
    await client.connectPromise;
    console.log('已连接');

    const bookId = 1;
    await ensureCollection(bookId);
    await loadAndProcessEPubStreaming(bookId);
  } catch (err) {
    console.error('主程序出错:', err.message);
  }
};

main().catch(err => {
  console.error('程序异常:', err.message);
});

4.2 核心设计解析

Schema 设计——为电子书量身定制:

字段类型作用
idVarChar (主键)格式 bookId_chapterNum_chunkIndex,全局唯一
book_idVarChar书籍标识,支持多本书共存
book_nameVarChar书名,便于展示和过滤
chapter_numInt32章节号,回答时可引用出处
indexInt32块在章节内的序号
contentVarChar原始文本内容
vectorFloatVector1024 维 Embedding 向量

处理流程图:

天龙八部.epub
      │
      ▼
 EPubLoader (splitChapters: true)
      │
      ▼
 50个章节 Document  ◀── 每章一个 Document
      │
      ├──▶ 第1章 ──▶ TextSplitter ──▶ [chunk0, chunk1, ...] ──▶ 向量化 ──▶ Milvus
      ├──▶ 第2章 ──▶ TextSplitter ──▶ [chunk0, chunk1, ...] ──▶ 向量化 ──▶ Milvus
      ├──▶ ...
      └──▶ 第N章 ──▶ TextSplitter ──▶ [chunk0, chunk1, ...] ──▶ 向量化 ──▶ Milvus

为什么用 splitChapters: true

如果设为 false,整本书会变成一个巨大的 Document,分块时章节边界会被打破——第3章结尾和第4章开头可能被拼到同一个 chunk 里。按章节拆分后,每个 chunk 都属于明确的章节,回答时可以告诉用户"出自第X章"。

为什么用 Promise.all 批量向量化?

const insertData = await Promise.all(
  chunks.map(async (chunk, chunkIndex) => {
    const vector = await getEmbedding(chunk);
    return { ... };
  })
);

Promise.all 会并发执行所有 Embedding 请求,比逐条 await 快得多。一章可能拆出十几个 chunk,并发处理大幅缩短处理时间。


五、第二步:语义搜索——用自然语言查小说

数据入库后,我们来测试语义搜索。query.mjs 演示了如何用自然语言在百万字小说中精准定位相关段落。

5.1 完整代码

import 'dotenv/config';
import {
  MilvusClient,
  MetricType
} 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 = '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) => {
  const result = await embeddings.embedQuery(text);
  return result;
};

async function main() {
  try {
    console.log('Connecting to Milvus...');
    await client.connectPromise;
    console.log('Connected\n');

    // 加载集合到内存(搜索前必须)
    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,                         // 返回最相似的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)}]
      ID: ${item.id}
      Chapter: 第${item.chapter_num}章
      Content: ${item.content}
      `);
    });
  } catch (err) {
    console.error('查询出错:', err.message);
  }
}

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

5.2 运行结果示例

Connecting to Milvus...
Connected

      1. [Score: 0.8421]
      ID: 1_12_7
      Chapter: 第12章
      Content: 段誉自幼不愿学武,后来在无量山琅嬛福地偶得"北冥神功"秘籍,
      又习得"凌波微步"步法...

      2. [Score: 0.7893]
      ID: 1_15_3
      Chapter: 第15章
      Content: 段誉体内的真气越积越多,六脉神剑的剑气时而从指尖激射而出...

      3. [Score: 0.7234]
      ID: 1_12_5
      Chapter: 第12章
      Content: 琅嬛福地中记载的武功包罗万象,段誉只学了其中两门...

关键点: 查询"段誉会什么武功"时,系统找到的文本片段中并没有出现"会什么武功"这几个字,但通过向量相似度,它精准定位到了描述段誉武功的段落——这就是语义搜索超越关键词匹配的地方。


六、第三步:完整 RAG——让 AI 基于原文回答问题

最后一步,将检索到的文本片段注入 Prompt,让大语言模型生成准确、有出处的回答。rag.mjs 是完整实现。

6.1 完整代码

import 'dotenv/config';
import { MilvusClient, MetricType } from '@zilliz/milvus2-sdk-node';
import { ChatOpenAI, OpenAIEmbeddings } from '@langchain/openai';

const ADDRESS = process.env.MILVUS_ADDRESS;
const TOKEN = process.env.MILVUS_TOKEN;
const COLLECTION_NAME = 'ebook2';
const VECTOR_DIM = 1024;

// ===== 初始化 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 }
});

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

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

// ===== R - 检索相关内容 =====
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: ['id', 'book_id', 'chapter_num', 'index', 'content']
    });
    return searchResult.results;
  } catch (err) {
    console.error('检索内容时出错:', err.message);
    return [];
  }
}

// ===== A + G - 增强上下文 + 生成回答 =====
async function answerEbookQuestion(question, k = 3) {
  try {
    const retrievedContent = await retrieveRelevantContent(question, k);

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

    // 将检索结果组装为上下文
    const context = retrievedContent
      .map((item, i) => `
        [片段 ${i + 1}]
        章节:第${item.chapter_num}章
        内容:${item.content}
      `)
      .join('\n\n----\n\n');

    // 构建 RAG Prompt
    const prompt = `你是一个专业的《天龙八部》小说助手。
基于小说回答问题,用准确、详细的语言。

请根据以下小说片段内容回答问题:
${context}

用户问题:${question}

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

AI 助手的回答:`;

    const response = await model.invoke(prompt);
    return response.content;
  } catch (err) {
    console.error('生成回答时出错:', err.message);
    return '抱歉,生成回答时出现错误。';
  }
}

// ===== 主函数 =====
async function main() {
  try {
    await client.connectPromise;

    try {
      await client.loadCollection({
        collection_name: COLLECTION_NAME
      });
      console.log('集合加载成功');
    } catch (err) {
      // 集合可能已处于加载状态
    }

    // 测试问答
    const result = await answerEbookQuestion('鸠摩智会什么武功?', 5);
    console.log(result);
  } catch (err) {
    console.error('主程序出错:', err.message);
  }
}

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

6.2 RAG 流程全景图

用户提问:"鸠摩智会什么武功?"
        │
        ▼
┌───────────────────┐
│   Embedding 编码   │  "鸠摩智会什么武功?" → [0.12, -0.34, ..., 0.89] (1024维)
└───────────────────┘
        │
        ▼
┌───────────────────┐
│  Milvus 向量检索   │  COSINE 相似度,top-5 最相关片段
│  limit: 5          │
└───────────────────┘
        │
        ▼
┌───────────────────┐
│  构建上下文 Context │  [片段1: 第10章...] + [片段2: 第15章...] + ...
└───────────────────┘
        │
        ▼
┌───────────────────┐
│  构建 RAG Prompt   │  System: 你是《天龙八部》助手...
│                    │  Context: {检索到的片段}
│                    │  Question: 鸠摩智会什么武功?
└───────────────────┘
        │
        ▼
┌───────────────────┐
│   LLM 生成回答     │  "鸠摩智精通火焰刀,曾以小无相功催动..."
└───────────────────┘

6.3 运行结果示例

集合加载成功

鸠摩智是吐蕃国师,号称"大轮明王",武功极高。根据小说原文:

1. **火焰刀**:鸠摩智的招牌武功,以掌力化为无形刀气伤人,威力惊人。

2. **小无相功**:鸠摩智从琅嬛福地中学得的逍遥派内功,他以此催动少林七十二绝技,虽形似而力不逮。

3. **少林七十二绝技**:鸠摩智曾强练少林绝技,但因内功根基不对(用小无相功而非少林正宗内功),导致走火入魔。

在第43章中,鸠摩智在西夏枯井中走火入魔,被段誉吸干内力后大彻大悟,从此返俗成为一代高僧。

注意:AI 不仅回答了鸠摩智会什么武功,还能引用具体章节、分析武功来源和结局——这些都是因为 RAG 把相关原文片段喂给了它,而非凭空"记忆"。


七、深入理解:关键参数调优

7.1 chunkSize:分块大小的影响

chunkSize优点缺点适用场景
200定位精准,单块语义聚焦上下文不足,答案可能碎片化精确段落检索
500平衡精度与上下文偶尔跨段信息丢失小说/技术文档(推荐)
1000上下文完整语义稀释,检索精度下降长篇论述类文档

7.2 chunkOverlap:重叠大小的权衡

chunkSize=500, overlap=0:
[0──────500][501────1000][1001────1500]   ← 语义可能被截断

chunkSize=500, overlap=50:
[0──────500][450────950][900────1400]     ← 相邻块有50字重叠,保证连贯

7.3 top-k:检索数量的选择

// k=3:精准但可能遗漏
const result = await answerEbookQuestion('鸠摩智会什么武功?', 3);

// k=5:平衡(推荐)
const result = await answerEbookQuestion('鸠摩智会什么武功?', 5);

// k=10:全面但可能引入噪声,且增加 Token 消耗
const result = await answerEbookQuestion('鸠摩智会什么武功?', 10);

经验法则k 值增大 → 上下文更丰富,但 Token 消耗增加、可能引入不相关内容。一般 3~5 是较好的平衡点。

7.4 IVF_FLAT 索引参数 nlist

await client.createIndex({
  collection_name: COLLECTION_NAME,
  field_name: 'vector',
  index_type: IndexType.IVF_FLAT,
  metric_type: MetricType.COSINE,
  params: { nlist: 1024 }   // 聚类簇数
});
nlist查询速度精度构建索引时间适用数据量
128略低< 10万
1024较快较高中等10万~100万(推荐)
4096稍慢> 100万

八、架构最佳实践

8.1 函数设计原则

// ✅ 一个函数只做一件事,返回值可预测
async function retrieveRelevantContent(question, k = 3) {
  // 只负责检索,返回数组
  return searchResult.results;
}

async function answerEbookQuestion(question, k = 3) {
  // 负责编排:检索 → 构建 Prompt → 生成
  const retrieved = await retrieveRelevantContent(question, k);
  const context = buildContext(retrieved);
  const prompt = buildPrompt(context, question);
  const response = await model.invoke(prompt);
  return response.content;
}

// ❌ 避免:一个函数既检索又生成又打印,职责混乱

8.2 错误处理规范

// ✅ 好的做法:捕获错误,返回有意义的默认值
async function retrieveRelevantContent(question, k = 3) {
  try {
    // ...
    return searchResult.results;
  } catch (err) {
    console.error('检索内容时出错:', err.message);
    return [];   // 返回空数组,调用方可继续处理
  }
}

// ❌ 避免:空的 catch 块,错误被静默吞掉
try {
  // ...
} catch (err) {
  // 什么也不做,问题无法被发现
}

8.3 ID 设计规范

// 主键 ID 格式:bookId_chapterNum_chunkIndex
id: `${bookId}_${chapterNum}_${chunkIndex}`
// 示例:1_12_7  →  第1本书,第12章,第7个分块

// 好处:
// 1. 全局唯一,不会冲突
// 2. 可读性强,一眼看出出处
// 3. 便于按书、按章过滤

九、扩展方向

方向实现思路价值
多书共存不同书用不同 book_id,查询时按 book_id 过滤一个库管理整个书架
混合检索结合向量相似度 + 关键词过滤(expr 参数)"鸠摩智" + 向量搜索,双重保障
引用溯源在回答中标注 [第X章] 出处增强可信度,可验证
流式输出使用 LLM 的 streaming 接口长回答逐字输出,体验更好
重排序检索 top-20 后用 Cross-Encoder 重排取 top-5提升检索精度
多模态小说插图也向量化(CLIP 模型)"找鸠摩智枯井那段配图"

混合检索示例:

const searchResult = await client.search({
  collection_name: COLLECTION_NAME,
  vector: queryVector,
  limit: 5,
  metric_type: MetricType.COSINE,
  expr: `book_id == "1" && chapter_num >= 10`,  // 标量过滤 + 向量搜索
  output_fields: ['id', 'content', 'chapter_num']
});

十、总结

10.1 RAG 全流程回顾

天龙八部.epub
      │
      ▼  EPubLoader (splitChapters: true)
50个章节 Document
      │
      ▼  RecursiveCharacterTextSplitter (chunkSize=500, overlap=50)
数百个文本块
      │
      ▼  OpenAIEmbeddings (1024维)
数百个向量 + 元数据
      │
      ▼  Milvus (IVF_FLAT + COSINE)
向量数据库
      │
      ▼  client.search (limit=5)
top-5 相关片段
      │
      ▼  ChatOpenAI (temperature=0.1)
准确、有出处的回答

10.2 核心知识点

知识点关键参数作用
EPubLoadersplitChapters: true按章节加载电子书
TextSplitterchunkSize: 500, overlap: 50文本分块,保持上下文
OpenAIEmbeddingsdimensions: 1024文本转向量
Milvus createIndexIVF_FLAT, nlist: 1024聚簇索引,加速检索
Milvus searchCOSINE, limit: 5语义相似度搜索
ChatOpenAItemperature: 0.1基于上下文生成回答

10.3 RAG 的本质

RAG 的本质是给大模型装上"外挂记忆"

  • 大模型负责理解问题和组织语言(生成能力)
  • 向量数据库负责精准定位相关知识(检索能力)
  • Prompt 工程负责把两者粘合在一起(增强能力)

没有 RAG,大模型是"闭卷考试";有了 RAG,大模型变成"开卷考试"——答案准确、可追溯、不幻觉。


参考资源


如果这篇文章对你有帮助,欢迎点赞收藏