把百万字小说装进向量数据库: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 | 从各种来源加载原始文档 | EPubLoader | splitChapters: true |
| Splitter | 将长文本切分为合适大小的块 | RecursiveCharacterTextSplitter | chunkSize: 500, chunkOverlap: 50 |
| Embedding | 将文本块转为高维向量 | OpenAIEmbeddings | dimensions: 1024 |
| Milvus | 存储向量并支持高效检索 | @zilliz/milvus2-sdk-node | IVF_FLAT + COSINE |
| RAG | 检索相关内容 + LLM 生成回答 | ChatOpenAI | temperature: 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 设计——为电子书量身定制:
| 字段 | 类型 | 作用 |
|---|---|---|
id | VarChar (主键) | 格式 bookId_chapterNum_chunkIndex,全局唯一 |
book_id | VarChar | 书籍标识,支持多本书共存 |
book_name | VarChar | 书名,便于展示和过滤 |
chapter_num | Int32 | 章节号,回答时可引用出处 |
index | Int32 | 块在章节内的序号 |
content | VarChar | 原始文本内容 |
vector | FloatVector | 1024 维 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 核心知识点
| 知识点 | 关键参数 | 作用 |
|---|---|---|
| EPubLoader | splitChapters: true | 按章节加载电子书 |
| TextSplitter | chunkSize: 500, overlap: 50 | 文本分块,保持上下文 |
| OpenAIEmbeddings | dimensions: 1024 | 文本转向量 |
| Milvus createIndex | IVF_FLAT, nlist: 1024 | 聚簇索引,加速检索 |
| Milvus search | COSINE, limit: 5 | 语义相似度搜索 |
| ChatOpenAI | temperature: 0.1 | 基于上下文生成回答 |
10.3 RAG 的本质
RAG 的本质是给大模型装上"外挂记忆":
- 大模型负责理解问题和组织语言(生成能力)
- 向量数据库负责精准定位相关知识(检索能力)
- Prompt 工程负责把两者粘合在一起(增强能力)
没有 RAG,大模型是"闭卷考试";有了 RAG,大模型变成"开卷考试"——答案准确、可追溯、不幻觉。
参考资源
如果这篇文章对你有帮助,欢迎点赞和收藏!