RAG电子书实现学习笔记

9 阅读21分钟

本次学习围绕RAG(Retrieval-Augmented Generation,检索增强生成)技术在电子书场景的落地实现展开,核心是基于LangChain、Milvus和OpenAI Embeddings,完成《天龙八部》EPUB电子书的知识库构建、向量检索及智能问答功能开发。通过对提供的代码和技术文档的逐行拆解、原理梳理和实操复盘,系统掌握RAG技术的核心流程、关键组件及工程化实现细节,现将学习内容整理为以下笔记,兼顾理论认知与实操落地,便于后续复习和拓展应用。

一、RAG技术核心认知

1.1 RAG技术定义与核心价值

RAG是一种结合“检索”与“生成”的AI技术,核心逻辑是:当用户提出问题时,先从预设的知识库中检索出与问题最相关的文本片段,再将这些片段作为上下文输入大语言模型,由模型基于检索到的准确信息生成回答。其核心价值在于解决了大语言模型“知识过时”“事实性错误”“上下文窗口有限”的痛点,让模型的回答既具备生成能力,又能依托具体知识库保证准确性和针对性。

本次案例中,RAG技术的应用场景是“电子书智能问答”——将《天龙八部》EPUB电子书解析为可检索的文本片段,构建向量知识库,当用户提出与小说相关的问题(如“段誉会什么武功?”)时,先从知识库中检索出相关章节片段,再通过大语言模型生成准确、详细的回答,实现“精准检索+智能生成”的闭环。

1.2 RAG核心流程梳理

结合本次电子书实现案例,RAG的完整流程可拆解为6个关键步骤,每个步骤对应具体的技术实现和代码模块,是后续工程开发的核心框架:

  1. 知识库构建:将原始EPUB电子书文件作为数据源,通过加载器解析为可处理的文档格式,这是RAG的基础,也是检索准确性的前提。
  2. 文档加载(Loader) :使用LangChain社区提供的EPubLoader,读取EPUB文件内容,支持按章节分割,将电子书拆分为结构化的文档对象。
  3. 文本分割(Splitter) :由于大语言模型和向量数据库对文本长度有限制,需将解析后的章节内容分割为固定长度的文本片段(Chunk),同时保留片段间的重叠部分,避免上下文断裂。
  4. 文档结构化:将分割后的文本片段整理为包含“内容(pageContent)”和“元数据(meta)”的文档对象,元数据包括书籍名称、章节号、片段索引等,便于后续检索和溯源。
  5. 向量嵌入(Embedding Model) :通过OpenAI Embeddings将文本片段转换为高维向量,将文本的语义信息转化为可计算的数值形式,为后续向量检索提供支持。
  6. 向量存储与检索(Milvus) :将生成的文本向量及对应的元数据存储到Milvus向量数据库中,建立向量索引,实现高效的语义检索,快速匹配与用户问题最相关的文本片段。

补充说明:本次案例中,RAG流程还延伸了“智能生成”环节——将检索到的相关文本片段作为上下文,输入ChatOpenAI模型,生成符合用户需求的自然语言回答,完成从“检索”到“生成”的完整闭环。

二、核心技术栈解析

本次RAG电子书实现依赖4大核心技术栈,分别是LangChain(文档处理与流程编排)、Milvus(向量数据库)、OpenAI Embeddings(向量生成)、Node.js(工程化开发环境),各技术栈的核心作用、关键API及使用场景如下,结合代码细节逐一拆解。

2.1 LangChain:RAG流程的核心编排工具

LangChain是一个用于构建LLM应用的开发框架,核心优势是提供了丰富的组件(Loader、Splitter、Embeddings、Chain等),可快速编排RAG的完整流程,无需重复开发基础功能。本次案例中主要使用LangChain的3个核心模块,具体解析如下:

2.1.1 @langchain/community:社区组件库(Loader)

@langchain/community是LangChain的社区扩展库,提供了大量第三方数据源的加载器(Loader),本次案例中使用的EPubLoader就来自该库,其核心作用是解析EPUB格式的电子书文件,将其转换为LangChain可处理的Document对象。

关键代码解析:

import { EPubLoader } from '@langchain/community/document_loaders/fs/epub';

// 初始化EPUB加载器,设置按章节分割
const loader = new EPubLoader(EPUB_FILE, {
    splitChapters: true,  // 按章节分割电子书,生成多个文档对象
});

// 加载电子书,返回Document数组
const documents = await loader.load();

补充说明:EPubLoader的核心参数splitChapters设为true时,会自动识别电子书的章节结构,每个章节生成一个Document对象,每个Document对象包含pageContent(章节内容)和meta(元数据,如章节标题),便于后续按章节处理内容。

2.1.2 @langchain/textsplitters:文本分割工具

由于EPUB章节内容通常较长,超过向量模型和向量数据库的处理上限,需使用文本分割器将章节内容拆分为固定长度的片段(Chunk)。本次案例中使用的是RecursiveCharacterTextSplitter(递归字符文本分割器),其核心优势是按“段落换行、标点符号”递归分割,最大程度保留文本的语义完整性,同时支持设置片段长度和重叠度。

关键代码解析:

import { RecursiveCharacterTextSplitter } from '@langchain/textsplitters';

// 初始化文本分割器
const textSplitter = new RecursiveCharacterTextSplitter({
    chunkSize: 500,       // 每个片段的最大长度(字符数)
    chunkOverlap: 50,     // 片段间的重叠长度(字符数),避免上下文断裂
    // 默认分割符:\n\n(段落换行)、\n(换行符)、。、,等标点符号
});

// 分割单个章节内容,返回片段数组
const chunks = await textSplitter.splitText(chapterContent);

参数说明:

  • chunkSize:每个文本片段的最大字符数,本次设置为500,需根据向量模型的维度(本次为1024)和Milvus的存储限制调整,避免片段过长导致向量生成失真。
  • chunkOverlap:片段间的重叠部分,本次设置为50,目的是确保相邻片段的上下文连续性,避免因分割导致关键信息断裂(如一句话被分割到两个片段中)。

2.1.3 @langchain/openai:OpenAI相关组件

该模块提供了OpenAI Embeddings(向量生成)和ChatOpenAI(对话生成)两个核心组件,分别用于将文本转换为向量、基于检索到的上下文生成回答。

关键代码解析(OpenAI Embeddings):

import { OpenAIEmbeddings } from '@langchain/openai';

// 初始化OpenAI Embeddings
const embeddings = new OpenAIEmbeddings({
    apiKey: process.env.OPENAI_API_KEY,          // OpenAI API密钥(从环境变量读取)
    model: process.env.EMBEDDING_MODEL_NAME,     // 嵌入模型名称(如text-embedding-ada-002)
    configuration: {
        baseURL: process.env.OPENAI_BASE_URL,    // OpenAI API基础地址(可配置代理)
    },
    dimensions: VECTION_DIM,                     // 向量维度(与Milvus集合定义一致,本次为1024)
});

// 生成单个文本片段的向量
async function getEmbedding(text){
    const result = await embeddings.embedQuery(text);
    return result;
}

关键代码解析(ChatOpenAI):

import { ChatOpenAI } from '@langchain/openai';

// 初始化ChatOpenAI模型
const model = new ChatOpenAI({
    temperature: 0.7,                            // 生成温度,0.7表示中等随机性,兼顾准确性和丰富性
    apiKey: process.env.OPENAI_API_KEY,          // OpenAI API密钥
    model: process.env.MODEL_NAME,               // 对话模型名称(如gpt-3.5-turbo)
    configuration: {
        baseURL: process.env.OPENAI_BASE_URL,    // 基础地址(可配置代理)
    },
});

// 基于上下文生成回答
const response = await model.invoke(prompt);

2.2 Milvus:向量数据库核心组件

Milvus是一款开源的向量数据库,专门用于存储、索引和检索高维向量,核心优势是支持高效的近似最近邻搜索(ANN),能够快速匹配与用户查询向量最相似的文本向量,是RAG技术中“检索”环节的核心载体。本次案例中使用Milvus 2.x版本,通过@zilliz/milvus2-sdk-node SDK进行操作,核心功能包括集合管理、向量插入、向量搜索等。

2.2.1 Milvus核心概念

在开始代码解析前,需先掌握Milvus的3个核心概念,避免理解偏差:

  • 集合(Collection):相当于关系型数据库中的“表”,用于存储向量数据及对应的元数据,本次案例中集合名称为“ebook”。
  • 字段(Field):相当于关系型数据库中的“列”,本次案例中定义了7个字段,包括主键(id)、元数据字段(book_id、book_name等)和向量字段(vector)。
  • 索引(Index):用于加速向量检索,本次案例中为vector字段创建IVF_FLAT索引,搭配COSINE(余弦相似度)作为度量方式,用于计算查询向量与存储向量的相似度。

2.2.2 Milvus核心操作代码解析

本次案例中,Milvus的操作主要集中在3个方面:集合初始化(ensureCollection)、向量插入(insertChunksBatch)、向量搜索(retrieveRelevantContent),逐一拆解如下:

(1)集合初始化(ensureCollection)

核心作用:判断“ebook”集合是否存在,若不存在则创建集合、定义字段 schema、创建索引,最后将集合加载到内存(便于后续检索),确保向量存储的基础环境就绪。

import { MilvusClient, DataType, MetricType, IndexType } from '@zilliz/milvus2-sdk-node';

// 初始化Milvus客户端
const client = new MilvusClient({
    address: process.env.MILVUS_ADDRESS,  // Milvus服务地址(从环境变量读取)
    token: process.env.MILVUS_TOKEN,      // Milvus访问令牌(从环境变量读取)
});

async function ensureCollection(bookId){
    try{
        // 1. 判断集合是否存在
        const hasCollection = await client.hasCollection({
            collection_name: COLLECTION_NAME,
        });

        // 2. 若集合不存在,创建集合并定义schema
        if(!hasCollection.value){
            console.log(`集合${COLLECTION_NAME}不存在,创建集合`);
            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,       // 书籍ID,用于区分不同书籍
                    },
                    {
                        name: 'book_name',
                        data_type: DataType.VarChar,
                        max_length: 100,       // 书籍名称(如《天龙八部》)
                    },
                    {
                        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: VECTION_DIM,       // 向量维度(与OpenAI Embeddings一致)
                    },
                ]
            });

            // 3. 为vector字段创建索引,加速检索
            await client.createIndex({
                collection_name: COLLECTION_NAME,
                field_name: 'vector',
                index_type: IndexType.IVF_FLAT,  // 索引类型,IVF_FLAT适合中小规模数据
                metric_type: MetricType.COSINE,  // 相似度度量方式,余弦相似度
                params: {
                    nlist: VECTION_DIM,          // 聚类数量,通常与向量维度一致
                },
            });
            console.log('索引创建成功');
        }

        // 4. 将集合加载到内存(Milvus检索需先加载集合)
        try{
            await client.loadCollection({
                collection_name: COLLECTION_NAME,
            });
            console.log('集合加载成功');
        } catch(err){
            console.error('集合已处于加载状态');
        }
    }catch(err){
        console.error(`集合${COLLECTION_NAME}创建失败: ${err.message}`);
        throw err;
    }
}

关键说明:

  • schema设计:字段的类型和长度需根据实际需求定义,例如content字段设为VarChar(10000),确保能容纳最长的文本片段;vector字段设为FloatVector,维度与OpenAI Embeddings的dimensions一致(1024),否则会导致向量插入失败。
  • 索引选择:IVF_FLAT是Milvus中最基础的索引类型,查询准确率高,适合数据量不大(如本次电子书片段,预计数千条)的场景;若数据量较大,可选择IVF_SQ8、HNSW等索引,兼顾检索速度和准确率。
  • 集合加载:Milvus的集合默认不加载到内存,检索前需通过loadCollection将集合加载到内存,加载后可重复使用,无需多次加载。
(2)向量插入(insertChunksBatch)

核心作用:将分割后的文本片段(chunks)转换为向量,搭配对应的元数据,批量插入到Milvus集合中,完成知识库的构建。为提升性能,采用Promise.all实现向量生成的并发处理,减少等待时间。

async function insertChunksBatch(chunks, bookId, chapterNum){
    try {
        if(chunks.length === 0){
            console.log('空段落,跳过');
            return 0;
        }

        // 并发生成向量,构建插入数据(符合Milvus集合schema)
        const insertData = await Promise.all(
            chunks.map(async (chunk,chunkIndex) => {
                const vector = await getEmbedding(chunk);  // 生成文本片段的向量
                return {
                    id: `${bookId}_${chapterNum}_${chunkIndex}`,  // 主键,格式:书籍ID_章节号_片段索引
                    book_id: bookId,
                    book_name: BOOK_NAME,
                    chapter_num: chapterNum,
                    index: chunkIndex,
                    content: chunk,
                    vector,  // 文本片段的向量
                }
            })
        );

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

        return Number(insertResult.insert_cnt) || 0;  // 返回插入成功的数量
    } catch(err){
        console.error(`插入段落失败: ${err.message}`);
        throw err;
    }
}

关键优化点:

  • 并发向量生成:使用Promise.all并发处理多个文本片段的向量生成,相比串行生成,大幅提升处理效率,尤其适合章节内容较多、片段数量大的场景。
  • 主键设计:主键id采用“bookId_chapterNum_chunkIndex”的格式,确保每条数据的唯一性,同时便于后续根据主键溯源文本片段的来源(哪本书、哪一章节、哪个片段)。
(3)向量搜索(retrieveRelevantContent)

核心作用:将用户的查询问题(如“段誉会什么武功?”)转换为向量,在Milvus集合中检索与该向量最相似的top N个文本片段,返回相关片段的元数据和内容,为后续智能生成提供上下文。

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,                                     // 检索top k个最相似的片段(默认k=3)
            metric_type: MetricType.COSINE,               // 相似度度量方式(与索引一致)
            output_fields: ['id', 'book_id', 'book_name', 'chapter_num', 'index', 'content']  // 需要返回的字段
        });
        return searchResult.results  // 返回检索结果(包含相似度分数和相关字段)
    } catch(err) {
        console.log('向量搜索失败:', err.message);
        return [];
    }
}

关键说明:

  • limit参数:控制检索返回的片段数量,本次默认设为3,数量过多会增加大语言模型的上下文负担,数量过少可能导致信息不足,需根据实际需求调整。
  • 相似度分数:检索结果中每个片段会包含一个score(余弦相似度分数),分数越接近1,说明该片段与查询问题的语义相似度越高,后续生成回答时可优先参考高分片段。

2.3 Node.js:工程化开发环境

本次案例采用Node.js作为开发环境,核心依赖dotenv(环境变量管理)、path(文件路径处理)等基础模块,确保代码的可移植性和安全性。

关键代码解析(环境变量与路径处理):

import 'dotenv/config';  // 加载.env文件中的环境变量
import { parse } from 'path';

// 从环境变量读取配置
const ADDRESS = process.env.MILVUS_ADDRESS;
const TOKEN = process.env.MILVUS_TOKEN;
const EPUB_FILE = './天龙八部.epub';  // EPUB文件路径
const BOOK_NAME = parse(EPUB_FILE).name;  // 解析书籍名称(从文件路径中提取)

补充说明:使用dotenv管理环境变量,将Milvus地址、OpenAI API密钥等敏感信息存储在.env文件中,避免硬编码到代码中,提升代码的安全性和可维护性(.env文件需加入.gitignore,避免泄露敏感信息)。

三、完整工程流程拆解(分模块解析)

本次RAG电子书实现的完整工程,可分为3个核心模块,分别对应“知识库构建”“向量检索”“智能问答”,每个模块独立运行且相互关联,形成完整的RAG闭环。以下结合代码,逐模块拆解流程、核心逻辑和注意事项。

3.1 模块一:知识库构建(电子书解析与向量入库)

该模块的核心功能是将《天龙八部》EPUB电子书解析为文本片段,生成向量并插入到Milvus集合中,完成RAG知识库的构建。完整流程包括:Milvus集合初始化 → EPUB文件加载 → 章节分割 → 向量生成与批量插入,对应的代码是工程中的第一个main函数。

3.1.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 = 'ebook'; // 集合名称
const VECTION_DIM = 1024;       // 向量维度
const CHUNK_SIZE = 500;         // 文本片段长度
const CHUNK_OVERLAP = 50;       // 片段重叠长度
const EPUB_FILE = './天龙八部.epub'; // EPUB文件路径
const ADDRESS = process.env.MILVUS_ADDRESS;
const TOKEN = process.env.MILVUS_TOKEN;
const BOOK_NAME = parse(EPUB_FILE).name;

// 初始化组件
const embeddings = new OpenAIEmbeddings({
    apiKey: process.env.OPENAI_API_KEY,
    model: process.env.EMBEDDING_MODEL_NAME,
    configuration: { baseURL: process.env.OPENAI_BASE_URL },
    dimensions: VECTION_DIM,
});
const client = new MilvusClient({ address: ADDRESS, token: TOKEN });

// 生成向量的工具函数
async function getEmbedding(text){
    const result = await embeddings.embedQuery(text);
    return result;
}

// 集合初始化函数(前文已解析)
async function ensureCollection(bookId){ ... }

// 批量插入文本片段(前文已解析)
async function insertChunksBatch(chunks, bookId, chapterNum){ ... }

// EPUB文件加载与处理
async function loadAndProcessEpubStreaming(bookId){
    try{
        console.log(`开始处理EPUB文件${EPUB_FILE}`);
        // 1. 加载EPUB文件,按章节分割
        const loader = new EPubLoader(EPUB_FILE, { splitChapters: true });
        const documents = await loader.load();

        // 2. 初始化文本分割器
        const textSplitter = new RecursiveCharacterTextSplitter({
            chunkSize: CHUNK_SIZE,
            chunkOverlap: CHUNK_OVERLAP,
        });

        let totalInserted = 0; // 累计插入片段数
        // 3. 遍历每个章节,分割并插入
        for(let chapterIndex = 0; chapterIndex < documents.length; chapterIndex++){
            const chapter = documents[chapterIndex];
            const chapterContent = chapter.pageContent;
            console.log(`处理第${chapterIndex + 1}/${documents.length}章`);

            // 4. 分割章节内容为片段
            const chunks = await textSplitter.splitText(chapterContent);
            console.log(`拆分为${chunks.length}个段落`);

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

            // 5. 批量插入片段(生成向量并插入Milvus)
            console.log('生成向量并插入中...');
            const insertedCount = await insertChunksBatch(chunks, bookId, chapterIndex+1);
            totalInserted += insertedCount;
            console.log(`插入成功${insertedCount}个段落,累计插入${totalInserted}个段落`);
        }

        console.log(`\n处理完成,共插入${totalInserted}个段落`);
        return totalInserted;
    }catch(err){
        console.error(`EPUB文件${EPUB_FILE}处理失败: ${err.message}`);
        throw err;
    }
}

// 主函数:执行知识库构建流程
async function main(){
    try{
        console.log('电子书处理');
        console.log('连接milvus');
        await client.connectPromise; // 连接Milvus
        console.log('连接成功');

        const bookId = 1; // 书籍ID(可根据实际需求调整)
        await ensureCollection(bookId); // 初始化集合
        await loadAndProcessEpubStreaming(bookId); // 处理EPUB并插入向量
    }catch(err){
        console.error(err);
    }
}

main();

3.1.2 核心流程梳理

  1. 初始化配置:导入依赖、定义常量(集合名称、向量维度、片段长度等)、初始化OpenAI Embeddings和Milvus Client。
  2. 连接Milvus:通过client.connectPromise连接Milvus服务,确保后续操作正常。
  3. 集合初始化:调用ensureCollection函数,创建集合(若不存在)、定义schema、创建索引、加载集合。
  4. EPUB处理:调用loadAndProcessEpubStreaming函数,加载EPUB文件、按章节分割、将章节拆分为文本片段。
  5. 向量插入:对每个文本片段生成向量,批量插入到Milvus集合中,统计插入数量。

3.1.3 注意事项

  • EPUB文件路径:确保EPUB_FILE路径正确,若文件不在项目根目录,需填写完整相对路径或绝对路径,否则会导致加载失败。
  • 向量维度一致性:OpenAI Embeddings的dimensions、Milvus集合中vector字段的dim、索引创建时的nlist,三者需保持一致(本次均为1024),否则会导致向量插入或检索失败。
  • 空章节处理:部分EPUB章节可能为空(如目录、版权页),需跳过空章节,避免生成空向量和无效数据。

3.2 模块二:向量检索(基础检索功能)

该模块是RAG流程中“检索”环节的基础实现,核心功能是接收用户查询问题,将其转换为向量,在Milvus集合中检索相关文本片段,并输出检索结果(相似度分数、片段内容、元数据等),对应的代码是工程中的第二个main函数。

3.2.1 完整代码解析

import 'dotenv/config';
import { MilvusClient, DataType, MetricType, IndexType } 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 = 'ebook';
const VECTION_DIM = 1024;

// 初始化组件
const embeddings = new OpenAIEmbeddings({
    apiKey: process.env.OPENAI_API_KEY,
    model: process.env.EMBEDDING_MODEL_NAME,
    configuration: { baseURL: process.env.OPENAI_BASE_URL },
    dimensions: VECTION_DIM,
});
const client = new MilvusClient({ address: ADDRESS, token: TOKEN });

// 生成向量的工具函数(与模块一一致)
async function getEmbedding(text){
    const result = await embeddings.embedQuery(text);
    return result;
}

// 主函数:执行向量检索
async function main(){
    try{
        console.log('Connecting to Milvus...');
        await client.connectPromise; // 连接Milvus

        // 加载集合(若未加载)
        try{
            await client.loadCollection({ collection_name: COLLECTION_NAME });
        }catch(err){
            console.log('Collection already loaded:', err.message);
        }

        // 用户查询问题
        const query = '段誉会什么武功?';
        // 1. 将查询问题转换为向量
        const queryVector = await getEmbedding(query);

        // 2. 在Milvus中检索相关片段
        const searchResult = await client.search({
            collection_name: COLLECTION_NAME,
            vector: queryVector,
            limit: 3, // 检索top 3个最相似片段
            metric_type: MetricType.COSINE,
            output_fields: ['id', 'content', 'book_id', 'chapter_num', 'index', 'book_name'],
        })

        // 3. 输出检索结果
        searchResult.results.forEach((item, index) => {
            console.log(`第${index + 1}个结果: Score=${item.score.toFixed(4)}`);
            console.log(`ID: ${item.id}`);
            console.log(`Content: ${item.content}`);
            console.log(`Book ID: ${item.book_id}`);
            console.log(`Chapter Number: ${item.chapter_num}`);
            console.log(`Index: ${item.index}`);
            console.log(`Book Name: ${item.book_name}`);
            console.log('------------------------------');
        })
    }catch(err){
        console.error('Connect to Milvus failed:', err.message);
    }
}

main();

3.2.2 核心逻辑与输出说明

该模块的核心逻辑是“查询向量生成 → 向量检索 → 结果输出”,其中:

  • 查询向量生成:通过getEmbedding函数,将用户的自然语言问题(如“段誉会什么武功?”)转换为1024维的向量,与知识库中存储的文本片段向量格式一致。
  • 向量检索:调用client.search函数,根据余弦相似度检索top 3个最相关的片段,返回的searchResult.results包含每个片段的相似度分数(score)和预设的输出字段。
  • 结果输出:遍历检索结果,打印每个片段的相似度分数、主键、内容、章节号等信息,便于查看检索的准确性,为后续智能生成提供参考。

示例输出(模拟):

Connecting to Milvus...
Collection already loaded: ...
第1个结果: Score=0.8923
ID: 1_5_2
Content: 段誉无意间习得凌波微步,这门武功是逍遥派的独门轻功,踏雪无痕,闪避能力极强,同时还能配合北冥神功吸收他人内力。
Book ID: 1
Chapter Number: 5
Index: 2
Book Name: 天龙八部
------------------------------
第2个结果: Score=0.8756
ID: 1_8_1
Content: 段誉在无量山洞中,意外发现了北冥神功的秘籍,习得后可吸收他人内力化为己用,解决了自身内力不足的问题。
Book ID: 1
Chapter Number: 8
Index: 1
Book Name: 天龙八部
------------------------------

3.3 模块三:智能问答(RAG完整闭环)

该模块是RAG技术的完整实现,在模块二“向量检索”的基础上,增加了“上下文格式化”和“智能生成”环节,将检索到的相关文本片段作为上下文,输入ChatOpenAI模型,生成符合用户需求的自然语言回答,完成从“检索”到“生成”的闭环,对应的代码是工程中的第三个main函数。

3.3.1 完整代码解析

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

// 配置常量(与前两个模块一致)
const ADDRESS = process.env.MILVUS_ADDRESS;
const TOKEN = process.env.MILVUS_TOKEN;
const COLLECTION_NAME = 'ebook';
const VECTION_DIM = 1024;

// 初始化组件(新增ChatOpenAI模型)
const embeddings = new OpenAIEmbeddings({
    apiKey: process.env.OPENAI_API_KEY,
    model: process.env.EMBEDDING_MODEL_NAME,
    configuration:{ baseURL: process.env.OPENAI_BASE_URL },
    dimensions: VECTION_DIM,
});
const model = new ChatOpenAI({
    temperature: 0.7,
    apiKey: process.env.OPENAI_API_KEY,
    model: process.env.MODEL_NAME,
    configuration:{ baseURL: process.env.OPENAI_BASE_URL },
});
const client = new MilvusClient({ address: ADDRESS, token: TOKEN });

// 生成向量的工具函数(与前两个模块一致)
async function getEmbedding(text) {
    const result = await embeddings.embedQuery(text);
    return result;
}

// 向量检索函数(封装模块二的检索逻辑)
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', 'book_name', 'chapter_num', 'index', 'content']
        });
        return searchResult.results
    } catch(err) {
        console.log('向量搜索失败:', err.message);
        return [];
    }
}

// 智能回答函数(核心:检索+生成)
async function answerEbookQuestion(question, k=3) {
    try {
        console.log('开始回答问题:', question);
        // 1. 检索相关内容
        const retrievedContent = await retrieveRelevantContent(question, k);
        console.log(`[检索到的内容]`, retrievedContent);

        // 若未检索到相关内容,返回提示
        if(retrievedContent.length === 0){
            console.log('没有检索到相关内容');
            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. 调用ChatOpenAI模型,生成回答
        console.log('AI助手的回答:');
        const response = await model.invoke(prompt);
        console.log(response.content);
        return response.content;
    } catch(err) {
        return '抱歉:处理您的问题时出现了错误,请稍后重试。';
    }
}

// 主函数:执行智能问答流程
async function main() {
    try {
        console.log('Connection to Milvus...');
        await client.connectPromise; // 连接Milvus

        // 加载集合(若未加载)
        try {
            await client.loadCollection({ collection_name: COLLECTION_NAME });
            console.log('Collection already loaded');
        } catch(err) {
            console.log('Collection not loaded');
        }

        // 执行智能问答
        const result = await answerEbookQuestion("段誉会什么武功?");
        console.log('回答:', result);
    } catch(err) {
        console.error('Connection to Milvus failed:', err.message);
    }
}

main();

3.3.2 核心亮点与关键步骤

该模块是整个RAG工程的核心,相比前两个模块,新增了“上下文格式化”和“Prompt工程”两个关键环节,确保生成的回答准确、贴合原文,具体解析如下:

(1)上下文格式化

将检索到的多个文本片段,按照“片段序号+章节号+内容”的格式整理,用分隔符(----)区分不同片段,让大语言模型能够清晰识别每个片段的来源和内容,便于综合多个片段的信息生成完整回答。例如:

[片段1]
章节:第5章
内容:段誉无意间习得凌波微步,这门武功是逍遥派的独门轻功,踏雪无痕,闪避能力极强,同时还能配合北冥神功吸收他人内力。

----

[片段2]
章节:第8章
内容:段誉在无量山洞中,意外发现了北冥神功的秘籍,习得后可吸收他人内力化为己用,解决了自身内力不足的问题。
(2)Prompt工程

Prompt(提示词)的设计直接影响模型生成回答的质量,本次案例的Prompt包含3个核心部分,确保回答的准确性和针对性:

  • 角色设定:明确模型的角色是“专业的《天龙八部》小说助手”,引导模型聚焦于小说内容,避免偏离主题。
  • 上下文输入:将格式化后的检索片段作为上下文,明确告知模型“基于这些片段回答问题”,确保回答的依据是检索到的知识库内容。
  • 回答要求:明确5条规则,例如“结合原文”“综合多个片段”“如实告知无相关信息”等,避免模型生成虚假信息或偏离原文的内容。
(3)示例输出(模拟)
开始回答问题: 段誉会什么武功?
[检索到的内容] [ {...}, {...} ]
AI助手的回答:
段誉主要会两门核心武功,分别是凌波微步和北冥神功:
1. 凌波微步:逍遥派的独门轻功,段誉无意间习得,特点是踏雪无痕、闪避能力极强,还可配合北冥神功吸收他人内力(第5章内容)。
2. 北冥神功:段誉在无量山洞中意外发现秘籍并习得,这门武功可吸收他人内力化为己用,解决了他自身内力不足的问题(第8章内容)。

四、实操注意事项与常见问题解决

在实际运行代码、搭建RAG电子书系统的过程中,会遇到各种环境配置、代码逻辑、依赖安装等问题,结合本次学习过程中的实践经验,整理以下注意事项和常见问题解决方案,便于后续实操落地。

4.1 环境配置注意事项

  1. Milvus环境配置

    1. 确保Milvus服务已启动,地址(ADDRESS)和令牌(TOKEN)正确,可通过Milvus Web UI验证连接是否正常。
    2. Milvus版本需与SDK版本匹配(本次使用@zilliz/milvus2-sdk-node,对应Milvus 2.x版本),避免版本不兼容导致API调用失败。
  2. OpenAI环境配置

    1. 确保OpenAI API密钥(OPENAI_API_KEY)有效,若使用代理,需正确配置baseURL(OPENAI_BASE_URL)。
    2. 嵌入模型(EMBEDDING_MODEL_NAME)推荐使用text-embedding-ada-002,对话模型(MODEL_NAME)推荐使用gpt-3.5-turbo,兼顾性能和成本。
  3. Node.js环境配置

    1. 推荐使用Node.js 16.x及以上版本,避免低版本不支持ES6语法和async/await。
    2. 安装依赖时,确保所有依赖包版本兼容,可通过package.json锁定版本,避免因依赖更新导致代码报错。

4.2 常见问题及解决方案

常见问题解决方案
EPubLoader加载失败,提示“文件不存在”1. 检查EPUB_FILE路径是否正确,确保文件存在;2. 若文件路径包含中文,确认Node.js环境支持中文路径;3. 更换EPUB文件,排除文件损坏问题。
向量插入失败,提示“向量维度不匹配”1. 检查OpenAI Embeddings的dimensions与Milvus集合中vector字段的dim是否一致;2. 确认索引创建时的nlist与向量维度一致;3. 重新初始化embeddings和Milvus集合。
Milvus检索返回空结果1. 检查集合是否已加载(调用loadCollection);2. 确认插入的数据是否成功(查看insert_cnt);3. 检查查询问题与知识库内容是否相关,调整limit参数或优化查询语句。
OpenAI API调用失败,提示“API key invalid”1. 检查OPENAI_API_KEY是否正确,是否过期;2. 确认API密钥的权限(是否支持embedding和chat模型);3. 检查baseURL是否正确,若使用代理,确保代理可正常访问。
文本分割后片段过长或过短调整chunkSize和chunkOverlap参数,根据EPUB章节内容长度调整,建议chunkSize在300-800字符之间,chunkOverlap为chunkSize的10%-20%。

4.3 性能优化建议

针对本次RAG电子书系统,可从以下3个方面进行性能优化,提升处理效率和检索体验:

  1. 向量生成优化:批量生成向量时,可限制并发数量,避免因并发过高导致OpenAI API限流;对于大量文本片段,可分批次处理,加入重试机制,避免单次失败导致整个流程中断。
  2. 检索优化:若数据量较大,可更换Milvus索引类型(如HNSW),提升检索速度;调整limit参数,根据实际需求返回合适数量的片段,平衡检索速度和回答质量。
  3. 代码优化:将重复的代码(如embeddings初始化、Milvus客户端初始化)封装为工具函数,减少代码冗余;加入日志记录,便于排查问题;对敏感信息(如API密钥)进行加密处理,提升安全性。

五、学习总结与拓展思考

5.1 学习总结

通过本次对RAG电子书实现代码的学习,系统掌握了RAG技术的核心流程、关键组件及工程化实现细节,具体收获如下:

  • 理论层面:理解了RAG技术“检索+生成”的核心逻辑,掌握了知识库构建、向量嵌入、向量检索、智能生成的完整流程,明确了各环节的核心作用和技术原理。
  • 技术层面:熟练掌握了LangChain(Loader、Splitter、Embeddings、ChatOpenAI)、Milvus(集合管理、向量插入、检索)、Node.js(环境配置、代码编写)的使用方法,能够独立搭建简单的RAG系统。
  • 实操层面:学会了排查RAG系统搭建过程中的常见问题,掌握了环境配置、代码优化的基本方法,能够将RAG技术应用到具体场景(如电子书智能问答)。

本次案例的核心亮点的是“端到端”实现RAG技术,从原始电子书解析到最终的智能问答,每个环节都有完整的代码实现,流程清晰、逻辑严谨,适合作为RAG技术入门的实践案例。

5.2 拓展思考

基于本次学习内容,可从以下几个方面进行拓展,进一步提升RAG系统的功能和性能,拓展应用场景:

  1. 多格式电子书支持:目前仅支持EPUB格式,可扩展支持PDF、TXT、MOBI等多种格式的电子书,通过LangChain的其他Loader(如PDFLoader、TextLoader)实现多数据源加载。
  2. 多书籍知识库管理:目前仅支持单本书籍(《天龙八部》),可优化集合设计,通过book_id区分不同书籍,实现多书籍的知识库管理,支持用户切换书籍进行问答。
  3. 前端界面开发:目前仅支持命令行运行,可开发前端界面(如React、Vue),提供电子书上传、问题查询、回答展示等功能,提升用户体验。
  4. 模型优化:可替换OpenAI模型为开源模型(如Llama 2、ChatGLM),降低API调用成本;优化Prompt设计,提升回答的准确性和丰富性;加入上下文记忆功能,支持多轮对话。