🚀我用 RAG 治好了大模型的 “瞎编病”:从原理到落地,把检索增强生成讲透

128 阅读9分钟

做过大模型应用的同学,肯定遇到过这种尴尬:让大模型回答 “公司内部课程有哪些”,它能一本正经编出不存在的课程;问 “最新产品文档里的功能”,它掏出半年前的旧信息瞎讲… 😅 这时候,检索增强生成(RAG)  就是大模型的 “救命药”—— 让大模型能 “联网查资料”“读私有文档”,回答又准又新。

今天就从 “大模型为什么需要 RAG” 讲起,一层层拆解 RAG 的核心逻辑、Embedding(文本嵌入)的魔法,最后带着大家用 Node.js 亲手实现一个 “本地知识库 + RAG” 的 Demo,彻底搞懂这套流程~

为什么大模型需要 RAG

大模型(比如 GPT-3.5、LLaMA 等)虽然能 “无中生有” 般生成流畅内容,但有两个致命弱点:

1. 知识有 “保质期”

大模型的训练数据是 “截止到某一时间” 的(比如 GPT-4 截止到 2023 年 4 月),对训练后出现的新信息(如 2023 年之后的热点、公司最新产品)一无所知。让它回答这类问题,只能 “瞎编”。

2. 私有知识 “进不去”

公司内部文档、业务数据、保密知识库等 “私有信息”,大模型在训练时根本接触不到。如果直接问 “公司内部有多少门课程”,大模型没见过这些内容,也只能 “胡诌”。

而 RAG 的核心价值,就是让大模型能 “临时学习” 外部知识:需要回答问题时,先从 “外部知识库” 里检索相关内容,再把这些内容作为 “参考资料” 喂给大模型,让它基于 “资料 + 自身能力” 生成回答。

RAG 到底是什么?一句话讲透核心逻辑

RAG,全称Retrieval-Augmented Generation(检索增强生成) ,本质是 “检索” 和 “生成” 两个步骤的结合,流程如下:

image.png

这里有两个关键概念要提一下 :

  • Function Call:LLM 调用外部工具的能力,“检索” 就是 LLM 调用的一个工具(比如调用代码读取你的知识库);
  • MCP:定义了 LLM 和外部资源(比如你的知识库、向量数据库)的通信协议,确保 LLM 能正确 “调用” 这些外部资源。

简单说,RAG 就是借助 Function Call 和 MCP,让 LLM “手脚更灵活”—— 能主动去你的私有数据里找答案,而不是只靠自己的 “记忆”。

RAG 的核心:“检索” 怎么实现

RAG 的关键在 “检索”—— 怎么从一堆私有数据里,快速找到和用户问题 “最相关” 的内容

比如用户问 “后端开发相关的文章有哪些?”,你电脑里有 100 篇文章,怎么挑出最相关的 3 篇?

传统的 “关键字搜索”(比如用LIKE "%后端开发%")有个大问题:只能匹配字面,不能理解语义。比如 “服务器编程” 和 “后端开发” 意思相近,但关键字搜不到;而 “后端开发工具” 虽然包含关键字,却可能不是用户要的内容。

这时候,Embedding(文本嵌入)  就派上用场了 —— 它能让机器 “理解” 文本的语义,再通过 “向量相似度” 找到最相关的内容。

1. Embedding:把文字变成 “数字身份证”

Embedding 的核心是 “文本→向量” 的转换:
用专门的模型(比如 OpenAI 的text-embedding-ada-002)把一段文字,转换成一串数字(比如 1436 个维度的向量)。这串数字不是随机的 ——语义越相近的文本,向量越 “像”(距离越近)

举个例子:

  • “后端开发” 的向量:[0.12, 0.34, -0.56, ..., 0.78](共 1436 个数字)
  • “服务器编程” 的向量:[0.11, 0.33, -0.55, ..., 0.77](和上面很像,距离近)
  • “前端 Vue 开发” 的向量:[0.89, -0.21, 0.45, ..., -0.67](和上面差别大,距离远)

这样一来,“找相关内容” 就变成了 “找向量距离近的文本”—— 机器能轻松计算。

2. 余弦相似度:判断向量 “有多像” 的尺子

要衡量两个向量的相似度,最常用的是余弦相似度(cosine similarity)。它的核心逻辑是:

  • 两个向量方向越一致(语义越像),余弦相似度越接近1
  • 两个向量方向越垂直(语义越不像),余弦相似度越接近0

用公式表示(代码里也能对应):

image.png

本地知识库 + RAG,实现 “精准回答课程问题”

现在结合代码,一步步实现 “读取本地课程文档→生成 Embedding→检索相似内容→喂给大模型生成回答” 的完整流程。

步骤 1:准备本地知识库(课程信息)

先创建一个lesson.txt文件,写入私有课程信息(大模型没见过的内容):

《A课程》
描述:A课程是Python课程,主要讲解Python基础。
定价:99元

《B课程》:
B课程是Python课程,主要讲解Python爬虫。
价格:199元

《C课程》:
C课程是Python课程,主要讲解Python数据分析。
188元。

步骤 2:给知识库内容生成 Embedding(文本向量化)

如果知识库内容很多,我们需要先把所有内容 “向量化”,存起来(比如存成posts_with_embedding.json),这样后续检索时就不用重复生成 Embedding 了。

下面是 “读取内容 + 生成 Embedding + 存储” 的代码(用 OpenAI 的text-embedding-ada-002模型):

// llm.mjs:封装OpenAI客户端和余弦相似度计算
import OpenAI from 'openai';
import dotenv from 'dotenv';
dotenv.config({ path: '.env' });

export const client = new OpenAI({
  apiKey: process.env.OPENAI_KEY,
  baseURL: process.env.OPENAI_BASE_URL, // 可选:对接Ollama等本地模型
});

// 计算两个向量的余弦相似度
export const cosineSimilarity = (v1, v2) => {
  // 计算点积
  const dotProduct = v1.reduce((acc, curr, i) => acc + curr * v2[i], 0);
  // 计算v1的模长
  const lenV1 = Math.sqrt(v1.reduce((acc, curr) => acc + curr * curr, 0));
  // 计算v2的模长
  const lenV2 = Math.sqrt(v2.reduce((acc, curr) => acc + curr * curr, 0));
  // 防止除零错误,加小epsilon
  return dotProduct / (lenV1 * lenV2 + 1e-10);
};
// generate-embeddings.mjs:给知识库内容生成Embedding并存储
import fs from 'fs/promises';
import { client } from './llm.mjs';

const inputFilePath = './data/posts.json'; // 假设posts.json是课程结构化数据
const outputFilePath = './data/posts_with_embedding.json';

async function generateEmbeddings() {
  // 1. 读取原始数据
  const rawData = await fs.readFile(inputFilePath, 'utf-8');
  const posts = JSON.parse(rawData);

  // 2. 逐个生成Embedding
  const postsWithEmbedding = [];
  for (const { title, category } of posts) {
    // 调用OpenAI Embedding API,文本转向量
    const response = await client.embeddings.create({
      model: 'text-embedding-ada-002',
      input: `标题: ${title}; 分类: ${category}`,
    });
    // 存储“文本+向量”
    postsWithEmbedding.push({
      title,
      category,
      embedding: response.data[0].embedding,
    });
  }

  // 3. 写入文件
  await fs.writeFile(
    outputFilePath,
    JSON.stringify(postsWithEmbedding, null, 2)
  );
  console.log('Embedding生成完成,已存入', outputFilePath);
}

generateEmbeddings();

这段代码的作用是:把posts.json里的每条课程信息,通过 OpenAI 的 Embedding 模型转成向量,存到posts_with_embedding.json里。后续检索时,直接用 “预生成的向量”,既省时间又省钱~

步骤 3:检索相似内容 + 喂给大模型生成回答

当用户提问时,要完成 “问题向量化→检索相似内容→增强 Prompt→大模型生成” 的流程。核心代码如下:

// answer-with-rag.mjs:实现RAG完整流程
import fs from 'fs/promises';
import { client, cosineSimilarity } from './llm.mjs';

// 1. 读取“带Embedding的知识库”
const KB_FILE_PATH = './data/posts_with_embedding.json';
const kbData = await fs.readFile(KB_FILE_PATH, 'utf-8');
const knowledgeBase = JSON.parse(kbData);

// 2. 用户问题
const userQuestion = '公司有多少门Python课程?分别多少钱?';

// 3. 生成“问题的Embedding”
const questionEmbeddingResp = await client.embeddings.create({
  model: 'text-embedding-ada-002',
  input: userQuestion,
});
const questionEmbedding = questionEmbeddingResp.data[0].embedding;

// 4. 检索“最相似的知识库内容”
const similarItems = knowledgeBase
  .map((item) => ({
    ...item,
    similarity: cosineSimilarity(questionEmbedding, item.embedding),
  }))
  .sort((a, b) => b.similarity - a.similarity) // 相似度降序
  .slice(0, 3); // 取Top3相似内容

// 5. 拼接“检索结果”和“问题”,生成增强Prompt
const retrievedContext = similarItems
  .map((item) => `标题:${item.title},分类:${item.category}`)
  .join('\n');

const augmentedPrompt = `
你是课程助手,请根据以下信息回答问题。
若问题与课程无关,请说明“仅能回答课程相关问题”。

课程信息:
${retrievedContext}

用户问题:${userQuestion}
`;

// 6. 调用大模型生成回答
const chatResp = await client.chat.completions.create({
  model: 'gpt-3.5-turbo',
  messages: [
    { role: 'system', content: '你是专业课程助手,回答简洁准确。' },
    { role: 'user', content: augmentedPrompt },
  ],
  temperature: 0.1, // 降低随机性,回答更稳定
});

console.log('用户问题:', userQuestion);
console.log('检索到的相关内容:', retrievedContext);
console.log('大模型回答:', chatResp.choices[0].message.content);

运行这段代码,你会看到:大模型不再 “瞎编”,而是基于lesson.txt的课程信息,精准回答 “有 3 门 Python 课程,价格分别是 99、199、188 元”~

  • 安全问题:代码里用dotenv管理 API 密钥,没有硬编码,避免泄露;如果是公司数据,还可以用本地部署的模型(比如 Ollama、Llama),不用把私有数据传到外部 API,更安全(对应你材料里的 “给它喂私有知识库,不怕私有被外界大模型训练了”)。

当知识库很大时,该怎么办?

前面的例子里,我们用文件存储 Embedding,用循环计算相似度 —— 如果你的知识库只有几十、几百条数据,没问题;但如果有几万、几十万条数据(比如公司的所有文档、几百万篇文章),循环计算相似度会特别慢(可能要几秒甚至几分钟),这时候就需要向量数据库

1. 向量数据库是什么?

向量数据库是专门用来存储和检索向量的数据库,它有两个核心优势:

  • 高效检索:支持 “近似最近邻搜索”(ANN),能在百万级向量中,几毫秒内找到最相似的向量(比循环计算快 1000 倍以上);
  • 方便管理:支持向量的增删改查、批量导入,还能给向量加标签(比如 “课程数据”“文章数据”),方便分类检索。

2. 常用的向量数据库有哪些?

  • 开源免费:Milvus(米洛斯,适合自建)、Chroma(轻量级,适合开发测试);
  • 商业付费:Pinecone(云服务,开箱即用)、Weaviate(支持多模态,比如图片 + 文本)。

3. 向量数据库怎么融入 RAG 流程?

只需要把 “读取文件算相似度” 的步骤,换成 “调用向量数据库的检索接口”,流程不变:

image.png

比如用 Chroma 向量数据库,只需几行代码就能实现检索:

// 伪代码:用Chroma检索
import { ChromaClient } from 'chromadb';

// 1. 连接Chroma数据库
const client = new ChromaClient();
const collection = await client.getCollection({ name: "course_collection" });

// 2. 检索相似向量(问题Embedding→找Top3)
const results = await collection.query({
  queryEmbeddings: [questionEmbedding], // 问题的Embedding
  nResults: 3 // 取Top3
});

// 3. 提取检索到的课程信息
const retrievedInfo = results.documents[0].join('\n');

这样一来,哪怕你的知识库有 100 万条数据,也能快速检索到相关内容 —— 这是文件存储无法比拟的。

总结:RAG 不是高深技术,而是 “让 LLM 落地的实用工具”

写了这么多代码,其实 RAG 的核心逻辑特别简单:
让 LLM 在回答前 “先查资料”,而不是 “凭记忆瞎说”

它的门槛不高 —— 哪怕你只会基础的 JavaScript,也能跟着上面的代码,实现一个能读取本地文件、精准回答的 RAG 小工具。而当数据量大了,再引入向量数据库做优化,就能应对更复杂的场景(比如公司的知识库问答、个人的文档检索)。