ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

用 Node.js 搭建 EPUB 问答助手:从文本切片、向量检索到 RAG

用 Node.js 搭建 EPUB 问答助手:从文本切片、向量检索到 RAG 1. 引言电子书EPUB是许多人阅读和学习的重要载体但面对动辄几百页的书籍想快速找到某个问题的答案并不容易。传统的关键词搜索只能做字面匹配无法理解语义而把整本书丢给大模型直接提问又会超出上下文窗口限制回答质量也难以保证。RAGRetrieval-Augmented Generation检索增强生成正是解决这一问题的经典方案先把书籍内容切片并向量化存入数据库提问时先检索出最相关的片段再把这些片段作为上下文交给大模型生成答案。本文将用 Node.js 从零搭建一个 EPUB 问答助手完整走通「文本切片 → 向量检索 → RAG 生成」的流程。2. 整体架构在动手写代码之前先明确整个系统的组成部分。一个完整的 EPUB 问答助手包含以下模块EPUB 解析读取.epub文件提取其中的 HTML 内容并转为纯文本。文本切片将长文本按章节或固定长度切分成小块便于向量化和检索。向量化将每个文本块转换为向量Embedding存入向量数据库。向量检索用户提问时将问题向量化并在库中检索最相似的文本块。RAG 生成把检索到的文本块拼入 Prompt交给大模型生成答案。整体流程如下图所示EPUB 文件解析提取文本文本切片向量化 Embedding存入向量数据库用户提问问题向量化向量检索 TopK拼接 Prompt大模型生成答案返回回答3. 技术选型本项目采用以下技术栈均为 Node.js 生态中成熟且易用的方案模块选型说明EPUB 解析epub2轻量解析 EPUB 文件提取章节 HTMLHTML 转文本html-to-text将 HTML 转为纯文本向量数据库sqlite-vec基于 SQLite 的向量扩展零部署成本向量化OpenAI Embedding API使用text-embedding-3-small模型大模型OpenAI Chat Completions使用gpt-4o-mini生成回答开发语言Node.js 20使用 ES Module 语法选择sqlite-vec而非专门的向量数据库如 Milvus、Pinecone是为了让示例更轻量——无需额外部署服务一个本地文件即可完成全部存储与检索非常适合个人工具和教学场景。4. 项目初始化首先创建项目并安装依赖mkdirepub-qa-assistantcdepub-qa-assistantnpminit-ynpminstallepub2 html-to-text sqlite-vec openai在package.json中确认type: module已设置以便使用 ES Module 语法{name:epub-qa-assistant,type:module,scripts:{index:node src/index.js,query:node src/query.js}}项目目录结构如下epub-qa-assistant/ ├── src/ │ ├── extract.js # EPUB 解析与文本提取 │ ├── chunk.js # 文本切片 │ ├── embed.js # 向量化 │ ├── store.js # 向量存储与检索 │ ├── index.js # 建库入口 │ └── query.js # 问答入口 ├── books/ # 存放 EPUB 文件 └── data/ # 存放向量数据库文件5. EPUB 解析与文本提取EPUB 文件本质上是 ZIP 压缩包内部包含多个 XHTML 文件。epub2库可以帮我们解压并读取每个章节的内容。创建src/extract.jsimport{EPub}fromepub2;import{convert}fromhtml-to-text;exportasyncfunctionextractEpub(filePath){constepubawaitEPub.createAsync(filePath);constchapters[];for(constchapterofepub.flow){consthtmlawaitnewPromise((resolve,reject){epub.getChapter(chapter.id,(err,text){if(err)reject(err);elseresolve(text);});});consttextconvert(html,{wordwrap:false,selectors:[{selector:img,format:skip},{selector:script,format:skip},{selector:style,format:skip},],});chapters.push({title:chapter.title||Chapter${chapters.length1},text:text.trim(),});}returnchapters.filter((c)c.text.length0);}这里做了两件事一是遍历epub.flow获取所有章节的 HTML 内容二是用html-to-text把 HTML 转为纯文本同时跳过图片、脚本和样式标签避免噪声进入后续的向量化流程。6. 文本切片大模型的上下文窗口有限向量检索的粒度也需要控制。切片太大会稀释语义、浪费 token切片太小则可能截断完整语义。这里采用「按章节优先、超长再切」的策略章节文本不超过maxChunkSize时整章作为一个切片超过时按段落边界切分尽量保持语义完整。创建src/chunk.jsconstMAX_CHUNK_SIZE800;// 单块最大字符数constOVERLAP_SIZE100;// 相邻块重叠字符数exportfunctionchunkText(text,maxSizeMAX_CHUNK_SIZE,overlapOVERLAP_SIZE){if(text.lengthmaxSize){return[text];}constchunks[];letstart0;while(starttext.length){letendMath.min(startmaxSize,text.length);// 尽量在段落边界处断开if(endtext.length){constboundarytext.lastIndexOf(\n\n,end);if(boundarystartmaxSize*0.5){endboundary;}}chunks.push(text.slice(start,end).trim());startend-overlap;}returnchunks.filter((c)c.length0);}exportfunctionchunkChapters(chapters){constchunks[];for(constchapterofchapters){constpartschunkText(chapter.text);parts.forEach((part,i){chunks.push({id:${chunks.length},title:chapter.title,index:i,text:part,});});}returnchunks;}重叠overlap的设计很关键它让相邻切片之间保留一部分重复内容避免一个完整句子或概念恰好被切断在边界上从而提升检索召回率。7. 向量化与存储向量化是把文本变成高维数值数组的过程语义相近的文本在向量空间中距离更近。这里使用 OpenAI 的 Embedding API。创建src/embed.jsimportOpenAIfromopenai;constopenainewOpenAI();exportasyncfunctionembedTexts(texts){constresponseawaitopenai.embeddings.create({model:text-embedding-3-small,input:texts,});returnresponse.data.map((d)d.embedding);}text-embedding-3-small模型性价比高默认输出 1536 维向量足以支撑语义检索。批量传入文本可以显著减少 API 调用次数。接下来创建src/store.js使用sqlite-vec完成向量表的创建、写入和检索importsqlite3fromsqlite3;import{createRequire}frommodule;constrequirecreateRequire(import.meta.url);const{Vector}require(sqlite-vec);constDB_PATH./data/vectors.db;exportfunctioninitStore(){constdbnewsqlite3.Database(DB_PATH);db.loadExtension(Vector.getLoadablePath());db.exec(CREATE VIRTUAL TABLE IF NOT EXISTS chunks USING vec0( id INTEGER PRIMARY KEY, text TEXT, title TEXT, embedding FLOAT[1536] ););returndb;}exportfunctioninsertChunks(db,chunks,embeddings){conststmtdb.prepare(INSERT INTO chunks (id, text, title, embedding) VALUES (?, ?, ?, ?));chunks.forEach((chunk,i){stmt.run(chunk.id,chunk.text,chunk.title,JSON.stringify(embeddings[i]));});stmt.finalize();}exportasyncfunctionsearchChunks(db,queryEmbedding,topK5){constrowsawaitnewPromise((resolve,reject){db.all(SELECT id, text, title, distance FROM chunks WHERE embedding MATCH ? ORDER BY distance LIMIT ?,[JSON.stringify(queryEmbedding),topK],(err,rows)(err?reject(err):resolve(rows)));});returnrows;}sqlite-vec使用虚拟表vec0存储向量MATCH语法执行 KNN 检索distance越小表示越相似。8. 建库入口把以上模块串起来创建src/index.js作为建库入口import{extractEpub}from./extract.js;import{chunkChapters}from./chunk.js;import{embedTexts}from./embed.js;import{initStore,insertChunks}from./store.js;constEPUB_PATHprocess.argv[2]||./books/example.epub;asyncfunctionmain(){console.log( 解析 EPUB...);constchaptersawaitextractEpub(EPUB_PATH);console.log(共提取${chapters.length}个章节);console.log(✂️ 文本切片...);constchunkschunkChapters(chapters);console.log(共生成${chunks.length}个文本块);console.log( 向量化...);constembeddingsawaitembedTexts(chunks.map((c)c.text));console.log( 写入向量库...);constdbinitStore();insertChunks(db,chunks,embeddings);db.close();console.log(✅ 建库完成);}main().catch((err){console.error(建库失败:,err);process.exit(1);});运行建库命令nodesrc/index.js ./books/your-book.epub9. 问答查询RAG 生成建库完成后就可以实现问答了。查询流程分为三步问题向量化 → 向量检索 → 拼接 Prompt 交给大模型。创建src/query.jsimportOpenAIfromopenai;import{embedTexts}from./embed.js;import{initStore,searchChunks}from./store.js;constopenainewOpenAI();constTOP_K5;asyncfunctionanswer(question){// 1. 问题向量化const[queryEmbedding]awaitembedTexts([question]);// 2. 向量检索constdbinitStore();constresultsawaitsearchChunks(db,queryEmbedding,TOP_K);db.close();if(results.length0){return抱歉在书中没有找到相关内容。;}// 3. 拼接上下文constcontextresults.map((r)【${r.title}】\n${r.text}).join(\n\n---\n\n);// 4. 交给大模型生成constresponseawaitopenai.chat.completions.create({model:gpt-4o-mini,messages:[{role:system,content:你是一个严谨的阅读助手。请仅根据提供的书籍片段回答问题如果片段中没有答案请明确说明。引用内容时标注来源章节。,},{role:user,content:书籍片段如下\n\n${context}\n\n问题${question},},],});returnresponse.choices[0].message.content;}constquestionprocess.argv[2]||这本书主要讲了什么;answer(question).then((res){console.log(❓${question}\n\n${res});});运行问答nodesrc/query.js作者对时间管理提出了哪些建议10. 效果与优化方向这套方案在几百页的 EPUB 上通常能取得不错的效果检索阶段能准确召回与问题语义相关的片段生成阶段则基于这些片段给出有出处的回答有效缓解了大模型的幻觉问题。如果希望进一步提升效果可以从以下几个方向优化切片策略根据书籍类型调整块大小与重叠度对代码类书籍可按代码块边界切分。混合检索在向量检索之外叠加关键词 BM25 检索再做结果融合兼顾语义与字面匹配。重排序Rerank对 TopK 结果用交叉编码器重排把最相关的片段排到最前。增量更新监听书籍目录新增章节时只对增量部分做向量化入库。多轮对话把历史对话也拼入 Prompt支持追问与上下文理解。11. 总结本文从零搭建了一个基于 Node.js 的 EPUB 问答助手完整覆盖了 RAG 的核心链路EPUB 解析提取文本、按章节与段落切片、Embedding 向量化、sqlite-vec存储与检索最后把检索结果交给大模型生成带出处的回答。整套代码轻量、可本地运行适合作为学习 RAG 的起点也可以在此基础上扩展成更完整的个人知识库问答工具。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表