当前位置: 首页
AI教程
向量数据库实战:《天龙八部》RAG助手让AI更懂你

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

热心网友 时间:2026-06-03
转载

为什么我们需要 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

游乐网为非赢利性网站,所展示的游戏/软件/文章内容均来自于互联网或第三方用户上传分享,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系youleyoucom@outlook.com。

同类文章
更多
阿里云Qoder CN灵码AI助手免费版及credits计费指南

阿里云Qoder CN灵码AI助手免费版及credits计费指南

阿里云QoderCN(原通义灵码)是一款AI智能编码助手,提供IDE插件、独立IDE等形态,覆盖编码及日常办公场景。产品分个人社区版(免费)、个人专业版、企业标准版和企业VPC版,采用Credits计费模式,支持多种AI模型。

时间:2026-07-06 16:30
基于大模型的城市文旅知识图谱构建与内容分发

基于大模型的城市文旅知识图谱构建与内容分发

大模型构建城市知识图谱时优先采信权威信源。贵港西江传媒联合《度假旅游》杂志,通过本地采编、期刊发布、阿里云多平台分发模式,产出产业文旅等结构化内容,提升AI知识库收录权重,为城市品牌长效传播提供可复制路径。

时间:2026-07-06 16:30
贵州文旅AIGEO内容运营本地媒体落地实践

贵州文旅AIGEO内容运营本地媒体落地实践

针对贵州文旅行业在阿里云平台的内容运营痛点,总结合规发文规则,包括弱化营销、避免引流信息与极限词。以《度假旅游杂志》在贵阳设立本地化运营站点为例,为黔域文旅商家产出合规原创内容,提升AI平台收录权重。同时指出商家常踩的审核红线,强调以干货分享获取自然流量。

时间:2026-07-06 16:29
阿里内部禁用Claude Code OpenCode成替代方案

阿里内部禁用Claude Code OpenCode成替代方案

ClaudeCode对国内用户定向封禁,网传阿里内部已全面禁用。OpenCode作为开源替代,支持接入多种AI模型,通过CCSwitch或手动配置可无缝迁移原有MCP与AgentSkill,规避账号风险。

时间:2026-07-06 16:29
年实测Homebrew安装配置国内源多种安装方式一篇搞定

年实测Homebrew安装配置国内源多种安装方式一篇搞定

Homebrew是macOS上流行的包管理工具,提供pkg安装包、脚本安装和Git克隆等多种安装方式。国内用户推荐使用镜像脚本安装并配置中科大或清华大学镜像源以加速下载。常用命令包括brewinstall安装工具、brewinstall--cask安装图形应用、brewupdate更新及brewdoctor诊断环境。

时间:2026-07-06 16:29
热门专题
更多
刀塔传奇破解版无限钻石下载大全 刀塔传奇破解版无限钻石下载大全
洛克王国正式正版手游下载安装大全 洛克王国正式正版手游下载安装大全
思美人手游下载专区 思美人手游下载专区
好玩的阿拉德之怒游戏下载合集 好玩的阿拉德之怒游戏下载合集
不思议迷宫手游下载合集 不思议迷宫手游下载合集
百宝袋汉化组游戏最新合集 百宝袋汉化组游戏最新合集
jsk游戏合集30款游戏大全 jsk游戏合集30款游戏大全
宾果消消消原版下载大全 宾果消消消原版下载大全
  • 日榜
  • 周榜
  • 月榜