写在前面:今天学 RAG(检索增强生成),老师说了一句话让我醍醐灌顶------"大模型不会说'我不知道',它会胡乱回答。" 你问他昨天世界杯谁赢了,它不知道(知识截止到训练时间),但它不会说"我不知道啊",它会编一个答案给你。而且编得还像模像样------这就是传说中的"AI 幻觉"。RAG 就是解决这个问题的:别让你的 AI 瞎编,先让它去查资料,查到了再回答。
一、AI 的"不懂装懂"------幻觉从哪来?
1.1 AI 的知识不是"活的"
老师说:
"大模型知道的知识,取决于训练时给它的数据集。如果你问它最近发生的事情,或者你企业内部私有的文档,LLM 是不知道的。"
但问题在于------LLM 不会说"我不知道"。
它会基于自己有限的训练知识,强行编一个听起来合理的答案 。这就是所谓的幻觉(Hallucination)。
你问它:"2024 年巴黎奥运会中国队拿了多少金牌?" 如果它的训练数据截止到 2023 年,它不知道。但它不会说"我不知道",它会说:"中国队获得了 38 枚金牌......"------这就是编的。
1.2 解决幻觉的三种方案
老师说:
"如何解决大模型的幻觉?想一下。"
| 方案 | 成本 | 说明 |
|---|---|---|
| 微调(Fine-tune) | 高 | 大公司、专业领域用,需要算力和数据 |
| RAG(检索增强生成) | 低 | 先查知识库,再回答 |
| 重新训练 | 非常高 | 几乎没人为了新知识重训大模型 |
RAG 是性价比最高的方案。
二、RAG 不是魔法,是"先查后答"
2.1 RAG 是什么?
老师说:
"RAG:Retrieval(检索)--- Augmented(增强)--- Generation(生成)。"
用户提问
↓
【Retrieval 检索】去知识库里找相关的文档片段
↓
【Augmented 增强】把找出来的内容加到 Prompt 里
↓
【Generation 生成】LLM 根据增强后的上下文回答问题
核心思想:在给 LLM 发 Prompt 之前,先去查一下知识库,把相关文档作为背景知识塞进去,再让 LLM 回答。
2.2 为什么不用关键词搜索?
老师说:
"关键词搜索------文本匹配,不太行。向量(Vector)语义查询就可以。"
举个例子:
| 关键词 | 用 LIKE 搜索 | 用向量搜索 |
|---|---|---|
| "光光" | 只匹配含"光光"的段落 | 即使搜"朋友",也能找到光光相关的段 |
| "足球比赛" | 只匹配含"足球比赛"的段落 | 搜"运动"也能找到足球相关内容 |
向量搜索找的是"意思",关键词搜索找的是"字"。
2.3 向量的"数学魔法"
老师画了一个很直观的例子:
css
水果向量:[0.9, 0.3]
苹果向量:[0.9, 0.5] ← 和水果很近
香蕉向量:[0.9, 0.1] ← 和水果也比较近
石头向量:[0.1, 0.9] ← 和水果很远
每个词都可以表示为一个多维向量。 语义相近的词,在向量空间中的夹角就小(余弦相似度高)。语义无关的词,夹角就大。
所以 RAG 一般用**嵌入模型(Embedding Model)**来做语义搜索,比关键词搜索精准得多。
三、LangChain 实现 RAG:一个完整的故事
3.1 准备知识库文档
看 index.mjs,老师用了一个"光光和东东"的小故事:
javascript
const documents = [
new Document({
pageContent: `光光是一个活泼开朗的小男孩,他有一双明亮的大眼睛,总是带着灿烂的笑容。
光光最喜欢的事情就是和朋友们一起玩耍,他特别擅长踢足球......`,
metadata: {
chapter: 1,
character: "光光",
type: "角色介绍",
mood: "活泼"
},
}),
new Document({
pageContent: `东东是光光最好的朋友,他是一个安静而聪明的男孩。
东东喜欢读书和画画,他的画总是充满了想象力......`,
metadata: {
chapter: 2,
character: "东东",
type: "角色介绍",
mood: "温馨"
},
}),
// ......一共 7 个片段
];
Document 是 LangChain 中 Embedding 的最小单元:
| 字段 | 作用 | 比喻 |
|---|---|---|
| pageContent | 要向量化的文本内容 | 一段话 |
| metadata | 附加信息,不参与向量化 | 标签(章节、角色、类型) |
3.2 把文档向量化,存入内存向量库
javascript
const embeddings = new OpenAIEmbeddings({
model: process.env.EMDEDDING_MODEL_NAME,
});
const vectorStore = await MemoryVectorStore
.fromDocuments(documents, embeddings);
console.log(`向量库创建成功,共 ${vectorStore.memoryVectors.length} 条文档已向量化`);
MemoryVectorStore 是 LangChain 提供的内存向量存储。 把文档通过 Embedding 模型转成向量,存到内存里。
老师说:
"简单就放内存,复杂就放向量数据库(如 PostgreSQL + pgvector)。"
3.3 创建检索器
javascript
const retriever = vectorStore.asRetriever({
k: 3 // 返回最相似的 3 条
});
检索器(Retriever)是一个标准入口:
知识库 → 文档 → Document → Embedding → MemoryVectorStore → Retriever
3.4 查询并获取相关文档
javascript
const question = "东东和光光是怎么成为朋友的?";
const docs = await retriever.invoke(question);
const scoredResults =
await vectorStore.similaritySearchWithScore(question, 3);
输出的结果包含相似度评分:
ini
[文档 1] 相似度: 0.9234
内容: 东东是光光最好的朋友,他是一个安静而聪明的男孩...
元数据:章节=2, 角色=东东, 类型=角色介绍
[文档 2] 相似度: 0.8876
内容: 有一天,学校要举办一场足球比赛...
元数据:章节=3, 角色=光光和东东, 类型=友情情节
[文档 3] 相似度: 0.7543
内容: 接下来的日子里,光光每天放学后都会教东东踢足球...
元数据:章节=4, 角色=光光和东东, 类型=友情情节
相似度越接近 1,说明和问题越相关。
3.5 增强 Prompt,生成回答
javascript
const context = docs
.map((doc, i) => `[片段${i}]\n ${doc.pageContent}`)
.join("\n\n-----\n\n");
const prompt = `你是一个讲友情故事的老师。
基于以下故事片段回答问题,用温暖生动的语言。
故事片段:
${context}
问题:${question}
老师的回答:`;
const response = await model.invoke(prompt);
检索到的文档片段被塞进了 Prompt 的"故事片段"部分,LLM 基于这个增强后的上下文来回答问题。
四、RAG 的完整流程回顾
arduino
用户提问:"东东和光光是怎么成为朋友的?"
↓ 1. 检索(Retrieval)
把问题转成向量 → 在向量库中搜索 → 找到最相似的 3 个片段
↓ 2. 增强(Augmented)
把 3 个片段组装成 Prompt 的"故事片段"部分
↓ 3. 生成(Generation)
LLM 基于故事片段 + 问题,生成回答
↓ 输出
"东东和光光是从幼儿园就认识的好朋友......
光光邀请东东一起参加足球比赛,虽然东东不会踢,
但光光耐心地教他,他们的友谊因此更加深厚了......"
如果没有 RAG,LLM 只能基于自己的训练知识回答。但如果这个故事不在训练数据里,LLM 就会"编"。
有了 RAG,LLM 先查知识库,基于真实的内容回答------幻觉就大大减少了。
五、RAG 在实际项目中的架构
老师说:
"RAG 要实现语义查询,需要基于向量来做,把文档向量化存储到向量数据库,查询的时候也把 Prompt 向量化,去数据库中做相似度检索,可以找到语义相近的文本块。"
生产环境的 RAG 架构:
原始文档(PDF、Word、网页等)
↓ 文本提取 + 分片
文本片段
↓ Embedding 模型
向量
↓ 存储
向量数据库(PostgreSQL+pgvector、Pinecone、Weaviate)
↓ 创建
检索器(Retriever)
↓ 查询
用户问题 → 向量化 → 相似度检索 → 相关片段 → 增强 Prompt → LLM 生成
| 组件 | 作用 | 我们用的什么 |
|---|---|---|
| 文档分片 | 把长文档切成有语义的段落 | 手动切分 7 个片段 |
| Embedding 模型 | 把文本转成向量 | OpenAIEmbeddings |
| 向量存储 | 存向量,支持相似度搜索 | MemoryVectorStore |
| 检索器 | 标准入口,输入问题输出文档 | vectorStore.asRetriever() |
六、总结:RAG 是 LLM 的"外挂知识库"
| 概念 | 说明 |
|---|---|
| LLM 幻觉 | LLM 不懂也不会说不知道,会强行编答案 |
| RAG | Retrieval Augmented Generation,检索增强生成 |
| 检索 | 从知识库中找到和问题相关的文档片段 |
| 增强 | 把片段加入 Prompt 上下文 |
| 生成 | LLM 基于增强后的上下文回答 |
| 向量搜索 | 用 Embedding 找语义相似的文本 |
| Document | LangChain 中 Embedding 的最小单元 |
| MemoryVectorStore | 内存向量存储,轻量级方案 |
| 检索器 | retriever.invoke(question) 标准入口 |
RAG 的核心思想:不要让你的 AI 空口白牙地编答案,先给它查资料,查到了再回答。
写在最后
今天最大的收获,是彻底理解了 RAG 的原理。以前觉得"AI 会编答案"是个玄学问题,现在知道了------LLM 不是故意骗你,它是真的不知道,但它"不好意思"说不知道。RAG 就是给 LLM 配了一个"秘书",遇到不懂的先去查资料,查到了再开口。
下次面试官问你:"RAG 的工作原理是什么?怎么用 LangChain 实现?"
你可以淡定地说:
"RAG 是 Retrieval Augmented Generation 的缩写。核心思想是:在给 LLM 发 Prompt 之前,先到知识库中检索相关文档。流程分三步:① 检索 ------把用户问题通过 Embedding 模型转成向量,在向量库中做相似度搜索,找到最相关的 N 个文档片段;② 增强 ------把检索到的片段作为上下文加入 Prompt;③ 生成 ------LLM 基于增强后的 Prompt 回答问题。在 LangChain 中,通过 Document 对象定义知识片段,MemoryVectorStore.fromDocuments() 创建向量库,vectorStore.asRetriever(k) 创建检索器,调用 retriever.invoke(question) 检索相关文档。这种方式有效解决了 LLM 的幻觉问题,让 AI 基于真实知识回答,而不是凭空编造。"
然后看着面试官满意的表情,心里默念:这波,又稳了。
本文所有代码示例均来自课堂学习资料,真实可运行。