深入浅出RAG:检索增强生成技术详解与实践

大型语言模型(LLM)如GPT系列,已成为AI领域的核心工具,其知识主要源于训练阶段的海量数据。这些数据包括文本、代码和各种结构化信息,通过参数化方式嵌入模型中。然而,LLM并非完美,尤其在处理未知或特定领域问题时,会出现"幻觉"(Hallucination)现象。这是一种AIGC(AI Generated Content)常见问题:当用户询问模型不熟悉的内容时,它不会直接承认无知,而是自信满满地编造答案。例如,问一个历史事件的细节,如果训练数据中缺少,模型可能输出看似合理的虚构描述。这不仅误导用户,还可能在专业应用中造成风险。

为了缓解这一问题,检索增强生成(Retrieval-Augmented Generation,简称RAG)技术应运而生。RAG不是简单依赖模型的内置知识,而是通过外部检索机制增强提示(Prompt),让LLM基于可靠的上下文生成响应。如果检索不到相关信息,系统可以设计为直接返回"不知道",从而减少幻觉。RAG的核心在于结合检索(Retrieval)、增强(Augmentation)和生成(Generation)三个步骤,实现更准确、可靠的输出。本文将从RAG的基本原理入手,逐步剖析其组件,并结合实际代码示例演示如何构建一个简单的RAG系统,帮助读者理解并实践这一技术。

RAG的核心原理:解决LLM幻觉的利器

RAG的本质是"检索增强",它将LLM的思考规划(Thinking Planning)与外部知识库结合,避免模型盲目生成。首先,回顾LLM的局限:训练数据有限,无法覆盖实时或私有信息,且语义理解有时依赖关键词匹配,无法实现真正的语义搜索。例如,查询"文中提到的水果,如苹果、香蕉、荔枝等",传统关键词匹配可能遗漏语义相近但表述不同的内容。

RAG通过向量嵌入(Embedding)解决这一痛点。向量是将文本转化为多维数字表示的形式,每个维度捕捉独特的语义特征。以一个简单比喻说明:假设我们用两个维度表示物体------食用性(0无到1高)和硬度(0软到1硬)。那么,"水果"可能表示为[0.9, 0.3],表示高食用性、中等硬度;"苹果"为[0.9, 0.5];"香蕉"为[0.9, 0.1];"石头"为[0.1, 0.9]。通过计算向量间的余弦相似度(Cosine Similarity),我们能实现语义搜索:查询"软而可食用的东西",系统会优先匹配香蕉而非石头。这比关键词匹配更智能,因为它捕捉了语义本质。

RAG的流程如下:

  1. 检索(Retrieval) :将用户查询嵌入向量,与知识库中的预嵌入文档比较,找出最相似的片段。
  2. 增强(Augmentation) :将检索到的文档片段插入原始提示中,形成增强提示。
  3. 生成(Generation) :LLM基于增强提示输出响应。如果相似度过低,可设置阈值返回"未知"。

这一机制特别适用于专家知识库、企业私有数据或多媒体文件(如TXT、PDF、MP3、视频)。对于大文件,需先切片成文档碎片(Document Chunks),然后嵌入向量存储。这确保了效率和准确性。

向量嵌入与语义搜索详解

为什么需要向量?传统文本匹配依赖关键词,无法处理同义词或上下文。例如,"苹果"可能指水果或公司,关键词搜索易混淆。向量嵌入使用模型如OpenAI的Embeddings,将文本映射到高维空间(通常数百维),每个维度代表语义属性,如情感、主题等。

语义搜索流程:

  • 嵌入查询:将用户问题转为向量。
  • 知识库构建:提前将文档嵌入向量,存储在向量数据库中。
  • 相似度计算 :用余弦相似度公式: <math xmlns="http://www.w3.org/1998/Math/MathML"> cos ⁡ ( θ ) = A ⋅ B ∣ A ∣   ∣ B ∣ \cos(\theta) = \frac{\mathbf{A} \cdot \mathbf{B}}{|\mathbf{A}| \, |\mathbf{B}|} </math>cos(θ)=∣A∣∣B∣A⋅B ,值越接近1越相似。
  • 可视化:在二维平面投影向量,相似向量聚簇,便于理解。

实际中,向量维度远高于2维,但原理相同。这使得RAG能处理复杂查询,如"光光和东东是怎么成为朋友的",通过检索故事片段增强响应。

RAG组件剖析

1. 检索器(Retriever)

检索器是RAG的入口。它将原始提示嵌入向量,与知识库比较。常用工具如LangChain的VectorStore,支持内存或持久化存储。相似度计算通常用余弦,确保高效检索。

2. 知识库(Knowledge Base)

知识库存储嵌入后的文档。可包括:

  • 专家领域知识(如医疗、法律)。
  • 企业私有数据(需确保安全)。
  • 多类型文件:大文件切片后嵌入,避免内存溢出。

3. 增强提示(Augment Prompt)

原始提示 + 检索文档 = 增强提示。例如,检索到2-3段相关片段,插入提示中。

4. 生成(Generation)

LLM如ChatOpenAI,使用增强提示生成。如果无匹配,设计为输出"故事中未提及"。

实践示例:用LangChain构建RAG系统

下面通过一段JavaScript代码(基于Node.js和LangChain库)演示RAG实现。代码使用OpenAI的Embeddings和Chat模型,构建内存向量存储,存储关于"光光和东东"友情故事的文档片段。注意:代码中需配置环境变量(如API Key),实际运行前确保dotenv加载。

代码如下:

JavaScript

javascript 复制代码
import "dotenv/config";
import {
  ChatOpenAI,
  OpenAIEmbeddings,
} from '@langchain/openai';

// 知识库中一段知识的抽象概念
import {
  Document,
} from '@langchain/core/documents';

// 内存向量数据库
import {
  MemoryVectorStore,
} from '@langchain/community/vectorstores/memory';

const model = new ChatOpenAI({
  modelName: process.env.MODEL_NAME,
  apiKey: process.env.OPENAI_API_KEY,
  configuration: {
    baseURL: process.env.OPENAI_API_BASE_URL,
  },
  temperature: 0,
});

const embeddings = new OpenAIEmbeddings({
  apiKey: process.env.OPENAI_API_KEY,
  model: process.env.EMBEDDING_MODEL_NAME,
  configuration: {
    baseURL: process.env.OPENAI_API_BASE_URL,
  }
});

const documents = [
  new Document({
    pageContent: `光光是一个活泼开朗的小男孩,他有一双明亮的大眼睛,总是带着灿烂的笑容。光光最喜欢的事情就是和朋友们一起玩耍,他特别擅长踢足球,每次在球场上奔跑时,就像一道阳光一样充满活力。`,
    metadata: {
      chapter: 1,
      character: "光光",
      type: "角色介绍",
      mood: "活泼"
    },
  }),
  new Document({
    pageContent: `东东是光光最好的朋友,他是一个安静而聪明的男孩。东东喜欢读书和画画,他的画总是充满了想象力。虽然性格不同,但东东和光光从幼儿园就认识了,他们一起度过了无数个快乐的时光。`,
    metadata: {
      chapter: 2,
      character: "东东",
      type: "角色介绍",
      mood: "温馨"
    },
  }),
  new Document({
    pageContent: `有一天,学校要举办一场足球比赛,光光非常兴奋,他邀请东东一起参加。但是东东从来没有踢过足球,他担心自己会拖累光光。光光看出了东东的担忧,他拍着东东的肩膀说:"没关系,我们一起练习,我相信你一定能行的!"`,
    metadata: {
      chapter: 3,
      character: "光光和东东",
      type: "友情情节",
      mood: "鼓励",
    },
  }),
  new Document({
    pageContent: `接下来的日子里,光光每天放学后都会教东东踢足球。光光耐心地教东东如何控球、传球和射门,而东东虽然一开始总是踢不好,但他从不放弃。东东也用自己的方式回报光光,他画了一幅画送给光光,画上是两个小男孩在球场上一起踢球的场景。`,
    metadata: {
      chapter: 4,
      character: "光光和东东",
      type: "友情情节",
      mood: "互助",
    },
  }),
  new Document({
    pageContent: `比赛那天终于到了,光光和东东一起站在球场上。虽然东东的技术还不够熟练,但他非常努力,而且他用自己的观察力帮助光光找到了对手的弱点。在关键时刻,东东传出了一个漂亮的球,光光接球后射门得分!他们赢得了比赛,更重要的是,他们的友谊变得更加深厚了。`,
    metadata: {
      chapter: 5,
      character: "光光和东东",
      type: "高潮转折",
      mood: "激动",
    },
  }),
  new Document({
    pageContent: `从那以后,光光和东东成为了学校里最要好的朋友。光光教东东运动,东东教光光画画,他们互相学习,共同成长。每当有人问起他们的友谊,他们总是笑着说:"真正的朋友就是互相帮助,一起变得更好的人!"`,
    metadata: {
      chapter: 6,
      character: "光光和东东",
      type: "结局",
      mood: "欢乐",
    },
  }),
  new Document({
    pageContent: `多年后,光光成为了一名职业足球运动员,而东东成为了一名优秀的插画师。虽然他们走上了不同的道路,但他们的友谊从未改变。东东为光光设计了球衣上的图案,光光在每场比赛后都会给东东打电话分享喜悦。他们证明了,真正的友情可以跨越时间和距离,永远闪闪发光。`,
    metadata: {
      chapter: 7,
      character: "光光和东东",
      type: "尾声",
      mood: "温馨",
    },
  }),
];

const vectorStore = await MemoryVectorStore.fromDocuments(
  documents,
  embeddings,
);

// 检索器
// k 返回多少段文档
const retriever = vectorStore.asRetriever({ k: 2 });

const questions = ['光光和东东是怎么成为朋友的'];

for (const question of questions) {
  console.log('='.repeat(80));
  console.log(`问题:${question}`);
  console.log('='.repeat(80));

  // 先将question 转换为向量
  // 再通过向量搜索,cosine 找到最相似的文档
  const retrievedDocs = await retriever.invoke(question);
  console.log(retrievedDocs);

  const storeResult = await vectorStore.similaritySearchWithScore(question, 3);
  console.log(storeResult);
  console.log(`\n [检索到文档及相似度评分]`);
  retrievedDocs.forEach((doc, i) => {
    const scoreResult = storeResult.find(([scoreDoc]) => {
      return scoreDoc.pageContent === doc.pageContent;
    });
    const score = scoreResult ? scoreResult[1] : null;
    const similarity = score ? (1 - score).toFixed(2) : "N/A";
    console.log(`\n 文档 ${i + 1} 相似度: ${similarity}`);
    console.log(`文档内容:${doc.pageContent}`);
    console.log(`文档元数据:${JSON.stringify(doc.metadata)}`);
  });

  const context = retrievedDocs.map((doc, i) => 
    `[片段${i+1}]\n ${doc.pageContent}`)
  .join("\n\n---\n\n");

  const prompt = `
    你是一个讲友情故事的老师。
    基于以下故事片段回答问题,用温暖生动的语言。
    如果故事中没有提及,就说"这个故事里没有提到这个细节"。

    故事片段:
    ${context}

    问题是:
    ${question}

    老师的回答:

  `;
  
  console.log(`\n [AI 回答]`);
  const response = await model.invoke(prompt);
  console.log(response.content);
  console.log("\n");
}

代码解析

  • 环境配置:使用dotenv加载API Key和模型名。temperature=0确保输出确定性。
  • 嵌入模型:OpenAIEmbeddings将文本转为向量。
  • 文档构建:每个Document包含pageContent(文本)和metadata(元数据,如章节、心情)。
  • 向量存储:MemoryVectorStore.fromDocuments创建内存数据库,适合小规模测试。
  • 检索器:asRetriever({k:2})返回Top-2相似文档。
  • 相似度计算:similaritySearchWithScore返回文档及分数(距离,1-距离为相似度)。
  • 增强提示:将检索片段插入prompt,指导LLM以"老师"身份回答。
  • 生成响应:model.invoke调用LLM。

运行示例:查询"光光和东东是怎么成为朋友的",检索器找到相关片段(如章节2和3),相似度约0.85-0.90。增强后,LLM输出如:"光光和东东从幼儿园就认识了,他们性格不同却一起度过了无数快乐时光,成为了最好的朋友。"

注意代码潜在优化:MemoryVectorStore仅内存使用,生产环境建议Faiss或Pinecone等持久化数据库。相似度阈值可添加,若低于0.7,返回"未知"。

RAG的优势与应用场景

RAG的优势在于:

  • 减少幻觉:外部知识验证响应。
  • 实时更新:知识库可动态添加,无需重训LLM。
  • 隐私保护:适用于企业内部数据。
  • 多模态扩展:支持图像、音频嵌入。

应用场景:

  • 问答系统:如客服Bot,检索产品手册。
  • 内容生成:写作助手,检索参考文献。
  • 教育工具:如故事讲述,基于片段生成续写。

挑战:嵌入质量依赖模型,知识库规模大时需优化检索效率。

进阶:优化RAG系统

  • 切片策略:大文档用固定大小或语义切片。
  • 混合检索:结合关键词+向量。
  • 重排序:检索后用LLM rerank结果。
  • 评估:用BLEU分数或人工评估响应质量。

例如,在代码中,可增加阈值判断:if(similarity < 0.7) return "未知"。

结语:RAG的未来展望

RAG作为LLM的强大补充,正推动AI向更可靠方向发展。通过向量语义搜索和提示增强,它不仅解决了幻觉,还开启了知识驱动的生成时代。读者可基于本文代码实验,探索RAG在实际项目中的潜力。

相关推荐
阿聪谈架构1 小时前
第03章:LCEL 链式调用 —— 让 AI 任务像流水线一样运转
人工智能
chaors1 小时前
从零学RAG0x04向量检索算法初探
人工智能·程序员·ai编程
chaors1 小时前
Langchain入门到精通0x01:结果解析器
人工智能·langchain·ai编程
龙国浪子1 小时前
从「选中一段」到「整章润色」:编辑器里的 AI 润色是怎么做出来的
前端·人工智能
gustt1 小时前
LangChain中的RAG Loader:从网页加载文档并实现智能分割与检索
人工智能·langchain·llm
一只叁木Meow1 小时前
Skills:让通用 AI 秒变"领域专家"
vue.js·人工智能
游魂Andy2 小时前
零成本搭建专属AI助手:OpenClaw永久免费部署全攻略
前端·人工智能·ai编程
IT_陈寒2 小时前
Vite凭什么比Webpack快10倍?5个核心优化原理大揭秘
前端·人工智能·后端
Baihai_IDP3 小时前
为什么 AI 巨头们放弃私有壁垒,争相拥抱 Agent Skills
人工智能·面试·llm