大白话拆解 RAG:从零搭建一个“有背景知识”的智能问答系统 当大模型开始“胡说八道”我们该如何给它装上“外挂大脑”前言当 AI 开始“一本正经地胡说八道”你有没有遇到过这种情况兴致勃勃地跑去问大模型“昨天我们公司内部会议讨论了什么”结果它给你编了一个激情澎湃但完全没发生过的故事。这真不怪它。大模型就像一个超级学霸它的大脑里装满了训练时看过的书籍和资料。但对于训练后才发生的事情或者你公司内部的私有文档它确实是一无所知的。更麻烦的是它不会坦白说“我不知道”而是会基于已有的知识强行推理这就产生了所谓的“幻觉”。说白了就是你不知道的事它也不知道但不同的是它不知道还会硬答。今天我们就来聊聊如何用 RAG检索增强生成技术给大模型装上一个“外挂知识库”让它学会“先查资料再回答问题”。一、RAG 是什么简单到离谱RAG 的全称是Retrieval-Augmented Generation中文叫检索增强生成。名字虽然高大上但本质原理极其朴素就三步检索Retrieval当用户提问时先去知识库里“翻书”找到相关的文档片段。增强Augmented把这些找到的文档片段作为“背景知识”塞进 Prompt 里增强提示词。生成Generation大模型看着手里的“小抄”Prompt 里的背景知识给出最终的靠谱回答。你可以把它理解为开卷考试传统大模型是闭卷考不会的题就瞎蒙而 RAG 允许它带着参考书进考场答案自然准确得多。二、核心技术一语义检索告别“关键词”的尴尬既然要“翻书”怎么翻就很有讲究了。直接 CtrlF 关键词匹配那搜“苹果”可能出来一堆“苹果公司”的新闻而你想要的是水果“苹果”的营养价值。这里的主角是向量Vector和嵌入模型Embedding Model。1. 万物皆可“向量化”你可以把向量理解为一串用来描述事物特征的数字。比如描述水果我们有两个维度食用性0 到 1硬度0 到 1那么苹果 [0.9, 0.5]香蕉 [0.9, 0.1]石头 [0.1, 0.9]在数学上余弦值越小夹角越大相关性越低。显然苹果和香蕉的夹角更小它们更相似。而石头则被远远地甩在一边。在实际应用中我们使用专门的嵌入模型Embedding Model将文本、图片甚至语音转化为成百上千维的高维向量。这种向量包含了丰富的语义信息能够精准地捕捉“苹果是一种水果”和“香蕉是一种水果”这种深层的相关性远比关键词匹配更“懂”你的意思。2. 查询流程离线阶段把知识库里的所有文档分片通过 Embedding 模型变成向量存入向量数据库。在线阶段用户提问时同样把问题通过 Embedding 模型变成向量。在向量数据库里找出与问题向量最相似余弦夹角最小的前 K 个文档片段。金句如果说关键词搜索是“按图索骥”那向量检索就是“灵魂匹配”。三、核心技术二文档分片Chunking知识库里的文档动辄几十页你不能把整本书直接塞给大模型上下文窗口有限。所以需要分片。按章节逻辑清晰但章节太长不适合。按页简单粗暴但语义可能不完整。按段落最推荐。保证每一块内容都有完整的自然语义。比如下面代码中我们把“光光和东东”的故事按情节发展分成了 7 个 Document 片段每个片段都有独立的主题。四、实战手把手搭建一个极简 RAG 系统说了这么多理论不如直接看代码来得痛快。下面我们使用LangChain和OpenAI API或兼容接口基于我们准备的光光和东东的故事搭建一个最简 RAG。环境准备import dotenv/config; import { ChatOpenAI, // 负责生成回答的大模型 OpenAIEmbeddings, // 负责向量化的嵌入模型 } from langchain/openai; import { Document } from langchain/core/documents;第一步初始化模型这里同时初始化了两种模型一种是负责“说话”的ChatOpenAI一种是负责“理解语义”的OpenAIEmbeddings。// 1. 生成模型负责根据背景知识回答问题 const model new ChatOpenAI({ temperature: 0, // 温度设为 0让回答更保守、准确 model: process.env.MODEL_NAME, apiKey: process.env.OPENAI_API_KEY, configuration: { baseURL: process.env.OPENAI_BASE_URL, } }); // 2. 嵌入模型负责将文本变成向量价格比生成模型便宜很多 const embeddings new OpenAIEmbeddings({ apiKey: process.env.OPENAI_API_KEY, model: process.env.EMBEDDING_MODEL_NAME, configuration: { baseURL: process.env.OPENAI_BASE_URL, } });第二步构建知识库Document 列表这里我们把光光和东东的故事拆分成带有元数据Metadata的 Document。元数据虽然不参与向量计算但非常有用可以用来过滤或溯源。const documents [ new Document({ pageContent: 光光是一个活泼开朗的小男孩...略, metadata: { chapter: 1, character: 光光, type: 角色介绍 }, }), // ... 其余 6 个 Document ];第三步向量化存储与检索最核心逻辑以下代码是本次实战的灵魂所在。我们创建一个内存向量存储MemoryVectorStore将文档向量化。当用户提问时自动检索最相关的文档片段。import { MemoryVectorStore } from langchain/vectorstores/memory; // 1. 创建向量存储把 documents 通过 embeddings 模型向量化并存入内存 const vectorStore await MemoryVectorStore.fromDocuments(documents, embeddings); // 2. 用户提问 const query 光光和东东是怎么成为朋友的; // 3. 语义检索把 query 也向量化去数据库中找最相似的文档 // 这里的 2 表示返回最相关的 2 个片段 const relevantDocs await vectorStore.similaritySearch(query, 2); // 看看我们检索到了啥 console.log(检索到的相关片段); relevantDocs.forEach((doc, index) { console.log(\n片段 ${index 1}); console.log(内容${doc.pageContent}); console.log(来源第 ${doc.metadata.chapter} 章); });执行后你会发现即使问题里没有直接提到“足球比赛”等关键词向量检索依然能精准定位到描述他们相识和共同训练的那几个片段第 2、3 章这就是语义搜索的强大之处。第四步增强生成拿到检索到的文档后我们把它们拼接进 Prompt再让大模型回答。// 4. 构建增强 Prompt const context relevantDocs.map(doc doc.pageContent).join(\n); const prompt 请根据以下参考资料回答用户的问题。 如果参考资料中没有相关答案请坦诚地告诉用户你不知道。 参考资料 ${context} 用户问题${query} 请基于参考资料给出准确、详细的回答 ; // 5. 调用大模型生成最终答案 const response await model.invoke(prompt); console.log(\n最终回答\n${response.content});运行结果预期最终大模型会基于参考资料回答而不是瞎编。它会告诉你光光和东东是在幼儿园认识的性格互补通过一次足球比赛的经历光光帮助东东练习最终他们一起赢得了比赛友谊变得更加深厚。有没有发现如果没有 RAG大模型是不知道“光光和东东”这两个人物的。有了 RAG它就成了这个故事的专家。五、总结与避坑指南RAG 的核心在于“开卷”它完美解决了 LLM 无法获取私有数据或实时数据的问题是现阶段落地企业应用的最佳方案之一。分片是门手艺活分片太大检索不精准分片太小上下文不完整。一般建议按段落切分或在代码中通过RecursiveCharacterTextSplitter设置chunk_size和chunk_overlap重叠部分来保证语义连贯。模型各司其职生成模型贵负责推理回答嵌入模型便宜负责检索。它们分工明确千万不要用生成模型去做向量化浪费资源。检索质量决定回答质量如果向量检索没把相关文档找回来大模型再厉害也无能为力。这就是经典的“Garbage in Garbage out”。最后送大家一句话在未来大模型的能力会越来越趋同而真正决定应用上限的是你如何驾驭和利用外部知识的能力。希望这篇实战文章能帮你迈出坚实的第一步。