游乐游手机版
首页/AI教程/文章详情

向量数据库实战:《天龙八部》RAG助手让AI更懂你

时间:2026-06-03 12:19
为什么我们需要 RAG? 设想一下,你手里有一本几百页的《天龙八部》电子书。要是突然想问一句:“段誉到底会哪些武功?” 传统搜索:你只能搜关键词“段誉”、“武功”。要是原文写的是“他施展出六脉神剑”,而压根没出现“武功”俩字,传统搜索很可能就漏掉了。 语义搜索(RAG):它能理解“会哪些武功”和“施

为什么我们需要 RAG?

设想一下,你手里有一本几百页的《天龙八部》电子书。要是突然想问一句:“段誉到底会哪些武功?”

第一次用向量数据库!手搓《天龙八部》RAG助手,让AI真正“懂”你

  • 传统搜索:你只能搜关键词“段誉”、“武功”。要是原文写的是“他施展出六脉神剑”,而压根没出现“武功”俩字,传统搜索很可能就漏掉了。
  • 语义搜索(RAG):它能理解“会哪些武功”和“施展出六脉神剑”在语义上是相关的。即使关键词不匹配,也能精准定位到相关段落,然后让AI总结给你听。

这,就是RAG(Retrieval-Augmented Generation,检索增强生成)的魅力。今天,我们就通过三个核心文件,一步步把这个流程实现出来。

️ 技术栈概览

本次MVP(最小可行性产品)选用了这样一套“黄金组合”:

  1. 数据源:天龙八部.epub(本地电子书)
  2. 文档加载与分割:@langchain/community(EPUB Loader)+ RecursiveCharacterTextSplitter
  3. 嵌入模型(Embedding):@langchain/openai(将文本转为向量)
  4. 向量数据库:Milvus(高性能向量检索,支持本地或云端)
  5. 大语言模型(LLM):ChatOpenAI(用于最终的回答生成)

核心流程解析

RAG的流程其实就两步:入库和查询。听起来简单,但每一步都有不少细节。

第一步:数据入库(ebook-writer.mjs)

这是最耗时的一步。我们需要把书“读”进去,切碎,变成向量,再存进Milvus。

1. 初始化 Milvus 集合(Schema 设计)

向量数据库不像MySQL那样随意建表,需要先定义Schema。在ensureBookCollection函数中,我们设计了如下字段:

// 核心 Schema 定义
fields: [
  { name: 'id', data_type: DataType.VarChar, is_primary_key: true },           // 主键
  { name: 'book_id', data_type: DataType.VarChar },                              // 书籍ID
  { name: 'book_name', data_type: DataType.VarChar },                            // 书名
  { 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: 1024 },                //  核心:1024维向量
]

2. 加载与切片(Loading & Splitting)

电子书不能一次性全塞进去,太大了。我们用LangChain的EPubLoader按章节加载,再用RecursiveCharacterTextSplitter按段落切分。

const loader = new EPubLoader(EPUB_FILE, { splitChapters: true });
const documents = await loader.load();
const textSplitter = new RecursiveCharacterTextSplitter({
  chunkSize: 500,        // 每个片段500字符
  chunkOverlap: 50,      // 重叠50字符,保持上下文连贯
});
// 遍历每一章,切分后生成向量
for (let chapterIndex = 0; chapterIndex < documents.length; chapterIndex++) {
  const chunks = await textSplitter.splitText(documents[chapterIndex].pageContent);
  // ... 后续生成向量并插入
}

3. 向量化与批量插入

这里是“魔法”发生的地方。调用OpenAI API将文本转为向量数组,然后批量写入Milvus。

// 并发生成向量,提升性能
const insertData = await Promise.all(
  chunks.map(async (chunk, chunkIndex) => {
    const vector = await getEmbedding(chunk);  // 调用 OpenAI Embedding
    return {
      id: `${bookId}_${chapterNum}_${chunkIndex}`,
      content: chunk,
      vector,  // ⭐ 存入向量
      // ...其他元数据
    };
  })
);
await client.insert({ collection_name: COLLECTION_NAME, data: insertData });

第二步:语义检索(ebook-query.mjs)

数据入库后,就可以进行语义搜索了。

用户提问:“段誉会什么武功?”

  1. Query Embedding:先把这个问题也变成向量。
  2. Vector Search:在Milvus中计算问题向量与库中所有向量的余弦相似度。
  3. 返回结果:取出相似度最高的Top 3个文本片段。
const query = '段誉会什么武功?';
const queryVector = await getEmbedding(query);
const searchResult = await client.search({
  collection_name: COLLECTION_NAME,
  vector: queryVector,
  limit: 3,                  // 取前3个最相关的
  metric_type: MetricType.COSINE, // 使用余弦相似度
  output_fields: ['content', 'chapter_num', 'book_name'], // 同时返回原文和元数据
});

此时,控制台会打印出得分最高的几个片段。哪怕原文里没有“武功”这两个字,只要语义相近(比如描述了打斗场景),都能被搜出来。

第三步:RAG 回答生成(ebook-rag.mjs)

光搜出来还不够,我们要让AI基于搜到的内容来回答问题。这才是RAG的完整形态。

构建 Prompt

我们将检索到的上下文(Context)注入到Prompt中,明确告诉LLM:“别瞎编,只根据下面提供的小说片段回答”。

const context = retrievedContent.map((item, i) => {
  return `[片段${i + 1}]章节:第${item.chapter_num}章内容:${item.content}`;
}).join('\n\n---\n\n');

const prompt = `你是一个专业的《天龙八部》小说助手。请根据以下《天龙八部》小说片段内容回答问题:
${context}
用户问题:${question}
回答要求:
1. 如果片段中有相关信息,请结合小说内容给出详情
2. 如果片段中没有相关的信息,请如实告知用户
3. 可以引用原文内容来支持你的回答`;

调用 LLM 生成答案

const model = new ChatOpenAI({ temperature: 0.7, model: process.env.MODEL_NAME });
const response = await model.invoke(prompt);
console.log(response.content);  // 输出:段誉主要学会了北冥神功、凌波微步以及六脉神剑...

完整源码展示

为了方便大家复现,以下是三个核心文件的完整代码。请确保你已经安装了依赖:npm install @zilliz/milvus2-sdk-node @langchain/openai @langchain/community @langchain/textsplitters dotenv

并且配置好.env文件:

MILVUS_ADDRESS=your_milvus_address
MILVUS_TOKEN=your_milvus_token
OPENAI_API_KEY=your_sk_key
OPENAI_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1  # 或其他袋里地址
EMBEDDING_MODEL_NAME=text-embedding-v3
MODEL_NAME=qwen-plus

1. 数据入库脚本:ebook-writer.mjs

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';    // 请确保当前目录下有此文件

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 ensureBookCollection(bookId) {
  try {
    const hasCollection = await client.hasCollection({ collection_name: COLLECTION_NAME });
    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 },
          { 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 },
        ],
      });
      await client.createIndex({
        collection_name: COLLECTION_NAME,
        field_name: 'vector',
        index_type: IndexType.IVF_FLAT,
        metric_type: MetricType.COSINE,
        params: { nlist: VECTION_DIM },
      });
      console.log('集合与索引创建成功');
    }
    try {
      await client.loadCollection({ collection_name: COLLECTION_NAME });
    } catch(err) {
      console.log('集合已处于加载状态');
    }
  } catch(err) {
    console.error('创建集合失败:', err.message);
    throw err;
  }
}

async function insertChunksBatch(chunks, bookId, chapterNum) {
  if (chunks.length === 0) return 0;
  const insertData = await Promise.all(
    chunks.map(async (chunk, chunkIndex) => {
      const vector = await getEmbedding(chunk);
      return {
        id: `${bookId}_${chapterNum}_${chunkIndex}`,
        book_id: String(bookId),
        book_name: BOOK_NAME,
        chapter_num: chapterNum,
        index: chunkIndex,
        content: chunk,
        vector,
      };
    })
  );
  const insertResult = await client.insert({
    collection_name: COLLECTION_NAME,
    data: insertData,
  });
  return Number(insertResult.insert_cnt) || 0;
}

async function loadAndProcessEPubStreaming(bookId) {
  console.log('开始加载 EPUB 文件');
  const loader = new EPubLoader(EPUB_FILE, { splitChapters: true });
  const documents = await loader.load();
  const textSplitter = new RecursiveCharacterTextSplitter({
    chunkSize: CHUNK_SIZE,
    chunkOverlap: CHUNK_OVERLAP,
  });
  let totalInserted = 0;
  for (let chapterIndex = 0; chapterIndex < documents.length; chapterIndex++) {
    const chapter = documents[chapterIndex];
    console.log(`处理第 ${chapterIndex + 1}/${documents.length} 章`);
    const chunks = await textSplitter.splitText(chapter.pageContent);
    if (chunks.length === 0) continue;
    const insertedCount = await insertChunksBatch(chunks, bookId, chapterIndex + 1);
    totalInserted += insertedCount;
    console.log(`插入成功 ${insertedCount} 个片段, 累计 ${totalInserted}`);
  }
  console.log(`\n处理完成, 共插入 ${totalInserted} 个片段`);
}

async function main() {
  try{
    await client.connectPromise;
    console.log('连接 Milvus 成功');
    const bookId = 1;
    await ensureBookCollection(bookId);
    await loadAndProcessEPubStreaming(bookId);
  } catch(err) {
    console.error('主流程错误:', err);
  }
}

main();

2. 纯向量检索测试:ebook-query.mjs

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 = '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) {
  return await embeddings.embedQuery(text);
}

async function main() {
  try {
    await client.connectPromise;
    await client.loadCollection({ collection_name: COLLECTION_NAME }).catch(() => {});
    const query = '段誉会什么武功?';
    console.log(`正在搜索:"${query}"`);
    const queryVector = await getEmbedding(query);
    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','book_name'],
    });
    searchResult.results.forEach((item, index) => {
      console.log(`\n--- 第${index + 1}个结果 (Score: ${item.score.toFixed(4)}) ---`);
      console.log(`章节:${item.book_name}${item.chapter_num}章`);
      console.log(`内容:${item.content.substring(0, 100)}...`);
    });
  } catch(err){
    console.log('执行失败', err);
  }
}

main();

3. RAG 智能问答:ebook-rag.mjs

import 'dotenv/config';
import { MilvusClient, MetricType } 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;

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) {
  return await embeddings.embedQuery(text);
}

async function retrieveRelevantContent(question, k = 3) {
  const queryVector = await getEmbedding(question);
  const searchResult = await client.search({
    collection_name: COLLECTION_NAME,
    vector: queryVector,
    limit: k,
    metric_type: MetricType.COSINE,
    output_fields: ['book_name', 'chapter_num', 'content']
  });
  return searchResult.results;
}

async function answerEbookQuestion(question, k = 3) {
  console.log(`\n? 用户提问:${question}`);
  const retrievedContent = await retrieveRelevantContent(question, k);
  if (retrievedContent.length === 0) {
    return '抱歉,没有在书中找到相关内容。';
  }
  const context = retrievedContent.map((item, i) => {
    return `[片段${i + 1} - 第${item.chapter_num}章]: ${item.content}`;
  }).join('\n');
  const prompt = `你是一个专业的《天龙八部》小说助手。请严格基于以下提供的小说片段内容回答问题。
【参考片段】:
${context}
【用户问题】:
${question}
【回答要求】:
1. 准确、详细,符合原著情节。
2. 如果片段信息不足,请说明"根据现有片段无法完全确认",不要胡编乱造。
3. 适当引用原文。
AI 回答:`;
  const response = await model.invoke(prompt);
  console.log(`\n✅ AI 回答:\n${response.content}`);
  return response.content;
}

async function main() {
  try {
    await client.connectPromise;
    await client.loadCollection({ collection_name: COLLECTION_NAME }).catch(() => {});
    await answerEbookQuestion('谁的武功最厉害?');
  } catch (err) {
    console.log('运行出错', err.message);
  }
}

main();

总结与展望

通过这三个文件,我们完成了一个完整的RAG闭环:

  1. 非结构化数据(EPUB)→ 结构化向量(Milvus)。
  2. 自然语言提问 → 语义匹配 → 精准上下文。
  3. 上下文 + LLM → 高质量回答。

当然,这只是一个MVP。在正式的商业级开发中,还有很多可以优化的方向:

  • 混合检索:关键词匹配(BM25)+ 向量检索,效果更稳定。
  • 重排序(Rerank):对检索回来的结果再次打分排序。
  • 多模态:不仅搜文字,还能搜书里的插图。
  • 权限管理:不同用户只能搜自己上传的书。

向量数据库早已不是什么黑科技,它已经成为AI应用的标配基础设施。赶紧动手试试,把你的知识库“喂”给AI吧。

来源:https://juejin.cn/post/7615426060051529728
上一篇Veo 3.1视频生成模型技术更新解析 下一篇从零搭建企业级私有知识库:RAG大模型实战完整教程(附代码)
本站内容用于信息整理与展示,如有侵权或内容问题请及时联系处理。

相关推荐

补充同频道和同主题内容,方便继续浏览更多相关内容。

同类最新

继续查看同栏目最近更新的文章。

更多
2026实测解析GPT-5.5模型能力详解与国内合规使用规范
AI教程 · 2026-06-03

2026实测解析GPT-5.5模型能力详解与国内合规使用规范

2026年,AI大模型迎来了又一次迭代升级。GPT-5 5凭借在多模态精细化处理能力上的跨越式突破,正逐步成为职场办公、内容创作、代码开发以及数据优化等领域的核心生产力工具。然而,对国内多数用户而言,当前仍面临不少现实难题:渠道杂乱、合规边界模糊、账号频繁被封、数据泄露风险——各类非正规镜像站、共享

分时操作系统和实时操作系统的主要区别
AI教程 · 2026-06-03

分时操作系统和实时操作系统的主要区别

分时操作系统和实时操作系统区别 ?️ 操作系统家族里,有两类系统经常被放在一起比较:分时操作系统和实时操作系统。它们虽然都叫“操作系统”,但设计哲学、工作机制和应用场景可以说是天差地别。一个追求“公平共享”,一个追求“确定性响应”。这篇文章打算从定义、核心机制、调度策略、实际应用等维度,把这两者的本

企业AI智能体从零搭建实战踩坑经验全记录
AI教程 · 2026-06-03

企业AI智能体从零搭建实战踩坑经验全记录

去年开始用腾讯云智能体开发平台(ADP)跑了几个企业项目,从最基础的客服Bot一路干到多Agent协同系统,中间踩的坑不少,但积累下来的经验价值也相当可观。这篇文章就聊聊实际落地过程里的那些关键节点和教训,给同样在腾讯云上折腾AI Agent的朋友做个参考。为什么选腾讯云ADP而不是从零搭建做第一个

Selenium自动化测试入门:从环境搭建到首个可维护用例
AI教程 · 2026-06-03

Selenium自动化测试入门:从环境搭建到首个可维护用例

Selenium 入门的核心不在于记住多少 API,而在于把三件事想清楚:环境别装错版本、等待机制别用 sleep、用例结构别写成流水账。下面按照“装环境 → 跑通第一个脚本 → 理解等待 → 选对定位器 → 拆成 Page Object”的顺序走一遍,每一步都附上代码,踩过的坑直接标出来。 Sel

专业表格魔法师 QoderWork CN 让脏数据秒变仪表盘神器
AI教程 · 2026-06-03

专业表格魔法师 QoderWork CN 让脏数据秒变仪表盘神器

使用案例 今天聊聊怎么用阿里巴巴的 QoderWork CN 桌面应用智能体,把 Excel 里那堆乱糟糟的原始数据清洗干净,再做成可视化的看板。整个过程基本不需要写代码,全靠自然语言对话就能搞定。下面就用一个实际案例,把操作步骤拆开来讲。 步骤一:安装并注册 QoderWork CN 账号 先到