
前面讲上下文工程的时候,我们提到过一个问题:
大模型本身并不知道你的私有资料。
它不知道你公司的内部文档,不知道数据库里的最新记录,也不知道你本地项目里刚写完的代码。
如果只靠模型训练时学到的知识,它很容易出现两种情况:
- 不知道最新信息
- 一本正经地胡说
这就是为什么现在很多 AI 应用不会只把问题丢给大模型,而是会在回答前先去"找资料"。
这个过程,就是理解 RAG 的入口。
RAG 的全称是 Retrieval-Augmented Generation,中文一般叫"检索增强生成"。
拆开看就是三步:
text
Retrieval 检索
Augmented 增强
Generation 生成
一句话说:
RAG 不是让大模型凭空回答,而是先从外部资料里检索相关内容,再把这些内容作为上下文交给大模型生成答案。
为什么需要 RAG?
先看一个普通搜索问题。
假设我们有一批文章数据:
json
[
{
"title": "如何使用 Nuxt.js 进行服务器端渲染",
"category": "前端开发"
},
{
"title": "使用 Nest.js 和 TypeScript 构建一个简单的微服务应用",
"category": "后端开发"
},
{
"title": "如何在 Vue.js 中使用 Vuetify 实现 Material Design 风格",
"category": "前端开发"
}
]
如果用户搜索:
text
有哪些 Vue 相关的内容?
传统关键词搜索可以做:
sql
title like '%Vue%'
这当然能找到一部分结果。
但关键词搜索有一个明显问题:
它更关心"字面上有没有出现这个词",不是真的理解语义。
比如:
text
酸辣土豆丝的做法
和:
text
马铃薯怎么做?
这两个问题在字面上不一样,一个叫"土豆",一个叫"马铃薯"。
但人一看就知道它们语义接近。
传统关键词搜索可能会漏掉这种关系。
RAG 的第一步,就是先解决"怎么找到真正相关的资料"。
从关键词搜索到语义搜索

语义搜索的核心是 Embedding。
Embedding 可以理解成:
把一段文字转换成一组数字。
这组数字不是随便来的,它会尽量保留文字的语义信息。
比如:
text
Vue 组件库怎么做?
前端如何做可视化组件库?
Nuxt.js 服务器端渲染怎么实现?
这些文字会被转换成向量。
语义越接近,向量在空间里的距离通常也越近。
所以搜索时,不再只是判断某个词有没有出现,而是:
text
用户问题 -> 转成向量
文章内容 -> 提前转成向量
计算相似度 -> 找到最接近的文章
这就是语义搜索。
一个最小 RAG Demo
我们用一个本地文章搜索的例子来理解。
项目结构大概是:
text
posts-demo
├── app.service.mjs
├── create-embedding.mjs
├── semantic-search.mjs
└── data
├── posts.json
└── posts-embedding.json
其中 posts.json 是原始文章数据。
每条数据只有两个字段:
json
{
"title": "如何使用 Nuxt.js 进行服务器端渲染",
"category": "前端开发"
}
我们要做的事情是:
- 先把每篇文章转成 embedding
- 把 embedding 存到本地 JSON 文件里
- 用户输入问题时,把问题也转成 embedding
- 计算问题和文章之间的相似度
- 返回最相关的文章
这其实是 RAG 的前半段,也就是 Retrieval。
真正完整的 RAG,还会继续把检索结果放进 prompt,再让大模型生成最终答案。
第一步:准备 Embedding Client
项目里用 OpenAI SDK 兼容模式调用通义千问的 DashScope 接口:
js
import OpenAI from 'openai';
import dotenv from 'dotenv';
dotenv.config();
export const client = new OpenAI({
apiKey: process.env.DASHSCOPE_API_KEY,
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1'
});
这里有两个关键点:
DASHSCOPE_API_KEY放在.env里,不要写死到代码中baseURL指向 DashScope 的 OpenAI 兼容接口
这样后面就可以像调用 OpenAI SDK 一样调用 embedding 接口。
第二步:把文章转成向量
原始文章数据是人能看懂的文本。
但语义搜索需要向量。
所以我们先写一个脚本,把所有文章提前向量化:
js
import fs from 'fs/promises';
import { client } from './app.service.mjs';
const inputFilePath = './data/posts.json';
const outputFilePath = './data/posts-embedding.json';
const data = await fs.readFile(inputFilePath, 'utf-8');
const posts = JSON.parse(data);
const postsWithEmbedding = [];
for (const { title, category } of posts) {
const response = await client.embeddings.create({
model: 'text-embedding-v4',
input: `标题:${title},分类:${category}`
});
postsWithEmbedding.push({
title,
category,
embedding: response.data[0].embedding
});
}
await fs.writeFile(
outputFilePath,
JSON.stringify(postsWithEmbedding, null, 2)
);
这一步做完后,posts-embedding.json 里就不只是文章标题了,还会多出一个 embedding 字段。
它看起来大概是这样:
json
{
"title": "如何使用 Nuxt.js 进行服务器端渲染",
"category": "前端开发",
"embedding": [0.0123, -0.0345, 0.0876]
}
真实 embedding 会很长。
这个 demo 里每条 embedding 是 1024 维。
可以把它理解成:
模型给这篇文章生成了一张"语义坐标"。
以后搜索时,不用每次重新处理所有文章,只要读取已经保存好的 embedding 即可。
第三步:把用户问题也转成向量
文章已经向量化了。
用户输入问题时,也要做同样的事情。
比如用户输入:
text
有哪些前端可视化相关内容?
程序会调用 embedding 接口:
js
const response = await client.embeddings.create({
model: 'text-embedding-v4',
input: answer
});
const { embedding } = response.data[0];
现在,用户问题也变成了一组向量。
接下来就可以比较:
text
用户问题的向量
和
每篇文章的向量
谁更接近?
第四步:计算相似度
向量之间怎么比较接近程度?
常见做法是余弦相似度。
代码是:
js
const cosineSimilarity = (v1, v2) => {
const dotProduct = v1.reduce((acc, curr, i) => acc + curr * v2[i], 0);
const lengthV1 = Math.sqrt(v1.reduce((acc, curr) => acc + curr * curr, 0));
const lengthV2 = Math.sqrt(v2.reduce((acc, curr) => acc + curr * curr, 0));
return dotProduct / (lengthV1 * lengthV2);
};
不用被公式吓到。
可以先这么理解:
余弦相似度不是看两个向量"数值差多少",而是看它们"方向像不像"。
方向越接近,说明语义越接近。
所以我们可以对每篇文章算一个分数:
js
const results = posts
.map(item => ({
...item,
similarity: cosineSimilarity(embedding, item.embedding)
}))
.sort((a, b) => b.similarity - a.similarity)
.slice(0, 3);
最后返回相似度最高的前三篇文章。
这就是最小语义搜索。
这和 RAG 有什么关系?
到这里,我们其实还没有完成完整 RAG。
我们完成的是:
text
Retrieval:检索
也就是从资料库中找到最相关的内容。
完整 RAG 还需要后两步:
text
Augmented:把检索结果塞进 prompt,补充上下文
Generation:让大模型基于这些上下文生成答案
完整流程应该是:
text
用户问题
↓
问题向量化
↓
向量相似度检索
↓
取出 Top K 相关资料
↓
把资料放进 prompt
↓
LLM 基于资料生成答案
也就是说:
语义搜索解决的是"找资料"。
RAG 解决的是"找完资料以后,让模型基于资料回答"。
RAG 不是让模型重新学习
这里有一个常见误解:
很多人以为 RAG 是把资料"训练进模型"。
不是。
RAG 不修改模型参数。
它更像考试时开卷。
模型本身还是原来的模型。
只是回答问题前,系统先帮它翻出几页相关资料,然后把资料放到它面前。
所以 RAG 的优势是:
- 更新知识更方便
- 不需要重新训练模型
- 可以接入私有知识库
- 可以减少幻觉
- 可以让回答更可追溯
但它也有边界。
如果检索阶段找错了资料,后面的生成也会跟着跑偏。
所以 RAG 的效果,很大程度取决于检索质量。
RAG 和普通搜索有什么不同?
普通搜索更像:
text
用户输入关键词
系统返回相关文档列表
RAG 更像:
text
用户提出问题
系统先找资料
再让大模型根据资料组织答案
区别在最后一步。
搜索引擎通常把结果列表交给用户。
RAG 会把结果交给大模型,让模型帮用户读、整理、归纳、回答。
所以 RAG 适合这些场景:
- 企业知识库问答
- 客服机器人
- 法律文档检索
- 项目代码库问答
- 课程资料问答
- 内部制度查询
- 产品文档助手
用户不想自己翻一堆资料。
用户想直接问:
text
这个项目里登录逻辑在哪里?
或者:
text
公司报销超过 5000 元要走什么流程?
这时 RAG 就很有价值。
生产环境里还缺什么?
这个 demo 用 JSON 文件保存 embedding。
这适合学习。
但真实项目里,数据量一大,就需要专门的向量数据库或支持向量检索的存储。
比如:
- Milvus
- PostgreSQL + pgvector
- Elasticsearch 向量检索
- 向量数据库服务
真实 RAG 系统通常还要考虑:
- 文档怎么切块
- chunk 多大合适
- metadata 怎么存
- 召回 Top K 取多少
- 检索结果要不要重排
- prompt 怎么组织上下文
- 引用来源怎么返回
- 敏感资料怎么做权限控制
- 检索不到资料时怎么回答
所以 RAG 看起来只是"检索 + 生成",但真正做成产品时,它其实是一个上下文工程系统。
小结
RAG 的核心不是让模型变成数据库。
它真正解决的是:
大模型回答前,如何先拿到正确的外部资料。
关键词搜索只能匹配字面文本。
Embedding 让我们可以按语义检索资料。
语义搜索找到相关内容后,再把内容放进 prompt,让 LLM 基于上下文生成答案,这就是 RAG。
所以可以这样记:
text
Embedding 让资料可检索
Retrieval 找到相关资料
Augment 把资料塞进上下文
Generation 让模型基于资料回答
RAG 是上下文工程里最常见、也最容易落地的一种方式。
它把大模型从"凭记忆回答",推进到了"带资料回答"。