RAG 实战:给大模型装上“开卷考试”的外挂

RAG 实战:给大模型装上"开卷考试"的外挂

摘要:RAG 让 LLM 从"闭卷考试"变为"开卷考试"。本文从定义出发,拆解 RAG 的建库与查询流程,用代码演示如何将 JSON 数据向量化并实现语义搜索。

📑 目录

  • RAG 是什么?开卷考试 vs 闭卷考试
  • RAG 的工作流程:建库与查询
  • 切割策略:JSON 条目即 Chunk
  • 代码实战:将 JSON 数据向量化
  • 语义搜索:用余弦相似度找答案
  • 一个完整的查询流程演示
  • 核心误区:LLM 没有"预训练向量数据库"
  • 一点总结
  • 互动讨论

RAG 是什么?开卷考试 vs 闭卷考试

RAG(Retrieval-Augmented Generation)的核心思想很简单:让大模型在回答问题前,先去外部知识库检索相关材料,再基于这些材料生成答案。

  • 传统 LLM:闭卷考试。全靠训练时背下来的知识,没学过就瞎编(幻觉)。
  • 微调:考前突击补习。花大量时间把新知识强行灌进模型。
  • RAG:开卷考试。允许带参考书进考场,不知道就翻书。

RAG = 检索(Retrieval)+ 增强(Augmented)+ 生成(Generation)。它不是让 LLM 变聪明,而是让 LLM 每次答题都带上"参考书"。

RAG 的工作流程:建库与查询

RAG 分为两个阶段:

阶段一:建库(离线,做一次就行)

text

javascript 复制代码
原始数据(JSON/PDF/Word)
    ↓
文档切块(Chunking)→ 切成小段落
    ↓
向量化(Embedding)→ 每段文字转为高维向量
    ↓
存储(Store)→ 存入向量数据库

阶段二:查询(在线,每次提问都执行)

text

css 复制代码
用户提问
    ↓
问题向量化 → 转为向量
    ↓
相似度检索 → 在向量数据库中找最相似的 Top-K 个段落
    ↓
增强拼接 → 将检索到的原文拼成 Prompt
    ↓
生成答案 → LLM 基于 Prompt 回答

切割策略:JSON 条目即 Chunk

在 RAG 系统中,文档切块(Chunking) 是非常关键的一步。切得太粗,检索精度低;切得太碎,上下文丢失。

常见的切块策略有三种:

策略 说明
固定大小分块 每段固定字数,简单但可能切断语义
语义分块 按段落、章节或句子边界切分
重叠分块 相邻段落有重叠,保证上下文衔接

在本次实现中,我采用了以 JSON 条目为最小 Chunk 单元 的切分方式。每条文章记录(包含 titlecategory)作为一个独立的向量单元。

json

json 复制代码
// 一个 JSON 条目 = 一个 Chunk
{
    "title": "如何使用 Nuxt.js 进行服务器端渲染",
    "category": "前端开发"
}

这样做的好处:

  • 语义完整:每条文章本身就是一个完整的知识单元,不会被切断。
  • 结构清晰title + category 的组合提供了丰富的语义上下文。
  • 检索精准:用户搜索"Vue 响应式布局"时,能直接匹配到相关文章标题。

代码实战:将 JSON 数据向量化

create-embedding.mjs 实现了建库阶段的核心逻辑:读取 JSON 数据 → 逐条向量化 → 保存结果。

javascript

javascript 复制代码
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) {
    console.log(title, category, 'embedding...');
    const response = await client.embeddings.create({
        model: 'text-embedding-v4',
        input: `标题:${title},分类:${category}`
    });
    postsWithEmbedding.push({
        title,
        category,
        embedding: response.data[0].embedding
    });
    await sleep(200); // 防止限流
}

await fs.writeFile(outputFilePath, JSON.stringify(postsWithEmbedding, null, 2));

关键设计

  1. 每条 JSON 记录独立向量化 :每条文章的 title + category 被拼成一段文本,由 Embedding 模型转化为 1024 维向量。
  2. input 加中文前缀强化语义标题:${title},分类:${category} 让模型更精准地理解这段文本的结构。
  3. 200ms 延时:防止请求过快被大模型服务限流。

输出的 posts-embedding.json 长这样:

json

less 复制代码
[  {    "title": "如何使用 Nuxt.js 进行服务器端渲染",    "category": "前端开发",    "embedding": [0.0341, -0.0671, 0.0435, ...] // 1024 维向量
  }
]

语义搜索:用余弦相似度找答案

查询阶段的核心是计算余弦相似度------用用户问题的向量去匹配已存储的所有 Chunk 向量,找出最相似的。

javascript

javascript 复制代码
function 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);
}

余弦相似度的值介于 -1 和 1 之间,越接近 1 表示两个向量的方向越一致,语义越相似。

语义搜索的威力:用户搜索"马铃薯怎么做",传统关键词匹配找不到"酸辣土豆丝",但向量搜索能理解"马铃薯"和"土豆"是同一个东西,返回相关结果。这就是 Embedding 将语义相似度转化为数学距离计算。

一个完整的查询流程演示

semantic-search.mjs 实现了一个完整的命令行交互搜索:

javascript

javascript 复制代码
const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout
});

const handleInput = async (answer) => {
    // 1. 用户问题 → 向量
    const response = await client.embeddings.create({
        model: 'text-embedding-v4',
        input: answer
    });
    const { embedding } = response.data[0];

    // 2. 计算所有 Chunk 的相似度并排序
    const results = posts
        .map(item => ({
            ...item,
            similarity: cosineSimilarity(embedding, item.embedding)
        }))
        .sort((a, b) => a.similarity - b.similarity)
        .reverse()
        .slice(0, 3)  // 取 Top 3
        .map((item, index) => `${index + 1}.${item.title}.${item.category}`)
        .join('\n');

    console.log(`\n搜索结果:\n${results}`);
    rl.question("\n请输入你要搜索的内容:", handleInput);
};

rl.question("\n请输入你要搜索的内容:", handleInput);

用户输入"Vue 响应式布局"时,系统会:

  1. 将问题向量化
  2. 与所有 Chunk 的 embedding 计算余弦相似度
  3. 返回相似度最高的 3 篇文章标题
  4. 这些标题/内容可以作为上下文,喂给 LLM 生成最终答案

核心误区:LLM 没有"预训练向量数据库"

关于 RAG,有几个常见的误区需要澄清:

常见误区 ✅ 真相
❌ "LLM 有预训练向量数据库,RAG 就是结合两个库。" LLM 根本没有"预训练向量数据库"! 训练完成后,训练语料已经被压缩成了固定的模型权重(神经网络参数),LLM 无法"检索"自己的训练数据。
❌ "RAG 是让 LLM 用自己的知识 + 外部知识一起回答。" RAG 是强行把外部资料"塞进"LLM 的上下文(Prompt) ,让 LLM 完全基于这些资料来回答。LLM 只是充当"优秀阅读理解者"的角色。
❌ "RAG 是让 LLM 变聪明。" RAG 是让 LLM"带书考试" ,它本身的智力(模型权重)没有变化,只是每次考试都带了参考书。

也就是说,RAG 本质上是一种上下文工程:把检索到的内容拼进 Prompt,让 LLM 根据这些内容生成答案。它不修改模型本身。

一点总结

通过这次 RAG 实战,我理解了:

  1. RAG 的核心是检索 + 生成:先查资料,再回答,让 LLM 从闭卷变开卷。
  2. Chunk 策略影响检索效果:以 JSON 条目为 Chunk 单元,语义完整、检索精准。
  3. Embedding 将语义转化为数学计算:余弦相似度让机器能"理解"语义相近的文本。
  4. RAG 不改变模型本身:它只是每次答题时给 LLM 带上"参考书",解决的是知识更新和私有数据的问题。
  5. LLM 没有预训练向量数据库:训练完成后,数据已经被压缩成模型权重,无法检索。RAG 是唯一能让 LLM 访问外部知识的手段。

互动讨论

  1. 为什么固定大小分块可能导致语义丢失? 你遇到过因为切块不当导致检索不准的情况吗?
  2. title + category 作为 Chunk 的输入文本,如果换成 title + category + 正文摘要,效果会更好吗?
  3. 余弦相似度的值在什么范围内表示强相关? 你平时会设一个阈值来过滤低相关结果吗?
  4. 如果你的知识库有 100 万条数据,遍历计算余弦相似度会非常慢,有什么优化方案?
  5. RAG 和微调(Fine-tuning)有什么区别? 什么时候用 RAG,什么时候用微调?

📌 一点心得:RAG 的价值不在于"让 AI 更聪明",而在于"让 AI 在特定场景下更靠谱"。它不修改模型,却能让模型回答出原本答不出的私有知识问题。

相关推荐
慕嵩云2 小时前
当我们讨论 Harness 的时候,我们在讨论什么?
ai编程
乘风gg2 小时前
代码都已经让 AI 写了,为什么有些人却更慌了?
前端·ai编程·claude
ZJPRENO2 小时前
Claude 必备技能 Superpowers
ai编程
辰合软件2 小时前
通达OA二次开发入门指南
ai编程
我不是外星人2 小时前
一键还原任意网站,这套 Playwright + 多 Agent 工程流太猛了
前端·后端·ai编程
程序猿小卡2 小时前
模型越来越便宜之后,AI 应用团队要重算的不是 token 价格
agent·ai编程
zzgnbfd65882 小时前
2026最新2款AI编程工具平替实测深度对比
ai编程
Arranging157882 小时前
2026最新5款高适配AI编程工具平替实测
ai编程
怕浪猫3 小时前
第10章 实战项目二:自动化研发运维Agent
aigc·openai·ai编程