别再信它"一本正经地胡说"了!用 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) 的流程可以概括为三步:
- 检索 (Retrieval) :去知识库里找和问题最相关的片段。
- 增强 (Augment) :把找到的片段塞进提示词(Prompt)里。
- 生成 (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时,程序背后在做这件事:
- 把"光光擅长踢足球"这句话发给嵌入模型。
- 嵌入模型返回类似
[0.9, 0.2, -0.5, ...]的向量。- 把这个向量和原文存在一起。 这样,计算机就拥有了对这段文字的"语义理解"。
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 的核心闭环:
- 数据私有化 :我们的故事片段(
documents数组)可以替换成任何企业内部的 PDF、Word 或数据库记录。这些数据不需要重新训练大模型,只需存入向量库。 - 语义理解 :利用
OpenAIEmbeddings和MemoryVectorStore,我们超越了关键词匹配,实现了真正的"懂你所问"。 - 可控生成 :通过动态拼接
prompt,我们强行将大模型的注意力聚焦在检索到的事实上,用上下文(Context)约束了幻觉(Hallucination) 。
RAG 不仅仅是一项技术,它是大模型从"玩具"走向"工具"的关键桥梁。 它告诉我们要相信事实,而不是概率。下次当你需要构建一个靠谱的企业 AI 助手时,记得加上这段"检索增强"的代码,让你的 AI 从此不再"胡说八道"!