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 在特定场景下更靠谱"。它不修改模型,却能让模型回答出原本答不出的私有知识问题。

相关推荐
神奇霸王龙10 小时前
国产音乐视频 Prompt 三段式屠夫榜:5 个国产视频模型实测对比
人工智能·ai·prompt·音视频·agent·ai编程·agi
神奇霸王龙10 小时前
AI视频Prompt结构化实战指南:可灵/万相/豆包
人工智能·ai·prompt·aigc·音视频·ai编程
To_OC10 小时前
啃完流式输出:从一个卡顿的 LLM 接口开始,我搞懂了数据流到底怎么 “流”
前端·javascript·llm
To_OC10 小时前
调了一上午 DeepSeek 参数,我终于摸透了 temperature 和 Top K 的真实作用
人工智能·llm·deepseek
寅时码11 小时前
一个 Hook 堵住 Claude Code、Codex 的大部分权限"漏洞"
openai·ai编程·claude
kyriewen11 小时前
我用了三周Claude Code Skills——总结出5条铁律,第3条最反直觉
前端·ai编程·claude
孤狼GPT11 小时前
ChatGPT、Codex与Pro:AI开发正在从“单工具竞争”走向“系统协同”
chatgpt·ai编程·codex·chatgpt pro·系统协同
东小西12 小时前
第9篇:《AI终于记住我是谁了:多轮对话的上下文记忆与压缩》
openai·ai编程
咖啡星人k14 小时前
MonkeyCode、Cursor、Claude Code 与 Copilot 的选型思路对比
copilot·ai编程·monkeycode
寒水馨14 小时前
Linux下载、安装llama.cpp-b10068(附安装包llama-b10068-bin-ubuntu-vulkan-x64.tar.gz)
linux·ubuntu·llm·llama·本地部署·llama.cpp·推理引擎