不知道你在调戏各种 AI 大模型的时候,有没有遇到过以下经典翻车现场:
你 :"请问我们公司昨天新办的报销流程是什么?" AI:(一本正经地胡说八道)"根据规定,您需要先骑上一只尖叫鸡,然后提交一份由火星办事处盖章的纸质申请......"
这种现象在人工智能领域有个专门的名字,叫"幻觉"(Hallucination)。
今天我们就来扒一扒,为什么聪明的大模型会变成"糊涂蛋"?而我们程序员又是如何用一项叫做 RAG 的技术,在不花大钱"微调"(Fine-tune)模型的情况下,让 AI 乖乖听话、有据可依的。
1. 大模型的"打工人痛点":为什么它会胡说八道?
要理解大模型为什么会产生幻觉,我们得先明白它是怎么被塞满知识的。
核心症结:数据集的局限性
大模型(LLM)所有能回答出来的知识,完全取决于它在出厂训练时吃进去的数据集。这就导致了两个致命的知识盲区:
- 时效性差:如果模型是 2025 年底训练完成的,你问它 2026 年最新发生的科技新闻,它绝对一问三不知。
- 私密性孤岛:你企业内部的私有文档、你个人的秘密笔记,公网上的大模型是绝对接触不到的。
致命性格:宁可毒死你,也不愿意"交白卷"
如果是一个诚实的打工人,不知道往往会说"对不起,我没学过"。但大模型不是。它本质上是一个"概率超级Next-Token预测机",它的天职就是根据你上一句话,猜出下一句话最可能是什么。
当你问它它不知道的事情时,它为了完成"接话"的使命,就会开始根据已有的字词概率生搬硬套,胡乱回答。这就是幻觉。
2. 拯救大模型的两条路:微调 vs RAG
既然大模型有知识盲区,我们怎么给它"补课"呢?行业里通常有两种做法。
方案 A:微调(Fine-tuning)
简单来说,就是把你的私有文档打碎成训练格式,把大模型送回工厂"重新深造"。
- 缺点:成本高得吓人!不仅需要昂贵的显卡(GPU)算力,而且每次你的私有文档一更新(比如公司昨天改了报销制度),你就得重新训练一次。这谁顶得住?
方案 B:给它一本"开卷考试"的参考书(RAG)
既然重回学校太贵,那我们直接在它考试(回答问题)的时候,把小抄......哦不,把参考答案塞到它手里不就行了?
当用户提出一个问题时,我们先去内部的"知识库"里把相关的文章查出来,然后把这些文章和用户的问题一起打包成一个超长的提示词(Prompt)喂给大模型:
"请结合以下参考资料,回答用户的问题。如果资料里没写,就老实说不知道。 【参考资料:公司2026年最新报销流程.pdf】 用户问题:怎么报销?"
大模型有了底气,看着参考资料作答,幻觉自然就消失了。这种技术,就叫做 RAG。
3. 拆解 RAG:硬核概念的白话文翻译
RAG 是三个英文单词的缩写:Retrieval-Augmented Generation ,翻译过来叫 检索增强生成。
我们把它拆成三步来理解:
scss
[用户提问] ──> 1. Retrieval (检索相关文档) ──> 2. Augmented (塞入Prompt增强) ──> 3. Generation (大模型生成回答)
- Retrieval(检索) :去你的企业知识库或者个人文档里,把和用户问题相关的文档片段(可以是一个自然段、一页 PDF 或者一个章节)给找出来。
- Augmented(增强) :把找出来的这堆文档片段,死缠烂打地拼接进原始的 Prompt 里面。这时候的 Prompt 变得非常丰满,这就叫增强。
- Generation(生成) :大模型阅读了这些背景知识后,吐出最终的、靠谱的回答。
4. 检索的演进:从"死板匹配"到"灵魂共鸣"
RAG 的核心瓶颈在于第一步:你凭什么能把最相关的文档片段准确地查出来?
传统做法:关键词搜索(Lexical Search)
早期的搜索像老学究,玩的是文本硬匹配。比如你搜"西红柿",它能帮你找到含有"西红柿"的文档。但如果你搜"番茄",它可能就懵圈了,因为它觉得这两个词的长相完全不一样。这种文本匹配在复杂的语义面前,可以说是"不太行"。
现代做法:向量(Vector)语义查询
为了让计算机理解"番茄"和"西红柿"是同一种东西,科学家引入了多维向量。
什么叫向量?你可以把它想象成给每个词语或者句子打上各式各样的"属性标签标签"。 为了方便理解,我们假设世界上只有两个属性指标: "食用性" (0表示不能吃,1表示极好吃)和"硬度"(0表示液体,1表示极硬)。 那么,我们可以用一个包含两个数字的数学列表(也就是二维向量)来表达它们的特征:
- 水果 :
[0.9, 0.3](能吃,偏软) - 苹果 :
[0.9, 0.5](能吃,比一般水果硬点) - 香蕉 :
[0.4, 0.1](能吃,非常软) - 石头 :
[0.1, 0.9](不能吃,硬邦邦)
在由这两个坐标轴组成的数学空间里,"水果"、"苹果"和"香蕉"因为属性接近,它们对应的点离得非常近。如果从原点向它们分别画一条线,这些线之间的夹角就会非常小。通过计算夹角的余弦值(余弦相似度),计算机就能一秒看出:"哦!苹果和水果相关性很大,而石头和水果完全不搭边!"
在实际应用中,我们会用成百上千个维度(比如几百上千个数字组成的超长列表)来表达极其复杂的丰富语义。不管是文本、图片、PDF 还是语音,都能被抹平转换成这一串串数字向量,实现真正的"语义搜索"。
而负责把大白话和各种文档转换成这种高维向量的专用模型,就叫做嵌入模型(Embedding Model) 。它通常比生成模型(Generation Model)要便宜得多。
5. RAG 的落地实现:向量数据库
既然文档变成了向量,我们普通的 MySQL 数据库可就犯难了。MySQL 擅长找 id = 1 或者 name = '张三' 的数据,你要是让它在几百万条几百维的数字列表里找出"哪个向量和当前问题的向量夹角最小",它会直接cpu烧干。
于是,向量数据库(Vector Database)应运而生。它专门用来做两件事:
- 高效存储海量的多维向量数据(持久化存储)。
- 提供极快的"相似度检索"算法,能在电光石火间找出语义最接近的文本块。
6. 代码实战:搭建你的第一个 RAG 脚手架
概念听够了,我们来点真的。我们将使用大名鼎鼎的 AI 编排框架 LangChain 逐步搭建一个 RAG 基础工程。
步骤一:环境配置与依赖引入
首先,我们需要引入环境变量,并拉取 LangChain 中专门对接 OpenAI 模型的工具。同时,我们引入 Document 对象,它是 LangChain 内部用来封装每一段文本块的基础数据结构。
JavaScript
javascript
import 'dotenv/config'
import { ChatOpenAI, OpenAIEmbeddings } from 'langchain/openai'
// Document 是 LangChain 中表示文档片段的核心类,可以把 txt、js、pdf 切成有一定大小的段落
import { Document } from 'langchain/core/documents'
步骤二:初始化模型实例
接下来,我们实例化两辆核心马车:一辆是用来生成最终回答的 ChatOpenAI 聊天模型,另一辆是负责把文本变成数学向量的 OpenAIEmbeddings 嵌入模型。
为了保证回答的严谨性,我们将聊天模型的 temperature(随机性)设置为 0,让它老老实实根据资料回答,绝不瞎发挥。
JavaScript
arduino
// 1. 初始化聊天生成模型(Generation)
const model = new ChatOpenAI({
temperature: 0, // 设置为0,尽可能让其严谨,不胡乱发挥
model: process.env.MODEL_NAME,
apiKey: process.env.OPENAI_API_KEY,
configuration: {
baseURL: process.env.OPENAI_API_BASE, // 这里的配置基址确保能正确连接到 API 服务
}
})
// 2. 初始化嵌入模型(Embedding),用于将文本向量化
const embeddings = new OpenAIEmbeddings({
apiKey: process.env.OPENAI_API_KEY,
model: process.env.OPENAI_EMBEDDINGS_MODEL,
configuration: {
baseURL: process.env.OPENAI_BASE_URL,
}
})
步骤三:模拟知识库构建与向量化
有了工具,我们手写两个文档片段来模拟我们的私有"知识库"。一个是关于公司最新报销流程的,另一个是毫不相关的冷知识。我们尝试使用 embeddings 将它们转化成多维数字向量。
JavaScript
ini
// 3. 准备知识库里的文档片段(已经过切分处理)
const doc1 = new Document({ pageContent: "2026年新规定:公司差旅报销必须在行程结束后3个工作日内提交,且需附带电子行程单。" });
const doc2 = new Document({ pageContent: "世界冷知识:大熊猫每天花12个小时吃竹子。" });
// 4. 将这些文本转为多维向量(以 doc1 为例)
const vectorSample = await embeddings.embedQuery(doc1.pageContent);
console.log("文本成功向量化!前5个维度的数字为:", vectorSample.slice(0, 5));
// 输出看起来会是这样:[ 0.01234, -0.05678, 0.09101, -0.0023, 0.0456 ]
步骤四:检索与最终生成(构建 RAG 闭环)
现在,用户提问了:"差旅报销要在几天内提交?"
完整的 RAG 逻辑如下:在没有向量数据库的简化场景下,我们通过代码模拟这个检索、拼接并交给大模型回答的闭环:
JavaScript
javascript
// 5. 用户提问
const userQuery = "差旅报销要在几天内提交?";
// 6. 【检索阶段 (Retrieval)】:
// 在真实场景中,我们会将 userQuery 向量化后去向量数据库做相似度查询。
// 这里我们肉眼可见 doc1 与问题最相关,直接将其作为检索出来的背景背景材料。
const retrievedContext = doc1.pageContent;
// 7. 【增强阶段 (Augmented)】:拼装带背景知识的超强 Prompt
const augmentedPrompt = `
你是一个诚实的助手。请严格根据以下给出的背景知识回答用户的问题。如果背景知识中没有提到,请回答"不知道"。
背景知识:
${retrievedContext}
用户问题:${userQuery}
`;
// 8. 【生成阶段 (Generation)】:让大模型根据小抄作答
const response = await model.invoke(augmentedPrompt);
console.log("=== AI的终审回答 ===");
console.log(response.content);
// 输出:2026年新规定指出,公司差旅报销必须在行程结束后3个工作日内提交。
7. 总结:RAG 真正落地需要突破的难关
虽然上面的代码跑通了,但在真实的工程落地中,还有几个骨头极难啃的技术突破点:
- 文档怎么切分(Chunking) :一个 100 页的 PDF,你不能整本塞进去(大模型吃不下,而且很贵)。你得把它切成一个个片段。但怎么切才能保证语义不被打断?按字数硬切?还是按自然段落、章节、页码切?这是个技术活。
- 检索的精准度(Retrieval Quality) :怎么保证向量数据库挑出来的文档片段真的完美命中了用户的意图,而不是找了一堆干扰项?
- 多模态融合:如果你的文档里有复杂的架构图、表格、流程图,怎么把它们也顺利地向量化并让大模型看懂?
不过别慌,万丈高楼平地起。理解了"向量化"和"检索-增强-生成"的底层逻辑,你已经超越了绝大多数只会给大模型发简单提示词的普通玩家了。接下来的文章里,我们将深入探讨如何高效切分文档以及向量数据库的持久化实战,敬请期待!