大型语言模型(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的流程如下:
- 检索(Retrieval) :将用户查询嵌入向量,与知识库中的预嵌入文档比较,找出最相似的片段。
- 增强(Augmentation) :将检索到的文档片段插入原始提示中,形成增强提示。
- 生成(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在实际项目中的潜力。