RAG 是什么?从关键词搜索到语义搜索的完整过程


前面讲上下文工程的时候,我们提到过一个问题:

大模型本身并不知道你的私有资料。

它不知道你公司的内部文档,不知道数据库里的最新记录,也不知道你本地项目里刚写完的代码。

如果只靠模型训练时学到的知识,它很容易出现两种情况:

  • 不知道最新信息
  • 一本正经地胡说

这就是为什么现在很多 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": "前端开发"
}

我们要做的事情是:

  1. 先把每篇文章转成 embedding
  2. 把 embedding 存到本地 JSON 文件里
  3. 用户输入问题时,把问题也转成 embedding
  4. 计算问题和文章之间的相似度
  5. 返回最相关的文章

这其实是 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 是上下文工程里最常见、也最容易落地的一种方式。

它把大模型从"凭记忆回答",推进到了"带资料回答"。

相关推荐
AINative软件工程1 小时前
AI Agent 不是会重试就可靠:长任务的断点恢复与 Durable Execution 设计
人工智能·后端·ai编程
ZZZMMM.zip1 小时前
营养方程式-营养餐单规划的HarmonyOS开发实践
人工智能·华为·harmonyos·鸿蒙系统
广歌1 小时前
我花了半年驯养AI Agent,崩溃经历全记录
人工智能·开源
lauo1 小时前
从52起融资看教育科技新范式:PopLang引擎与Token节点经济如何重塑AI学习终端
人工智能·科技·学习
C137的本贾尼1 小时前
Agent 与 LLM 的交互协议:从 ReAct 到工具调用
人工智能·交互
BreezeJiang1 小时前
从零手写一个 AI 编程 Agent 之后,我终于理解了 Cursor 的核心原理
人工智能
进击切图仔1 小时前
基于千问的白盒蒸馏 SOP
运维·服务器·人工智能·python·效率
Token炼金师1 小时前
工程基础设施层:MLOps闭环断裂让模型上线即裸奔
人工智能·深度学习·llm
小雨青年2 小时前
【HarmonyOS 7开发者前瞻】06 HarmonyOS 7 AI 能力接入前,先用业务场景判断优先级
人工智能·华为·harmonyos