从 0 到 1 搭建 RAG 应用:用 LangChain + Chroma + qwen-plus 实现《红楼梦》问答

如何从零搭建RAG应用:《红楼梦》问答系统实战

在本文中,我们将基于《红楼梦》构建一个检索增强生成(Retrieval-Augmented Generation, RAG)系统。通过Langchain、Chroma和qwen-plus等工具,实现对小说文本的高效索引与查询。

什么是RAG?

RAG是一种结合了大规模语言模型与向量数据库的技术框架,旨在提升问答系统的准确性和相关性。通过将知识库中的信息与大语言模型相结合,RAG能够在回答问题时提供更精准的答案和上下文支持。

使用到的工具介绍

  • Langchain:一个模块化、可扩展的开源框架,用于简化大型语言模型的应用开发。
  • Chroma:一款高性能的向量数据库,专为语义相似性搜索设计。它允许我们高效地存储和检索高维向量数据。
  • qwen-plus:阿里巴巴推出的大规模预训练语言模型。

初始化向量数据库

在构建RAG系统的第一步是初始化向量数据库。我们需要创建一个Embedding对象,用于后续的索引操作:

const embeddings = new OpenAIEmbeddings({
  apiKey: dashscopeApiKey,
  model: "text-embedding-v4",
  batchSize: 10,
  configuration: {
    baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
  },
});

接下来,我们初始化Chroma向量数据库:

import { Chroma } from "@langchain/community/vectorstores/chroma";
import { OpenAIEmbeddings } from "@langchain/openai";

let vectorStore: Chroma;

async function main(): Promise
<void> {
  // 初始化向量数据库
  vectorStore = await Chroma.fromExistingCollection(embeddings, {
    collectionName: "hongloumeng",
    url: "http://localhost:8000",
  });
}

这里,fromExistingCollection方法用于连接一个已存在的Chroma集合,并返回可用于读写和检索的vectorStore实例。参数包括指定的集合名、数据库地址以及绑定的Embedding模型。

文本切分与向量库入库

为了将《红楼梦》文本数据存储到我们的RAG系统中,我们需要执行一系列步骤来完成文本切割并将其存储在Chroma向量库中:

async function ingestData() {
  console.log("📚 正在读取《红楼梦》文本...");
  const rawText = fs.readFileSync("./data/hong.txt", "utf-8");

  console.log("🔪 正在进行文本分割...");
  const textSplitter = new RecursiveCharacterTextSplitter({
    chunkSize: 1000, // 每个块的大小
    chunkOverlap: 200, // 块之间的重叠,保持上下文连贯性
    separators: ["\n\n", "\n", "。", "!", "?", ";", ",", " "], // 针对中文优化的分隔符
  });

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

  console.log(`📝 分割完成,共 ${docs.length} 个片段。正在存入向量数据库...`);

  // 将文档添加到现有的 Chroma 集合中
  await vectorStore.addDocuments(docs);

  console.log("✅ 数据入库完成!");
}

上述代码首先读取本地存储的《红楼梦》文本文件,然后使用RecursiveCharacterTextSplitter类进行文本切割。每个片段大小约为1000字节,并且每段之间有200字节重叠以保持上下文一致性。

检索向量数据库并生成答案

最后一步是基于Chroma检索到的《红楼梦》相关片段,调用大模型来生成和返回最终的答案。具体步骤包括:

  1. 设置检索器获取最相关的文本片段。
  2. 使用提示词模板定义问题回答格式。
  3. 创建文档组合链与检索链。

下面是一个示例代码段用于实现这些功能:

async function chatWithHongLouMeng(query: string) {
  console.log(`\n🔍 正在检索关于 "${query}" 的内容...`);

  // 设置检索器,获取最相关的 4 个片段
  const retriever = vectorStore.asRetriever({
    searchType: "similarity",
    k: 4,
  });

  // 定义提示词模板
  const prompt = ChatPromptTemplate.fromTemplate(`
    你是一个精通《红楼梦》的文学助手。请根据以下检索到的上下文信息回答用户的问题。
    如果上下文中没有相关信息,请直接说明你不知道,不要编造内容。

    上下文信息:
    {context}

    用户问题:{input}
    回答:
  `);

  // 创建文档组合链
  const combineDocsChain = await createStuffDocumentsChain({
    llm: qwenPlus, // 大语言模型实例
    prompt,
  });

  // 执行检索并返回答案
  const response = await combineDocsChain.call({ inputVariables: { input: query } });
  console.log(`回答:${response}`);
}

在上述代码中,我们首先配置了用于获取最相关片段的检索器,并定义了一个提示词模板。接下来,创建了文档组合链以生成最终答案。

以上便是如何从零开始构建一个基于《红楼梦》的RAG问答系统的全过程。通过这种方式,我们可以高效地利用大规模语言模型和向量数据库来增强问答系统的能力。

将 RAG 能力封装成 API

为了将构建好的《红楼梦》问答系统以易于调用的方式提供给前端,我们可以将其能力封装为一个 HTTP 接口。这里选择使用 Express 框架实现这个目标。

首先创建一个基于 Express 的服务器实例,并监听 18000 端口:

const PORT = 18000;

app.listen(PORT, () => {
  console.log(`API server running at http://localhost:${PORT}`);
});

接下来,我们配置 JSON 解析中间件和 CORS 安全响应头。这一步是确保客户端可以跨域请求后端接口的必要步骤:

app.use(express.json());
app.use((_, res, next) => {
  res.header("Access-Control-Allow-Origin", "*");
  res.header("Access-Control-Allow-Methods", "POST,OPTIONS");
  res.header("Access-Control-Allow-Headers", "Content-Type");
  next();
});

然后,我们将实现的 chatWithHongLouMeng 函数封装到一个 POST 请求处理函数中。这样前端可以通过发送包含用户问题的请求来调用问答系统:

app.post("/hongloumeng/chat", async (req, res) => {
  const question = req.body?.question;

  if (!question) {
    res.status(400).json({
      error: "Missing body parameter `question`",
    });
    return;
  }

  try {
    const answer = await chatWithHongLouMeng(question);
    res.json({ question, answer });
  } catch (error) {
    console.error("chat api failed:", error);
    res.status(500).json({
      error: "Failed to generate answer",
    });
  }
});

RAG 系统前端部分开发

在前端部分,我们采用了 Vue.js + Vite + Element Plus 的技术栈来构建用户界面。由于大模型返回的文本是 Markdown 格式,我们需要使用 marked 库将这些内容转换为 HTML。

为了确保安全展示这些内容,并防止潜在的 XSS 攻击,需要对生成的 HTML 进行消毒处理:

import { marked } from "marked";
import DOMPurify from "dompurify";

function renderMarkdown(content: string): string {
  const html = marked.parse(content, { breaks: true });
  return DOMPurify.sanitize(html);
}

为了使前端能够访问到后端服务,我们需要通过 Vite 的配置文件设置代理:

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";

export default defineConfig({
  plugins: [vue()],
  server: {
    port: 5173,
    proxy: {
      "/hongloumeng": {
        target: "http://localhost:18000",
        changeOrigin: true,
      },
    },
  },
});

接下来,前端可以向 /hongloumeng/chat 接口发送 POST 请求,并接收返回的答案。在界面中展示时利用 renderMarkdown 函数处理响应内容:

const response = await fetch("/hongloumeng/chat", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ question: input }),
});

if (!response.ok) {
  throw new Error(await response.text());
}

const data = (await response.json()) as { answer?: string; error?: string };

messages.value.push({
  role: "assistant",
  content: renderMarkdown(data.answer ?? ""),
});

最终,用户可以在前端界面中与《红楼梦》问答系统进行交互,获取相关内容的解答。

总结

本文详细介绍了基于 Langchain、Chroma 和 qwen-plus 构建一个《红楼梦》RAG 问答系统的全过程。从初始化向量数据库到数据入库和检索生成,再到后端API的设计实现以及前端界面的搭建,每个步骤都紧密相连,共同完成了这个智能化问答应用。

通过以上流程,我们得以将复杂的自然语言处理技术与经典文学作品相结合,不仅提升了用户获取信息的便捷性,也为《红楼梦》的研究提供了新的视角和技术手段。


> 🔗 相关阅读LangChain.js 架构设计深度剖析