别再信它“一本正经地胡说”了!用 RAG终结大模型“幻觉”

别再信它"一本正经地胡说"了!用 RAG终结大模型"幻觉"

最近,很多人都在吐槽大模型(LLM)的一个致命毛病: "大模型幻觉"

你问它:"昨天马斯克发了什么推特?" 它可能自信满满地编造一条从未存在过的推文。 你问它:"我们公司去年的内部财报数据是多少?" 它可能直接胡诌一个数字,因为它压根没学过你们公司的内部文件。

这就是大模型幻觉(Hallucination) :当遇到它训练数据里没有的知识,它不会说"我不知道",而是倾向于"认认真真地胡说八道"。llm的知识来源于训练的时候会给它数据集,本质还是AIGC。

如何给这个爱"做梦"的天才戴上"紧箍咒"?答案就是目前 AI 界最火的解决方案:RAG(检索增强生成)

今天,我们不仅讲原理,还要直接上代码 。我们将使用 LangChain 框架,通过一段完整的 TypeScript 代码,手把手带你实现一个能"查资料、说真话"的 RAG 系统。


一、核心逻辑:从"闭卷"到"开卷"

如果把大模型比作一个博闻强记但无法翻书的天才学生

  • 没有 RAG(闭卷) :遇到没学过的题,只能靠猜 <math xmlns="http://www.w3.org/1998/Math/MathML"> → \rightarrow </math>→ 产生幻觉
  • 有了 RAG(开卷) :在回答前,先去查阅指定的资料库(知识库),找到依据后再作答 <math xmlns="http://www.w3.org/1998/Math/MathML"> → \rightarrow </math>→ 事实准确

RAG (Retrieval-Augmented Generation) 的流程可以概括为三步:

  1. 检索 (Retrieval) :去知识库里找和问题最相关的片段。
  2. 增强 (Augment) :把找到的片段塞进提示词(Prompt)里。
  3. 生成 (Generation) :让大模型基于这些片段回答问题。

下面,我们通过代码一步步拆解这个过程。


二、代码实战:构建你的第一个 RAG 系统

我们将使用 Node.js 和 LangChain 库来实现。假设我们有一个关于"光光和东东"的友情故事库,我们要解决大模型不知道这个故事细节的问题。

1. 准备环境与大模型

首先,我们需要初始化大模型和嵌入模型(Embeddings)。嵌入模型的作用是将文字转化为计算机能理解的"向量"(一串数字)。

arduino 复制代码
import "dotenv/config";
import { ChatOpenAI, OpenAIEmbeddings } from "@langchain/openai";
import { Document } from "@langchain/core/documents";
import { MemoryVectorStore } from "@langchain/classic/vectorstores/memory";

// 初始化大模型 (负责最后的回答)
const model = new ChatOpenAI({
    modelName: process.env.MODEL_NAME, // 例如 "gpt-4o"
    apiKey: process.env.OPENAI_API_KEY,
    temperature: 0, // 温度设为0,让回答更稳定,减少随机性
});

// 初始化嵌入模型 (负责将文字转为向量,用于搜索)
const embeddings = new OpenAIEmbeddings({
    apiKey: process.env.OPENAI_API_KEY,
    model: process.env.EMBEDDING_MODEL_NAME, // 例如 "text-embedding-3-small"
});

💡 代码解读 :这里我们设置了 temperature: 0。在 RAG 场景中,我们希望模型严格基于事实回答,不需要它发挥创意,所以要把随机性降到最低。

2. 构建知识库:切片与向量化

这是 RAG 最关键的一步。我们需要把故事变成"向量数据库"。 在代码中,我们手动创建了几个 Document 对象,每个对象包含内容 (pageContent)元数据 (metadata)

go 复制代码
const documents = [
    new Document({
        pageContent: `光光是一个活泼开朗的小男孩...他特别擅长踢足球...`,
        metadata: { chapter: 1, character: "光光", type: "角色介绍" },
    }),
    new Document({
        pageContent: `东东是光光最好的朋友...东东喜欢读书和画画...`,
        metadata: { chapter: 2, character: "东东", type: "角色介绍" },
    }),
    // ... (此处省略中间章节,实际应用中可以是成千上万个文档)
    new Document({
        pageContent: `比赛那天...东东传出了一个漂亮的球,光光接球后射门得分!...`,
        metadata: { chapter: 5, character: "光光和东东", type: "高潮转折" },
    }),
];

// 【核心步骤】从文档创建内存向量数据库
// 这行代码会自动调用 embeddings 模型,把所有文档内容转换成向量并存起来
const vectorStore = await MemoryVectorStore.fromDocuments(
    documents,
    embeddings
);

💡 原理揭秘 : 当你运行 fromDocuments 时,程序背后在做这件事:

  1. 把"光光擅长踢足球"这句话发给嵌入模型。
  2. 嵌入模型返回类似 [0.9, 0.2, -0.5, ...] 的向量。
  3. 把这个向量和原文存在一起。 这样,计算机就拥有了对这段文字的"语义理解"。

3. 检索 (Retrieval):像侦探一样找线索

现在,用户提出了问题: "光光和东东各自擅长什么?" 如果不加干预,大模型可能会瞎编。但在 RAG 中,我们先不让大模型回答,而是让检索器去数据库里找答案。

javascript 复制代码
// 创建检索器,设置 k=2,表示只找回最相关的 2 个片段
const retriever = vectorStore.asRetriever({ k: 2 });

const question = "光光和东东各自擅长什么?";

// 【核心步骤】执行检索
// 系统会将 question 也转为向量,然后计算它与库中所有向量的"距离" (余弦相似度)
const retrievedDocs = await retriever.invoke(question);

console.log("\n [检索到的文档及相似度评分]");
retrievedDocs.forEach((doc, i) => {
    console.log(`\n 文档 ${i+1}:`);
    console.log(`内容: ${doc.pageContent}`);
    console.log(`来源章节: ${doc.metadata.chapter}`);
});

💡 为什么能搜到? 哪怕你的问题里用的是"擅长",而原文里用的是"喜欢"或"特别会",传统的关键词搜索(Ctrl+F)会失效。 但向量搜索懂语义!在向量空间里,"擅长"和"喜欢"的距离非常近。所以,即使字面不匹配,系统也能精准地把"光光踢足球"和"东东画画"这两段最相关的内容捞出来。

4. 增强与生成 (Augment & Generation):带着证据答题

最后一步,我们把检索到的片段用户的问题拼在一起,构造一个新的 Prompt,发给大模型。

javascript 复制代码
// 将检索到的文档内容拼接成上下文字符串
const context = retrievedDocs
    .map((doc, i) => `[片段 ${i+1}] ${doc.pageContent}`)
    .join("\n\n----\n\n");

// 【核心步骤】构造增强后的 Prompt
const prompt = `
你是一个讲友情故事的老师。
请严格基于以下【故事片段】回答问题。
如果故事片段中没有提及相关信息,请直接回答"这个故事里没有提到这个细节",严禁编造!

故事片段:
${context}

问题:
${question}

老师的回答:
`;

// 让大模型基于增强后的 Prompt 生成回答
const response = await model.invoke(prompt);
console.log("\n [AI 回答]:");
console.log(response.content);

💡 幻觉是如何消失的? 注意看 Prompt 里的指令: "请严格基于以下【故事片段】回答...严禁编造" 。 此时的大模型,手里拿着我们刚刚检索出来的"小抄"(Context):

  • 片段1:光光特别擅长踢足球...
  • 片段2:东东喜欢读书和画画...

它不需要动用训练数据里的记忆,只需要做简单的阅读理解。它的答案被牢牢限制在了我们提供的片段范围内,从而彻底杜绝了幻觉。


三、完整运行效果预览

当你运行这段代码时,控制台会输出类似这样的结果:

markdown 复制代码
================================================================================
问题:光光和东东各自擅长什么?
================================================================================

 [检索到的文档及相似度评分]
 文档 1:
 内容: 光光是一个活泼开朗的小男孩...他特别擅长踢足球...
 来源章节: 1
 
 文档 2:
 内容: 东东是光光最好的朋友...东东喜欢读书和画画...
 来源章节: 2

 [AI 回答]:
 在这个温暖的故事里,光光非常擅长踢足球,他在球场上奔跑时就像一道阳光;而东东则擅长读书和画画,他的画作总是充满了无限的想象力。他们虽然特长不同,却是彼此最好的朋友!

看!回答精准、有据可依,而且完全没有胡编乱造。


四、总结:代码背后的哲学

通过这段代码,我们实现了 RAG 的核心闭环:

  1. 数据私有化 :我们的故事片段(documents 数组)可以替换成任何企业内部的 PDF、Word 或数据库记录。这些数据不需要重新训练大模型,只需存入向量库。
  2. 语义理解 :利用 OpenAIEmbeddingsMemoryVectorStore,我们超越了关键词匹配,实现了真正的"懂你所问"。
  3. 可控生成 :通过动态拼接 prompt,我们强行将大模型的注意力聚焦在检索到的事实上,用上下文(Context)约束了幻觉(Hallucination)

RAG 不仅仅是一项技术,它是大模型从"玩具"走向"工具"的关键桥梁。 它告诉我们要相信事实,而不是概率。下次当你需要构建一个靠谱的企业 AI 助手时,记得加上这段"检索增强"的代码,让你的 AI 从此不再"胡说八道"!

相关推荐
悟空码字2 小时前
SpringBoot + 腾讯地图实战:打造全能型地理位置服务平台,开箱即用!
java·spring boot·后端
martinzh2 小时前
AI 再也不用截图点点点了!用一行命令让它直接画流程图
后端
顺风尿一寸2 小时前
Spring事务回滚探秘:从@Transactional到数据库连接的完整旅程
java·后端
前端付豪2 小时前
练习单导出
前端·python·llm
雨夜之寂2 小时前
能动手才推 · AI · 03/14
后端
Cache技术分享2 小时前
351. Java IO API - Java 文件操作:java.io.File 与 java.nio.file 功能对比 - 3
前端·后端
QZQ541882 小时前
redis分布式锁相关思考
后端
彭于晏Yan2 小时前
SpringBoot如何调用节假日API
java·spring boot·后端
我爱娃哈哈2 小时前
Spring AI Alibaba 教程:集成阿里云大模型服务实战
后端