面试被问 RAG 只能说出六个字?这篇用 100 行 Node.js 代码帮你彻底搞懂

🔥 面试被问 RAG 只能说出六个字?这篇用 100 行 Node.js 代码帮你彻底搞懂

摘要:面试被问"什么是 RAG",只能说出"检索增强生成"六个字?本文不讲百科罗列,只讲你最该掌握的核心原理。从 Naive RAG 出发,带你用 100 行 Node.js 代码跑通完整流程,再沿着五个优化方向逐步升级到生产级方案。建议先收藏,再慢慢看~


📌 前言:你为什么需要了解 RAG?

上周面试,面试官问:"你了解 RAG 吗?"

我自信回答:"Retrieval-Augmented Generation,检索增强生成!"

面试官点点头:"那它的核心流程是什么?和长上下文有什么区别?生产环境怎么优化检索质量?"

我:阿巴阿巴......

后来我花了两周系统梳理了 RAG 的知识体系,踩了不少坑,也总结出一套从原理到实战的完整路径。这篇文章就是我的学习笔记,希望能帮到和我一样曾经"只会说六个字"的同学。

大模型的三个硬伤

RAG 为什么存在?因为大模型有三个先天缺陷:

硬伤一:知识有截止日期。训练数据有时间边界,问"2026 年最新的框架",AI 可能完全不知道。

硬伤二:会一本正经地胡说八道。这就是"幻觉"(Hallucination)------问一个不存在的 API,AI 编得有模有样,你信了就踩坑了。

硬伤三:缺乏私有知识。问"公司的请假流程",AI 完全答不上来,因为它从来没看过你的内部文档。

RAG 的核心思想就一句话:先搜再答。让 AI 在回答之前先去"翻书"找参考资料,再基于搜到的知识组织答案。就跟开卷考试一样------遇到不会的先翻书,再根据书里的知识答题。

RAG 和长上下文是什么关系?

有同学会问:现在大模型都支持百万 token 上下文了,还需要 RAG 吗?

需要,而且用得比以前更多了! 原因有两个:

  1. 成本:把所有文档塞进上下文窗口,token 费用会非常高
  2. Lost in the Middle:大模型对超长上下文中间部分的注意力会明显下降。就像听别人说话,开头和结尾印象深,中间容易忘

最佳实践是两者互补:先用 RAG 提供精确资料,再利用长上下文做分析推理。


Part 1:RAG 核心原理------从 100 行代码说起

不讲伪代码,直接看真实可运行的 Node.js 实现。看完这 100 行代码,你就理解了 RAG 的核心原理。

一个最简单的 RAG 是怎么工作的

整个流程分为两个阶段:

离线索引(提前准备):把文档切成小块 → 每块转成向量 → 存起来

在线查询(用户提问时):问题转成向量 → 找最相似的文档块 → 拼成 Prompt 给 AI

用代码来说就是:

javascript 复制代码
// ============ 离线索引阶段 ============
// 1. 读取原始数据
const posts = JSON.parse(await fs.readFile('./data/posts.json', 'utf-8'));

// 2. 每篇文章转成向量(调用 Embedding API)
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,  // 1536 维向量
    });
}

// ============ 在线查询阶段 ============
// 1. 用户问题也转成向量
const { embedding } = (await client.embeddings.create({
    model: "text-embedding-v4", input: userQuery
})).data[0];

// 2. 计算余弦相似度,找 Top 3
const result = posts
    .map(item => ({ ...item, similarity: cosineSimilarity(item.embedding, embedding) }))
    .sort((a, b) => b.similarity - a.similarity)
    .slice(0, 3);

三个关键环节拆解

环节 做什么 关键技术
分块 把长文档切成小段 chunk_size、chunk_overlap
向量化 文字变数字 Embedding 模型(如 text-embedding-v4)
检索+生成 找相似文档,拼成 Prompt 余弦相似度 + LLM

我踩过的坑

坑 1:API 限流 。批量处理 35 篇文章时,跑到第 10 篇就报 429 Too Many Requests。解决:每处理一条后 sleep(200)

坑 2:语义不够丰富。一开始 input 只写了标题,搜索"React 组件"返回了不相关的文章。解决:把标题和分类拼在一起,让模型理解更多上下文。

坑 3:向量维度理解错误 。一开始以为向量越短越好,后来才知道 text-embedding-v4 生成 1536 维向量是固定的,维度越高语义表达越精细。


Part 2:从 Naive 到生产级------五个关键优化方向

Naive RAG 能跑通,但离生产环境还差得远。我在实际项目中总结出五个优化方向,每个方向对应 2-3 种具体方案。

方向一:检索质量优化

问题 :纯向量检索无法精确匹配术语(如 ERROR_CODE_4012),传统关键词搜索不理解语义。

方案:Hybrid Search(混合检索)

同时跑两路检索------向量搜索负责语义理解,BM25 负责精确匹配,通过 RRF 算法合并结果:

python 复制代码
# 两路检索各自召回 Top 20
semantic_results = vector_store.search(embed(query), k=20)
keyword_results = bm25_index.search(query, k=20)

# RRF 融合:score += 1 / (60 + rank)
for each doc in (semantic_results ∪ keyword_results):
    if doc in semantic_results: score += 1 / (60 + rank_in_semantic)
    if doc in keyword_results: score += 1 / (60 + rank_in_keyword)

💡 几乎所有生产环境都建议用 Hybrid Search 替代纯向量搜索,尤其是技术文档、医疗、法律等术语密集的领域。

方案:Reranking(精排)

检索回来的候选文档里总会混着噪声。Reranking 在检索和生成之间加一个精排步骤:用 Reranker 模型给每对 (query, doc) 重新打分。

Embedding 模型是分别给 query 和 doc 算向量再比较距离,快但粗糙 ;Reranker 是把它们拼在一起送进模型打分,慢但精准

生产级方案是分层筛选:粗检索(150 条)→ 轻量 Reranker 初筛(20 条)→ Cross-Encoder 精排(Top 5)→ 生成回答。

🔑 Hybrid Search + Reranking 是大多数生产级 RAG 系统的基础配置,我实测后检索准确率从 72% 提升到了 89%。

方向二:查询理解优化

问题:用户提问方式千奇百怪。想知道公司报销流程,有人问"怎么报销",有人问"花了钱怎么找公司要回来"。措辞和文档差距大,向量检索就可能搜不到。

方案:Multi-Query(多查询改写)

让大模型把原始问题改成多种不同的表述,分别去搜,最后合并去重:

python 复制代码
queries = LLM.generate("请将以下问题改写成 3 个不同的表述:" + user_query)
# 例如: ["报销流程是什么", "费用审批怎么操作", "如何提交报销申请"]

适合面向普通用户的客服、电商等口语化场景。代价是多调用一次 LLM + N 次向量检索。

方案:HyDE(假设性文档嵌入)

用户的问题往往很短(如"KV Cache 是什么?"),但文档里是一大段技术解释。一短一长,在向量空间中可能离得很远。

HyDE 的做法:让大模型先凭空写一个"假答案"(不必完全准确),然后用假答案的向量去检索。因为假答案和真文档的文体更接近,命中率更高。

风险:如果 LLM 编的方向跑偏,检索结果会更差。冷门领域慎用。

方向三:生成质量优化

问题:搜到了垃圾文档,大模型还是会一本正经地基于垃圾内容生成答案。就像开卷考试带错了书,还照着抄了上去。

方案:CRAG(纠错 RAG)

在检索和生成之间插一个质检员,逐个审查文档是否和问题相关:

  • 打分高 → 资料靠谱,直接生成
  • 打分低 → 内部知识库没找到,回退到 Web 搜索兜底
  • 打分模糊 → 两边结果合一起送进去

方案:Self-RAG(自反思 RAG)

CRAG 只在检索阶段做质检,但生成阶段也可能产生幻觉。Self-RAG 设置四个检查点:

  1. 这个问题需要检索吗?
  2. 检索到的文档相关吗?
  3. 我的回答有文档支撑吗?(价值最高)
  4. 这个答案对用户有用吗?

第三个检查点最实用------能在生成阶段拦截幻觉。

方向四:数据结构优化

问题:前面的方法都假设答案落在某一个文档块里。但有些问题需要跨文档多跳推理。

文档 A:"张三是 AI 部门的负责人" 文档 B:"AI 部门属于技术中心" 问:"张三属于哪个中心?" → 需要串联推理

方案:GraphRAG(知识图谱 RAG)

微软 2024 年提出。用 LLM 从文档中抽取实体和关系,构建知识图谱,再基于图谱检索和推理。

⚠️ 我曾尝试用 GraphRAG 做企业知识库,结果发现图谱构建成本远超预期(每篇文档都要调 LLM 抽取实体),最终放弃了。简单事实查询没必要用,只有需要多跳推理时才值得投入。

方案:Text-to-SQL RAG

如果数据本身就是结构化表格,对表格做 Embedding 非常低效。让 LLM 直接把自然语言翻译成 SQL,执行查询,再把结果作为上下文回答。

方向五:架构扩展优化

问题:每种方案都有擅长的场景,但真实系统中这些场景可能同时存在。

方案:Agentic RAG

让一个 AI Agent 自主调度,配备一组检索工具(向量检索、Web 搜索、SQL 查询),它会自己决定用什么方式搜集信息。知名 AI 编程工具 Cursor 用的就是这种方式。

方案:Multi-Agent RAG

单个 Agent 同时兼顾理解意图、选择策略、验证质量,Prompt 太复杂。拆分为多个专职 Agent------Router Agent 负责分发、各 RAG Agent 负责对应领域、Verification Agent 负责质检。


Part 3:方案选型决策树

看完五个方向,你可能已经晕了。我到底该用哪个?

别慌,我帮你梳理了一个决策路径:

sql 复制代码
你的数据是什么类型?
├── 非结构化文本(文档、文章、手册)
│   ├── 追求基本可用 → Naive RAG
│   ├── 用户提问口语化 → + Multi-Query / HyDE
│   ├── 术语密集 → + Hybrid Search
│   └── 不能容忍幻觉 → + CRAG / Self-RAG
├── 结构化表格(销售数据、报表)
│   └── Text-to-SQL RAG
├── 关系网络(需要多跳推理)
│   └── GraphRAG
└── 多种类型混合
    └── Agentic RAG

方案对比速查表

方案 实现难度 适合场景 我的推荐指数
Naive RAG 入门学习、MVP 验证 ⭐⭐⭐⭐⭐
Multi-Query ⭐⭐ 客服、电商等口语化场景 ⭐⭐⭐⭐
HyDE ⭐⭐ 短问题查长文档 ⭐⭐⭐
Hybrid Search ⭐⭐⭐ 生产环境必备 ⭐⭐⭐⭐⭐
Reranking ⭐⭐⭐ 语料库 > 10 万条 ⭐⭐⭐⭐
CRAG / Self-RAG ⭐⭐⭐⭐ 医疗、法律等高准确率场景 ⭐⭐⭐⭐
GraphRAG ⭐⭐⭐⭐⭐ 跨文档多跳推理 ⭐⭐⭐
Agentic RAG ⭐⭐⭐⭐⭐ 多数据源混合 ⭐⭐⭐⭐

🔑 给初学者的建议:先跑通 Naive RAG,发现哪个环节出了问题,就针对性地加入对应的优化方案。千万别一上来就搞 Multi-Agent + GraphRAG + 多模态全家桶,不仅实现成本高,效果也不一定更好。


Part 4:完整代码实现

技术栈选型

技术 选择 原因
运行时 Node.js 前端工程师友好,ES Modules 原生支持
AI 模型 通义千问 text-embedding-v4 国内访问稳定,免费额度充足
API 协议 OpenAI SDK 兼容 一套代码切换多个模型服务商
数据存储 JSON 文件 Demo 简单够用,生产环境换向量数据库

项目结构

bash 复制代码
posts-demo/
├── app.service.mjs      # OpenAI 客户端封装(复用)
├── create-embedding.mjs # 步骤1:将文章数据向量化
├── semantic-search.mjs  # 步骤2:实现语义搜索
├── data/
│   ├── posts.json           # 原始文章数据(35 篇)
│   └── posts-embedding.json # 向量化后的数据
├── .env                 # API Key 配置
└── package.json

第一步:初始化项目

bash 复制代码
mkdir rag-demo && cd rag-demo
pnpm init
pnpm add openai dotenv

创建 .env 文件:

env 复制代码
# 通义千问 API Key(去 https://dashscope.console.aliyun.com/ 申请)
DASHSCOPE_API_KEY=sk-xxxx你的key
# 兼容 OpenAI 的接口地址
DASHSCOPE_API_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1

第二步:封装 OpenAI 客户端

javascript 复制代码
// app.service.mjs
import OpenAI from 'openai';
import dotenv from 'dotenv';
dotenv.config();

// 导出 client 供其他模块复用
export const client = new OpenAI({
    apiKey: process.env.DASHSCOPE_API_KEY,
    baseURL: process.env.DASHSCOPE_API_BASE_URL,
});

第三步:将文章向量化

javascript 复制代码
// create-embedding.mjs
import fs from 'fs/promises';
import { client } from './app.service.mjs';

const posts = JSON.parse(await fs.readFile('./data/posts.json', 'utf-8'));
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
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 sleep(200); // 防止 API 限流
}

await fs.writeFile('./data/posts-embedding.json',
    JSON.stringify(postsWithEmbedding, null, 2));
bash 复制代码
node create-embedding.mjs

第四步:实现语义搜索

javascript 复制代码
// semantic-search.mjs
import fs from 'fs/promises';
import { client } from './app.service.mjs';
import readline from 'readline';

const posts = JSON.parse(await fs.readFile('./data/posts-embedding.json', 'utf-8'));

// 余弦相似度:衡量两个向量的"方向"是否一致
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);
};

const rl = readline.createInterface({ input: process.stdin, output: process.stdout });

const handleInput = async (answer) => {
    console.log(`\n🔍 搜索: "${answer}"\n`);

    // 1. 问题向量化
    const { embedding } = (await client.embeddings.create({
        model: "text-embedding-v4", input: answer
    })).data[0];

    // 2. 相似度排序,取 Top 3
    const result = posts
        .map(item => ({ ...item, similarity: cosineSimilarity(item.embedding, embedding) }))
        .sort((a, b) => b.similarity - a.similarity)
        .slice(0, 3)
        .map((item, i) => `${i + 1}. [${item.similarity.toFixed(4)}] ${item.title} (${item.category})`)
        .join('\n');

    console.log(result);
    rl.question('\n请输入你要搜索的内容: ', handleInput);
};

rl.question('\n请输入你要搜索的内容: ', handleInput);
bash 复制代码
node semantic-search.mjs

运行效果

ini 复制代码
请输入你要搜索的内容: Vue状态管理

🔍 搜索: "Vue状态管理"

1. [0.9234] 如何在 Vue.js 中使用 Vuex 进行状态管理 (前端开发)
2. [0.8567] 如何使用 React Hooks 构建可复用的组件 (前端开发)
3. [0.8123] 如何使用 Nuxt.js 进行服务器端渲染 (前端开发)

请输入你要搜索的内容: Python后端接口

🔍 搜索: "Python后端接口"

1. [0.9012] 使用 Flask 和 Python 构建 RESTful API (后端开发)
2. [0.8876] 使用 Django 和 Python 构建一个简单的博客应用 (后端开发)
3. [0.8234] 使用 Node.js 和 Express 构建一个简单的 RESTful API (后端开发)

💡 用户输入"Vue状态管理",虽然标题里没有这四个字,但系统找到了"Vuex 进行状态管理"------这就是语义搜索的威力。


💡 一句话速查卡片

概念 一句话解释
RAG 先搜再答,让 AI 基于检索到的资料回答问题
Embedding 把文字变成一组数字(向量),数字之间的距离代表语义相似度
余弦相似度 衡量两个向量"方向"的一致性,越接近 1 越相似
Hybrid Search 向量搜索(语义)+ BM25(关键词)双路检索,RRF 算法融合
Lost in the Middle 大模型对超长上下文中间部分注意力下降,所以不能全塞进去
RAGAS RAG 评估框架:忠实度、答案相关性、上下文精确率、上下文召回率

🔗 参考资料


💬 交流讨论

你用过 RAG 吗?在实际项目中遇到过哪些坑?欢迎在评论区分享你的经验!

后续我会分享更多 AI 实战内容:

  • 🔜 《用 RAG + ChatGPT 打造智能技术问答机器人》
  • 🔜 《向量数据库选型指南:Milvus vs pgvector vs Pinecone》

觉得有用?点个赞👍收藏⭐关注👆,下次更新更多 AI 实战玩法!

相关推荐
宝贝儿好1 小时前
【LLM】第三章:BERT讲解+情感分析案例
人工智能·深度学习·神经网络·算法·自然语言处理·bert
-cywen-1 小时前
BLIP:Bootstrapping Language-Image Pre-training
人工智能
gis开发之家1 小时前
vue3组合式函数(Composables)的艺术——将业务逻辑从UI组件中连根拔起
前端·javascript·vue.js·前端框架·vue3·前端工程化·前端架构
2301_780356701 小时前
全视通智慧医院解决方案:构建数智化医疗新生态
大数据·网络·人工智能
sunywz1 小时前
【AI RAG知识库】09.【检索】【节点7】
人工智能
国服第二切图仔1 小时前
HarmonyOS APP《画伴梦工厂》开发第54篇-鸿蒙AI开放能力——语音与自然语言处理
人工智能·自然语言处理·harmonyos
AC赳赳老秦1 小时前
采购专员自动化:OpenClaw 自动比价、生成询价单、跟踪供应商报价进度实战指南
开发语言·数据库·人工智能·python·自动化·github·openclaw
QiLinkOS1 小时前
QiLink OS的失败数据共享平台的运作模式是否适用于所有行业?
android·开发语言·人工智能·算法·重构·开源
秦歌6661 小时前
agno-9-Culture
人工智能