Milvus + RAG 实战:《红楼梦》问答助手

用 Node.js + Milvus + 大模型,搭一个基于《红楼梦》小说的问答助手。

什么是 RAG ?

RAG 是 Retrieval-Augmented Generation(检索增强生成) ,是一种将大语言模型(LLM)与外部知识检索结合起来的 AI 技术。它的核心思想是:

先查资料,再回答问题。

这样模型不仅依赖训练时学到的知识,还可以利用最新或企业私有的数据。

为什么需要 RAG?

目的只有一个:让大模型回答地更准确。

因为大语言模型的知识可能过时、不了解私有数据,容易产生幻觉。

所以需要 RAG ,让大语言模型基于最新的知识或私有数据进行回答,解决大语言模型的幻觉问题。

所谓幻觉,就是大语言模型不知道答案时,可能不会说"不知道",而是一本正经地编一个答案。

RAG 的工作流程

可以把 RAG 理解成检索、增强和生成 3 个步骤:

  1. 检索:根据用户提问,在向量数据库中检索找到最相关的文档。

  2. 增强:将用户问题 + 检索结果一起组成 Prompt

  3. 生成:将 Prompt 传给 LLM,并得到答案。

例如:有一个由《员工手册》切分成的向量数据库

用户提问:公司年假是多少天?

RAG 会:

  1. 在员工手册中找到"年假政策"

  2. 把相关几段内容发给 LLM

  3. LLM 根据这些内容生成答案

这肯定比大模型仅根据训练数据来回答准确。

如何构建 RAG ?

构建一个 RAG 系统可以分为 7 步:

  1. 准备知识库:收集 PDF、Word、Markdown、网页等资料。

  2. 文本切分:把长文档切成一个个较小的文本片段(Chunk)。

  3. 文本向量化:使用 Embedding 模型把每个 Chunk 转换成向量。

  4. 存入向量数据库:将文本和对应的向量保存到 Milvus、Chroma 等向量数据库中。

  5. 用户提问:用户输入一个问题,并使用同一个 Embedding 模型将问题转换成向量。

  6. 相似度检索:在向量数据库中搜索与问题最相关的若干文本片段。

  7. 交给大语言模型生成答案:将 用户问题 + 检索到的文本 一起发送给 LLM,由 LLM 根据这些资料生成最终答案。

上述 7 步整体上分为两个阶段:

  1. 知识库构建阶段:1 ~ 4

  2. 用户回答阶段:5 ~ 7

什么是 Milvus ?

Milvus 是一个开源的向量数据库,专门用于存储、检索和管理向量数据,是构建 RAG、AI 搜索、推荐系统等 AI 应用的重要基础设施。

Milvus 针对海量向量数据进行了索引和检索优化,可以快速找到相似向量,支持从百万级到十亿级甚至更大规模的向量数据。适合用于构建企业级知识库。

具体实战

下面来具体实现《红楼梦》问答助手。

具体的技术栈如下:

  • Milvus 向量数据库:存文本 + 搜文本

  • 阿里云 text-embedding-v4:emmbedding 模型 ,用于给给文本"打标签"(向量化)

  • 阿里云 qwen-plus 大模型:大语言模型,用于回答用户问题

  • Express:Node.js 后端框架

  • Vue 3 + Element Plus:实现前端界面

  • SSE 流式传输:让答案逐字出现,实现"打字机"效果,减少用户等待时间

如上所述,整个系统分为两步:知识库构建阶段用户回答阶段

知识库构建阶段:把《红楼梦》存进数据库

具体步骤为:

  1. 把《红楼梦》全文切成小块,每块 1000 字

  2. 用 Embedding 模型把每块变成一串数字(向量)

  3. 把每块的向量和内容存入 Milvus

为什么要把文本变成向量?因为计算机不认识中文,但能算两串数字有多"像"。两段意思相近的文字,它们的向量也相近。

把《红楼梦》全文切成小块,每块 1000 (chunkSize 大小)字,这里的 1000 是上限,不是每块都是 1000 字。

chunkSize 太大或太小都不好,这里的 1000 是经验值。

具体的切分策略是:尽量在句号、逗号等自然边界切断,只要不超过 1000 字就行

typescript 复制代码
// 把 86 万字切成 1000 字一块,块之间重叠 200 字
const splitter = new RecursiveCharacterTextSplitter({
  chunkSize: 1000,
  chunkOverlap: 200,
  separators: ["\n\n", "\n", "。", "!", "?", ";", ",", " "],
});

同时 chunkSize 一定要配合 chunkOverlap 使用。

chunkOverlap 是指块与块之间重叠的数字。切分文本的时候,块与块有适当重叠,才能保证 LLM 回答地正确性。

如果在切分文本的时候,如果块与块之间没有重叠,万一关键内容恰好卡在切缝上

text 复制代码
块1结尾:......宝玉见黛玉进来,连忙起身让座。黛玉只觉他"面若中秋之月,
块2开头:色如春晓之花,鬓若刀裁,眉如墨画......

用户问"贾宝玉的外貌如何?",这句外貌描写的前半句在块 1 结尾,后半句在块 2 开头:

  • 向量检索命中了块 1,但块 1 里外貌描写只有一半,大模型信息不全

  • 向量检索命中了块 2,但块 2 开头突然冒出"色如春晓之花",没有主语,大模型可能理解错

同时会有丢失跨块语义的问题。《红楼梦》里大量使用前后呼应、指代:

text 复制代码
"......宝玉听了,登时发作起痴狂病来"(块1结尾)
"他把通灵玉摘下来,狠命摔去"(块2开头)

没有重叠时,"他"指谁?块 2 单独看语义残缺。有重叠后,"他"的上下文在块内就能找到。

用户回答阶段

具体步骤为:

  1. 用户提问:"贾宝玉的外貌如何?" ,将该问题同样转为向量

  2. 在 Milvus 里找和它最接近的向量,将对象对应的原文取出

  3. 把原文 + 问题拼成 Prompt 发给大模型

  4. 大模型流式输出回答问题

代码核心部分

连接 Milvus

typescript 复制代码
// 先确保数据库里有这张"表"(Milvus 叫集合)
await milvusClient.createCollection({
  collection_name: "hongloumeng",
  fields: [
    { name: "id", data_type: "VarChar", is_primary_key: true },
    { name: "text", data_type: "VarChar" }, // 存原文
    { name: "vector", data_type: "FloatVector", dim: "1024" }, // 存向量
  ],
});

// 创建索引,加速搜索
await milvusClient.createIndex({
  field_name: "vector",
  index_type: "IVF_FLAT",
  metric_type: "COSINE", // 用余弦相似度衡量两个向量"有多像"
});

// 加载到内存,不加载没法搜
await milvusClient.loadCollectionSync({ collection_name: "hongloumeng" });

createIndex 时,有 3 个字段

  • field_name :指定在哪个字段上创建索引

  • index_type :指定索引类型,这里用的是 IVF_FLAT

  • metric_type :衡量两个向量"有多像"的方式,这里用余弦相似度

文本切割 + 入库

typescript 复制代码
// 把 86 万字切成 1000 字一块,块之间重叠 200 字
const splitter = new RecursiveCharacterTextSplitter({
  chunkSize: 1000,
  chunkOverlap: 200,
  separators: ["\n\n", "\n", "。", "!", "?", ";", ",", " "],
});

const docs = await splitter.createDocuments([rawText]);

// 给每块加上 id
for (let i = 0; i < docs.length; i++) {
  docs[i].metadata.id = `hlm_${String(i + 1).padStart(6, "0")}`;
}

// 存入 Milvus(LangChain 会自动调 Embedding 模型把文字变成向量)
await vectorStore.addDocuments(docs);

86 万字 → 1160 块,每块约 1000 字,相邻块有 200 字重叠。

检索 + 问答

typescript 复制代码
async function* streamChatWithHongLouMeng(
  query: string
): AsyncGenerator<string> {
  console.log(`\n🔍 正在检索关于 "${query}" 的内容...`);
  // 1. 搜出最相关的 4 段原文
  const retriever = vectorStore.asRetriever({
    searchType: "similarity",
    k: 4,
  });
  const docs = await retriever.invoke(query);

  // 2. 组装提示词:告诉大模型它是谁 + 给它参考资料 + 用户问题
  const chain = prompt.pipe(model).pipe(new StringOutputParser());

  // 3. 流式生成:每出一个字就 yield 出去
  const stream = await chain.stream({
    context: formatContext(docs),
    input: query,
  });

  for await (const chunk of stream) {
    if (chunk) {
      yield chunk; // "贾" → "宝玉" → "的外貌" → ...
    }
  }
}

function formatContext(docs: Array<{ pageContent: string }>): string {
  return docs.map((doc) => doc.pageContent).join("\n\n");
}

提示词长这样:

text 复制代码
你是一个精通《红楼梦》的文学助手。

上下文信息:
(这里填搜出来的4段原文)

用户问题:贾宝玉的外貌如何?
回答:

SSE 流式推送

typescript 复制代码
// 告诉浏览器:我要用 SSE 协议,边生成边发
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");

// 每生成一个字,就包装成 SSE 事件发出去
for await (const chunk of streamChat(question)) {
  res.write(`event: chunk\n`);
  res.write(`data: {"content":"${chunk}"}\n\n`);
}

Content-Type 设置为 text/event-stream,告诉浏览器这不是普通响应,是事件流。

Cache-Control 设置为 no-cache,告诉浏览器不要缓存,导致用户获得旧的回答。

前端打字机

前端收到 SSE 事件后,不是一次性显示,而是逐个字符打到屏幕上:

typescript 复制代码
// 收到一个字 → 拆成打字单元 → 塞进队列
function push(text) {
  queue.push(...splitIntoTypingUnits(text)); // "贾宝玉" → ["贾宝", "玉"]
}

// 打字泵:从队列取字 → 显示 → 停顿 → 再取
async function pump() {
  while (active) {
    const unit = queue.shift();
    message.content += unit; // 追加到界面
    await sleep(getTypingDelay(unit)); // 句号停240ms,逗号140ms,中文70ms
  }
}

splitIntoTypingUnits 函数把一段流式文本拆成打字单元(每次显示的最小单位),让打字机节奏自然。

getTypingDelay 函数根据打字单元的内容类型,返回不同的停顿毫秒数,制造自然的人工打字节奏。

splitIntoTypingUnits 函数和 getTypingDelay 函数的具体实现,可在下文给出的代码仓库中查看。

效果演示

先执行 pnpm ingest 命令,把《红楼梦》存入数据库,这个命令只需跑一次。

然后执行 pnpm dev 命令,启动后端服务;执行 pnpm web:dev 命令,启动前端端服务。

浏览器打开 http://localhost:5173,输入问题即可体验。

完整代码实现已上传 github 仓库:github.com/Panda-plus5...

总结

本文以《红楼梦》问答助手为例,使用 Node.js + Milvus + 大模型实现了 RAG 系统。

用文本切分(1000 字块 + 200 字重叠)将 86 万字《红楼梦》小说向量化存入 Milvus,检索 Top-4 片段组装 Prompt 交给大模型,再通过 SSE 流式返回配合前端打字机效果,从而让 LLM 基于真实原著回答问题、有效抑制大语言模型的幻觉问题。

有关 Milvus 的更多知识可猛戳读者的另一篇文章:从 0 到实战:掌握向量数据库 Milvus,构建 AI 应用的核心能力

相关推荐
空堂与归1 小时前
机器学习如何入门?AI/ML/DL概念与建模流程全景
人工智能·机器学习
花花鱼1 小时前
TinyML Agent:MCU 上的微型智能体,AI 智能体下沉的底层实现
人工智能·单片机·嵌入式硬件
tech讯息1 小时前
商业化内容生产适用企业级视频生成模型云平台推荐|从模型生成至审核分发全链路 AWS 选型方案
人工智能·音视频·aws
独码侠1 小时前
FunASR 语音识别本地部署实录(下):34 分钟录音实测,踩过的坑、上线前要补的事
人工智能·语音识别
没落之王1 小时前
易学使者郑氏正脉郑冰推演马航事件
大数据·人工智能·算法·机器学习·可用性测试
wy_hhxx1 小时前
AI Agent & Harness Engineering 笔记
人工智能·笔记
yuhulkjv3351 小时前
Grok鸿蒙版导出word格式的终极解法:AI导出鸭如何重构AI内容到文档的最后一公里
人工智能·ai·word·harmonyos·ai导出鸭
ONEYAC唯样1 小时前
2026年中国电子元器件进出口全景分析:AI时代下的供应链重构与产业机会
人工智能
aneasystone本尊1 小时前
学习 Headroom 的输出 token 优化
人工智能