前言
直接用大模型问《天龙八部》武功、人物,经常乱编剧情、混淆人物,这是通用LLM的天生短板------训练数据有截止时间,没有私有文本记忆。
今天用Node.js完整落地一套端到端RAG知识库,把EPUB武侠小说存入Milvus向量库,实现基于原著片段的精准问答,彻底解决AI幻觉。
读完你能收获:
- 完整RAG四大核心模块实操:文档加载、文本分块、Embedding向量化、Milvus向量检索
- 可直接复制运行的4套工程代码:入库、单独检索、完整问答、基础配置
- Milvus建表、IVF_FLAT索引、余弦相似度调优实战经验
- RAG工程高频踩坑点与解决方案
- 可迁移到企业文档、知识库、产品手册的通用流程

一、整体技术栈与RAG流程拆解
1.1 用到的核心依赖
- LangChain :统一封装文档加载、文本分割、Embedding、LLM调用
EPubLoader:解析epub电子书,按章节拆分文档RecursiveCharacterTextSplitter:文本智能分块OpenAIEmbeddings:文本转1024维向量ChatOpenAI:对话大模型生成答案
- @zilliz/milvus2-sdk-node:Milvus向量数据库Node客户端,存储&语义检索
- dotenv:统一管理API、Milvus地址密钥
1.2 RAG完整流水线(四大环节)
scss
EPUB电子书 → Loader加载分章节 → Splitter切片(chunk) → Embedding生成向量 → Milvus入库存储
用户提问 → 问题向量化 → Milvus余弦相似度TopK检索 → 拼接原文片段上下文 → Prompt注入LLM → 输出原著可信答案
四大核心模块详解:
- Loader文档加载器:支持epub/csv/txt/pdf等多格式,本次用EPubLoader自动拆分小说章节,保留章节元数据,方便溯源
- Splitter文本分割器:设置chunk_size、chunk_overlap,避免上下文断裂,是检索准确率关键
- Embedding向量模型:输出1024维稠密向量,把文字语义转为数字,用于相似度匹配
- Milvus向量数据库:存储文本+元数据+向量,提供高速Cosine余弦检索,支持百万级文本检索
二、环境准备与配置文件
2.1 安装依赖
bash
npm install dotenv @zilliz/milvus2-sdk-node @langchain/openai @langchain/community @langchain/textsplitters
2.2 .env环境配置
env
# Milvus向量库配置
MILVUS_ADDRESS=你的Milvus服务地址
MILVUS_TOKEN=你的Milvus密钥
# 大模型&向量嵌入配置
OPENAI_API_KEY=你的APIkey
OPENAI_BASE_URL=代理地址
EMBEDDINGS_MODEL_NAME=text-embedding-xxx
MODEL_NAME=gpt-3.5-turbo
三、第一步:EPUB电子书入库Milvus(main.js完整代码)
该脚本实现:连接Milvus → 自动创建集合&索引 → 加载天龙八部epub → 按章节切割文本 → 批量生成向量入库。
javascript
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 CHUNK_OVERLAP = 50;
const EPUB_FILE = './天龙八部.epub'
const ADDRESS = process.env.MILVUS_ADDRESS;
const TOKEN = process.env.MILVUS_TOKEN;
const { name: BOOK_NAME } = parse(EPUB_FILE);
// 初始化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
});
// Milvus客户端
const client = new MilvusClient({ address: ADDRESS, token: TOKEN });
// 生成向量工具函数
async function getEmbedding(text) {
return await embeddings.embedQuery(text);
}
/**
* 校验/创建Milvus集合,定义表结构+索引
*/
async function ensureCollection(bookId) {
const hasCollection = await client.hasCollection({ collection_name: COLLECTION_NAME });
if(!hasCollection.value) {
console.log('创建ebook2集合....');
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('集合创建成功,开始构建IVF_FLAT索引');
// IVF_FLAT索引,余弦相似度,nlist=1024适配百万级文本
await client.createIndex({
collection_name: COLLECTION_NAME,
field_name: 'vector',
index_type: IndexType.IVF_FLAT,
metric_type: MetricType.COSINE,
params: {nlist: 1024}
})
console.log('索引创建完成');
}
// 加载集合至内存
try {
await client.loadCollection({ collection_name: COLLECTION_NAME });
console.log('集合加载成功');
} catch(err) {
console.log('集合已处于加载状态,无需重复加载');
}
}
/**
* 批量切片向量化,插入Milvus
*/
async function insertChunksBatch(chunks, bookId, chapterNum) {
const EMBED_BATCH_SIZE = 10; // 接口单次批量上限
const vectors = [];
for (let i = 0; i < chunks.length; i += EMBED_BATCH_SIZE) {
const batch = chunks.slice(i, i + EMBED_BATCH_SIZE);
const batchVectors = await embeddings.embedDocuments(batch);
vectors.push(...batchVectors);
}
// 组装入库数据,携带书籍、章节、切片索引元数据
const data = chunks.map((content, index) => ({
id: `${bookId}_${chapterNum}_${index}`,
book_id: String(bookId),
book_name: BOOK_NAME,
chapter_num: chapterNum,
index,
content,
vector: vectors[index]
}));
await client.insert({ collection_name: COLLECTION_NAME, data });
return data.length;
}
/**
* 核心:加载EPUB、分章节、文本切片、批量入库
*/
async function loadAndProcessEPubStreaming(bookId) {
console.log(`开始加载EPUB文件: ${EPUB_FILE}`);
// splitChapters=true 自动按章节拆分文档,保留章节结构
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: CHUNK_OVERLAP
});
let totalInserted = 0;
for (let chapterIndex = 0; chapterIndex < documents.length; chapterIndex++) {
const document = documents[chapterIndex];
const chapterContent = document.pageContent;
console.log(`正在处理第${chapterIndex+1}/${documents.length}章`);
const chunks = await textSplitter.splitText(chapterContent);
console.log(`本章切分${chunks.length}个文本切片`);
if(chunks.length === 0) continue;
const insertedCount = await insertChunksBatch(chunks, bookId, chapterIndex+1);
totalInserted += insertedCount;
console.log(`累计入库切片:${totalInserted}`);
}
console.log(`全部处理完成,总入库切片${totalInserted}条`);
return totalInserted;
}
// 程序入口
const main = async () => {
console.log('==========电子书向量入库程序启动==========');
await client.connectPromise;
console.log('Milvus连接成功');
const bookId = 1;
await ensureCollection(bookId);
await loadAndProcessEPubStreaming(bookId);
}
main().catch(err => console.error('入库程序异常:',err))
入库关键参数说明
chunkSize:500:单块文本500字符,平衡检索精度与向量接口消耗chunkOverlap:50:切片重叠50字符,避免章节交界处上下文丢失MetricType.COSINE:余弦相似度,文本语义匹配最优算法IVF_FLAT:轻量索引,百万级文本检索速度快,无需高性能硬件
四、第二步:单独向量检索测试(query.js)
入库完成后,可单独运行检索脚本,验证向量匹配是否正常,用于调试相似度得分。
javascript
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 embedding = new OpenAIEmbeddings({
apiKey: process.env.OPENAI_API_KEY,
model: process.env.EMBEDDINGS_MODEL_NAME,
configuration: { baseURL: process.env.OPENAI_BASE_URL },
dimension: VECTOR_DIM,
})
const client =new MilvusClient({ address :ADDRESS , token : TOKEN })
const getEmbedding = async (text) => await embedding.embedQuery(text);
async function main(){
console.log('正在连接Milvus...')
await client.connectPromise;
console.log("连接成功")
// 测试提问:段誉会什么武功
const query = "段誉会什么武功"
const queryVector=await getEmbedding(query);
// Top3相似度片段召回
const searchResult= await client.search({
collection_name: COLLECTION_NAME,
vectors: [queryVector],
limit: 3,
metric_type: MetricType.COSINE,
output_fields: ['id', 'book_id', 'chapter_num', 'index', 'content'],
})
// 打印检索结果、相似度得分、章节号、原文
searchResult.results.forEach((item,index)=>{
console.log(`
=====检索片段${index+1}=====
相似度得分:${item.score.toFixed(4)}
章节号:${item.chapter_num}
切片ID:${item.id}
原文片段:${item.content}
`)
})
}
main().catch(console.error)
五、第三步:完整RAG问答链路(rag.js)
核心业务代码:检索原文片段 + 构造约束Prompt + 调用LLM生成基于原著的答案,解决AI幻觉。
javascript
import 'dotenv/config'
import { MilvusClient, MetricType } from '@zilliz/milvus2-sdk-node'
import { OpenAIEmbeddings, ChatOpenAI } from '@langchain/openai'
import { SystemMessage } from '@langchain/core/messages'
const ADDRESS =process.env.MILVUS_ADDRESS
const TOKEN=process.env.MILVUS_TOKEN
const COLLECTION_NAME = 'ebook2';
const VECTOR_DIM=1024;
// 向量模型初始化
const embedding = new OpenAIEmbeddings({
apiKey: process.env.OPENAI_API_KEY,
model: process.env.EMBEDDINGS_MODEL_NAME,
configuration: { baseURL: process.env.OPENAI_BASE_URL },
dimension: VECTOR_DIM,
})
// 对话大模型
const model = new ChatOpenAI({
temperature: 0.7,
model: process.env.MODEL_NAME,
apiKey: process.env.OPENAI_API_KEY,
configuration: { baseURL: process.env.OPENAI_BASE_URL },
})
// Milvus客户端
const client =new MilvusClient({ address :ADDRESS , token : TOKEN })
const getEmbedding = async (text) => await embedding.embedQuery(text);
/**
* 向量检索:召回TopK相关小说片段
*/
const retrievevRelevantContent = async (question,k=3) => {
try{
const queryVector =await getEmbedding(question)
const srarchResult = await client.search({
collection_name: COLLECTION_NAME,
vectors: [queryVector],
limit: k,
metric_type: MetricType.COSINE,
output_fields: ['id','content',"book_id","chapter_num"],
})
return srarchResult.results
}catch(error){
console.error('检索异常:', error);
}
}
/**
* 组装RAG提示词,强约束模型只能基于原文回答
*/
const answerEbookQuestion = async (question,k=3) => {
try{
console.log('='.repeat(50))
console.log(`用户问题:${question}`);
console.log('='.repeat(50))
const retrievedDiaries = await retrievevRelevantContent(question,k)
if(!retrievedDiaries || retrievedDiaries.length===0){
return "知识库中未找到《天龙八部》相关内容,无法回答";
}
// 打印召回的原文片段与相似度
retrievedDiaries.forEach((diary,index)=>{
console.log(`检索片段${index+1} | 相似度:${diary.score.toFixed(4)}\n章节${diary.chapter_num}原文:${diary.content}\n`)
})
// 拼接上下文,携带章节溯源信息
const context = retrievedDiaries.map((diary,i)=>`[片段${i+1}] 章节号:${diary.chapter_num},原文内容:${diary.content}`).join('\n\n----\n\n')
// 强约束Prompt,杜绝编造内容
const prompt =`你是专业《天龙八部》原著问答助手,所有回答必须严格依据下方提供的小说原文片段。
参考原文片段:
${context}
用户提问:${question}
回答硬性要求:
1. 仅使用片段内存在的信息作答,无相关内容直接回复"书中暂无相关记载";
2. 可整合多段原文综合回答,支持引用原著文字佐证;
3. 严格贴合小说人物、武功、剧情设定,禁止编造不存在的情节;
4. 回答条理清晰,分点说明,标注对应章节来源。
`
return prompt
}catch(error){
console.error('构造prompt异常:', error);
}
}
// 程序入口,完整问答链路
async function main(){
try{
await client.connect()
await client.loadCollection({ collection_name: COLLECTION_NAME })
console.log("向量库集合加载完成,启动RAG问答");
// 示例提问:鸠摩智会什么武功?
const prompt=await answerEbookQuestion("鸠摩智会什么武功?",5)
const response=await model.invoke([new SystemMessage(prompt)])
console.log('\n=====AI最终回答=====')
console.log(response.content)
}catch(error){
console.error('RAG主程序异常:', error);
}
}
main().catch(console.error)
Prompt设计核心亮点
通过硬性规则限制大模型输出,从根源解决幻觉:
- 无匹配原文直接告知"无相关记载",禁止脑补;
- 强制绑定检索到的章节片段,答案可溯源;
- 要求引用原文,降低编造概率。
六、实战高频踩坑汇总(避坑手册)
6.1 向量维度不匹配(最高频报错)
- 问题:Embedding模型输出维度1024,Milvus建表时dim写错,检索直接报错
- 解决方案:全局统一
VECTOR_DIM=1024常量,建表、Embedding初始化全部复用该变量
6.2 文本切片过大/过小
- chunkSize过大:单段内容杂乱,语义不聚焦,检索匹配准确率暴跌
- chunkSize过小:丢失完整上下文,人物、剧情断裂
- 最优配置:500字符chunk + 50字符重叠,小说/文档通用
6.3 Milvus未加载集合,检索空数据
创建集合后必须执行loadCollection,否则向量库无法读取索引,检索返回空结果。
6.4 Embedding批量超限
多数Embedding接口单次批量上限10条,代码中设置EMBED_BATCH_SIZE=10分批处理,避免接口400报错。
6.5 相似度得分过低,召回无关片段
- 检查是否使用
MetricType.COSINE,文本场景禁用L2距离; - 增大检索limit值(5-10),过滤低相似度片段;
- 调整chunkSize,让文本语义更完整。
6.6 LLM依旧编造内容
Prompt缺少强约束规则,必须增加"无原文则如实告知,禁止编造"硬性条款。
七、流程拓展:不止武侠小说,通用企业知识库
这套RAG架构可无缝迁移到各类私有文档场景:
- 企业内部规章制度、培训手册(EPUB/PDF/TXT)
- 产品说明书、售后知识库
- 项目技术文档、接口手册
- 历史资料、合同档案
仅需替换两点:
EPubLoader更换为PDFLoader/TextLoader等对应加载器;- 调整Prompt角色,从"小说助手"改为"企业文档顾问"。
八、完整总结
本文完整落地一套Node.js+Milvus+LangChain工业级简易RAG系统,覆盖全链路:
- 数据入库链路:EPUB加载 → 章节拆分 → 文本切片 → 批量向量化 → Milvus建表建索引存储;
- 问答检索链路:问题向量化 → 余弦相似度TopK召回原文 → 拼接上下文约束Prompt → LLM生成可信答案;
- 掌握三大核心知识点:文档分块调优、向量数据库Schema设计、RAG提示词幻觉抑制方案。