你有没有想过,让AI陪你重温一部百万字的小说?想象一下,你可以随时向AI提问:"方源为什么要炼制定仙游?"或者"白凝冰的最终结局是什么?"——AI不仅能够准确回答,还能引用小说原文作为佐证。

这不是科幻小说,而是我最近完成的一个小项目:基于RAG(Retrieval-Augmented Generation,检索增强生成)技术的智能问答系统。今天,我想和你分享这个从零到一的实战过程,让《蛊真人》这部经典网络小说遇上了现代AI技术。
在深入了解代码之前,让我们先理解RAG的核心思想。
传统的大语言模型(LLM)虽然强大,但它们有天然的局限:知识截止日期固定,无法获取最新信息;训练数据有限,对于特定领域的专业知识了解不足;而且容易产生"幻觉",即编造不存在的事实。
RAG技术正是为解决这些问题而生。它的工作流程简单而优雅:
这种"检索+生成"的模式,让AI不再凭空想象,而是基于真实文档回答问题。就像给AI配备了一个可以随时查阅的图书馆。
在开始编码之前,我需要选择合适的工具链。经过调研,我确定了以下技术栈:
这些工具的组合,构成了一个完整的RAG流水线。
项目的第一步是配置环境。我使用.env文件管理敏感信息:
MILVUS_ADDRESS=your_milvus_addressMILVUS_TOKEN=your_milvus_tokenOPENAI_API_KEY=your_api_keyOPENAI_BASE_URL=your_base_urlEMBEDDING_MODEL_NAME=text-embedding-3-smallMODEL_NAME=deepseek-chat
数据准备阶段,我下载了《蛊真人》的纯文本版本。这是一部约800万字的长篇小说,恰好适合测试RAG系统的性能。
知识库构建是整个系统的基石。我设计了以下流程:
小说有其天然的结构——章节。我实现了一个splitByChapters函数,使用正则表达式匹配"第一节"、"第二节"等标记:
function splitByChapters(text) {const chapterRegex = /第[一二三四五六七八九十百千万]+节/g;// 查找所有章节标题,按位置分割文本// 返回包含章节内容和元数据的数组}
这样做的好处是保持了小说的叙事完整性,每个片段都包含完整的故事情节。
对于长文档,需要进一步分割成适合向量化的小块。我使用RecursiveCharacterTextSplitter,设置块大小为500字符,重叠50字符:
const textSplitter = new RecursiveCharacterTextSplitter({chunkSize: 500,chunkOverlap: 50,});
重叠部分确保上下文不丢失,就像两个相邻的拼图块有重叠的边缘。
对每个文本块,我调用OpenAI的Embedding API将其转换为1024维的向量:
async function getEmbedding(text) {const result = await embeddings.embedQuery(text);return result;}
然后,将这些向量连同元数据(章节号、片段索引、书籍名称等)一起存入Milvus:
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}}))
为了提升性能,我使用Promise.all并发处理向量生成,大大加快了插入速度。
在Milvus中,数据以"集合"(Collection)的形式组织。我设计了一个ensureBookCollection函数,负责集合的创建、索引构建和加载:
async function ensureBookCollection(bookId) {// 检查集合是否存在const hasCollection = await client.hasCollection({collection_name: COLLECTION_NAME,});// 如果不存在,创建集合并定义schema// 字段包括:id, book_id, book_name, chapter_num, index, content, vector// 创建向量索引,使用IVF_FLAT算法和COSINE相似度await client.createIndex({collection_name: COLLECTION_NAME,field_name: 'vector',index_type: IndexType.IVF_FLAT,metric_type: MetricType.COSINE,});// 加载集合到内存await client.loadCollection({collection_name: COLLECTION_NAME});}
索引类型选择IVF_FLAT,这是一种高效的近似最近邻搜索算法,在海量数据中也能快速检索。
传统搜索依赖关键词匹配,而语义搜索能理解问题的真实含义。当用户问"贾金生是谁杀的?"时,系统会:
const searchResult = await client.search({collection_name: COLLECTION_NAME,vector: queryVector,limit: 3,metric_type: MetricType.COSINE,output_fields: ['id', 'content', 'book_id', 'chapter_num', 'index', 'book_name'],})
搜索结果按相似度得分排序,得分越高表示相关性越强。
最后,我将检索到的相关内容作为上下文,构建Prompt送给LLM:
const prompt = `你是一个专业的《蛊真人》小说助手。基于小说内容回答问题...请根据以下小说片段内容回答问题:${context}用户问题:${question}回答要求:1. 如果有相关信息,给出详细准确的回答2. 可以综合多个片段的内容3. 如果没有相关信息,如实告知4. 可以引用原文内容AI助手的回答:`
这个Prompt设计有几个关键点:
在实际开发中,我遇到了一些性能瓶颈,并做了针对性优化:
最初,每个文本块单独调用Embedding API,处理800万字耗时数小时。改为Promise.all并发后,速度提升了近10倍。
选择合适的索引类型和参数至关重要。对于1024维的向量,我设置nlist=1024,在搜索精度和速度之间取得平衡。
按章节处理而不是一次性加载全部文本,既节约内存,又便于错误恢复。
经过完整的流程,我的《蛊真人》问答系统可以回答各种问题:
通过这个项目,我深刻体会到RAG技术的魅力:
当然,也有一些值得改进的地方:
从一部网络小说出发,我构建了一个完整的RAG智能问答系统。这个过程让我深刻理解了RAG的每个环节,也看到了它的广阔应用前景。
无论是企业知识库、智能客服,还是教育辅导系统,RAG都能让AI真正"懂"你的数据,为用户提供准确、可信的智能服务。
正如《蛊真人》中方源的修炼之路一样,RAG技术的探索也是一条充满挑战和机遇的道路。希望这篇文章能为你开启这扇大门,让你也体验当AI遇上知识的美妙时刻。