大家好,我是双越。wangEditor 作者,前百度 滴滴 资深前端工程师,慕课网金牌讲师,PMP,前端面试派 作者。
我正致力于两个项目的开发和升级,感兴趣的可以私信我,加入项目小组。
本文是一篇面向 Node.js 开发者的 RAG 学习笔记,涵盖核心概念、技术细节、工程实现与 Agent 应用场景。
RAG 是什么
RAG(Retrieval-Augmented Generation,检索增强生成)是一种将信息检索 与大语言模型生成相结合的技术架构。
背景与动机
大语言模型(LLM)在实际应用中存在几个核心痛点:
- 知识截止日期(Knowledge Cutoff) :模型训练数据有时效性,无法获取最新信息
- 无法访问私有数据:企业内部文档、数据库等私有知识对模型不可见
- 幻觉问题(Hallucination) :模型会自信地编造并不存在的信息
RAG 的核心思路是:先查资料,再回答问题。就像一个员工在回答老板问题之前,先去查阅相关文档一样。通过在生成答案前动态检索相关知识,RAG 让 LLM 的回答有据可查、可验证。
核心技术架构
整体流程
csharp
用户提问
↓
[Retrieval 检索阶段]
将问题向量化 → 在向量数据库中搜索 → 返回相关文档片段
↓
[Augmentation 增强阶段]
将检索到的内容拼入 Prompt(作为上下文)
↓
[Generation 生成阶段]
LLM 基于上下文生成最终答案
RAG 系统分为两条流水线:索引流水线 (离线处理文档)和查询流水线(在线响应用户)。
处理阶段(Indexing Pipeline)
这是离线阶段,负责将原始文档转化为可检索的向量索引:
scss
原始文档(PDF / Word / 网页 / 数据库)
↓ 解析(提取纯文本)
纯文本
↓ Chunking(分块)
文本片段(通常 200~1000 tokens)
↓ Embedding 模型(如 text-embedding-3-small)
向量(float[] 数组)
↓ 存入
向量数据库(Pinecone / Weaviate / pgvector / Chroma)
查询阶段(Query Pipeline)
这是在线阶段,负责实时响应用户提问:
javascript
async function ragQuery(userQuestion) {
// Step 1: 将问题转为向量
const queryEmbedding = await embeddings.embed(userQuestion);
// Step 2: 向量相似度搜索
const relevantChunks = await vectorDB.similaritySearch(queryEmbedding, topK = 5);
// Step 3: 构建增强 Prompt
const context = relevantChunks.map(c => c.text).join('\n\n');
const prompt = `
根据以下资料回答问题,如果资料中没有相关信息请说明。
【参考资料】
${context}
【问题】
${userQuestion}
`;
// Step 4: LLM 生成答案
return await llm.generate(prompt);
}
深入技术细节
Chunking 策略
分块方式直接影响检索质量,是 RAG 系统中最容易被忽视、却影响最大的环节之一。
| 策略 | 说明 | 适用场景 |
|---|---|---|
| Fixed-size | 按固定 token 数切割 | 简单场景快速验证 |
| Sliding Window | 带重叠的滑动窗口 | 防止关键信息在分块边界断裂 |
| Semantic | 按语义边界切割(句子/段落) | 通用场景,推荐默认选择 |
| Recursive | 递归按标题层级切割 | 结构化文档(Markdown / HTML) |
| Document-specific | 针对特定格式专门处理 | 代码文件、表格等特殊内容 |
核心原则:Chunk 不能太大(引入噪音)也不能太小(缺失上下文),通常 512 tokens 左右是比较合适的起点,需要根据实际效果调整。
检索增强主流程
生产级 RAG 系统的完整检索流程如下:
css
用户输入模糊问题
↓ Query Rewriting (扩展查询,提升召回率)
多个查询并行检索
↓ Hybrid Search (混合检索,粗筛 Top 50)
Top 50 候选文档
↓ Re-ranking (精准排序,筛出 Top 5)
Top 5 最相关文档
↓
注入 Prompt → LLM 回答
三个技术各司其职,组合使用才能达到生产级效果。
Hybrid Search 混合检索
为什么需要混合检索
单一检索方式各有缺陷:
向量检索(Vector Search) 将文字转换成数字向量,语义相近的内容向量距离就近。它能理解同义词、近义词,但对精确关键词不敏感------搜索 "GPT-4o" 可能找不到含有 "GPT-4o" 字样的文档。
关键词检索(BM25) 基于词频统计打分,是传统搜索引擎(Elasticsearch)的核心算法。它精确命中专有名词、代码、型号,但完全不懂语义------搜"汽车"找不到只写了"轿车"的文档。
arduino
用户搜索: "苹果手机拍照虚化效果怎么弄"
向量检索:能找到"iPhone 人像模式使用教程"(语义相关)✅
但可能漏掉含 "f1.8光圈" 的专业文档
BM25: 精确命中含"虚化"关键词的文档 ✅
但找不到只写了 "Bokeh效果" 的文档
混合检索:两者结果都要,然后合并排序 🎯
RRF 融合算法
两个检索系统各自返回一个排序列表,通过 RRF(Reciprocal Rank Fusion) 合并:
scss
RRF得分 = Σ 1 / (k + rank) (k 通常取 60)
举例说明:
less
向量检索结果: BM25检索结果:
第1名: 文档A 第1名: 文档C
第2名: 文档B 第2名: 文档A
第3名: 文档C 第3名: 文档D
RRF 计算:
文档A: 1/(60+1) + 1/(60+2) = 0.03252 ← 综合最高
文档C: 1/(60+3) + 1/(60+1) = 0.03226
文档B: 1/(60+2) + 1/(60+4) = 0.03176
最终排序:A → C → B → D
文档 A 在两个列表中都靠前,综合得分最高。RRF 的精髓是奖励在多个系统中都表现好的文档。
BM25 的输入注意事项
BM25 基于词频统计,不适合输入长句子,会适得其反:
- 关键词被"在"、"中"、"通常"等无意义词稀释
- 停用词干扰打分
- 词越多,结果越发散
两种检索的最佳输入策略截然相反:
| 向量检索 | BM25 | |
|---|---|---|
| 最佳输入 | 完整的句子/段落 | 精炼的关键词 |
| 原因 | Embedding 能压缩整体语义 | 词频统计,词越精准越好 |
| 类比 | 理解文意的人 | Ctrl+F 全文搜索 |
Node.js 实现
ini
import { EnsembleRetriever } from "langchain/retrievers/ensemble";
import { BM25Retriever } from "@langchain/community/retrievers/bm25";
const vectorRetriever = vectorStore.asRetriever({ k: 10 });
const bm25Retriever = BM25Retriever.fromDocuments(docs, { k: 10 });
const hybridRetriever = new EnsembleRetriever({
retrievers: [vectorRetriever, bm25Retriever],
weights: [0.6, 0.4], // 向量检索权重更高
});
const results = await hybridRetriever.invoke("Node.js fs模块怎么用");
Re-ranking 重排序
核心原理:Bi-encoder vs Cross-encoder
初始检索用 Bi-encoder(双塔模型) :Query 和文档各自独立编码,提前存好向量,查询时只计算距离,速度极快,但两者从未"见过对方",理解不够深。
Re-ranking 用 Cross-encoder(交叉模型) :Query 和文档拼在一起让模型读,能看到两者完整关系,理解深度远超 Bi-encoder,但无法提前计算,只能实时处理少量文档。
为什么两阶段缺一不可
sql
假设有 100 万份文档:
Cross-encoder 直接检索:100万次模型推理 → 慢到无法接受 ❌
Bi-encoder 检索:毫秒级返回,但理解粗糙 ✅(用于粗筛)
最优方案:粗筛缩小范围 → 精排提升质量
完整两阶段流程
css
100万文档
↓ 向量/混合检索(毫秒级,粗筛)
Top 20~50 候选文档
↓ Cross-encoder Re-ranking(精排,只处理少量文档)
Top 5 高质量文档
↓ 注入 Prompt → LLM 生成答案
Node.js 实现
javascript
import { CohereRerank } from "@langchain/cohere";
import { ContextualCompressionRetriever } from "langchain/retrievers/contextual_compression";
const baseRetriever = vectorStore.asRetriever({ k: 20 });
const reranker = new CohereRerank({
apiKey: process.env.COHERE_API_KEY,
topN: 5,
model: "rerank-multilingual-v3.0", // 支持中文
});
const retriever = new ContextualCompressionRetriever({
base_compressor: reranker,
base_retriever: baseRetriever,
});
const results = await retriever.invoke("Node.js 如何处理文件上传");
// 自动完成:粗筛20条 → rerank精排 → 返回最相关的5条
类比理解:Re-ranking 就像招聘时的两轮筛选------简历筛选 (检索)快速从 1000 份中选出 20 个,再安排面试(Re-ranking)深度评估,最终选出最合适的 5 个。
Query Rewriting 查询改写
为什么需要改写
用户输入的问题往往口语化、模糊、信息量不足,直接检索效果很差:
arduino
用户输入: "nodejs怎么连数据库"
可能会漏掉这些相关文档:
❌ "使用 Prisma ORM 操作 PostgreSQL"
❌ "Sequelize 配置连接池最佳实践"
❌ "mysql2 驱动安装与初始化"
根本原因:用户问的方式 和文档写的方式之间存在表达鸿沟。
三种常见做法
做法一:同义扩展(生成多个查询)
arduino
// 让 LLM 将一个问题改写成多个不同角度的查询
const queries = [
"Node.js 数据库连接方法",
"Prisma Sequelize 使用教程",
"mysql2 pg 驱动配置",
"Node.js connection pool 连接池"
];
// 用这4个查询分别检索,合并结果 → 召回率大幅提升
做法二:HyDE(假设性文档生成)
不改写问题,而是让 LLM 先假装回答一遍,再拿这个"假答案"去检索:
arduino
原始问题: "nodejs怎么连数据库"
↓ LLM 生成假设答案
"在 Node.js 中连接数据库通常使用 mysql2 或 pg 这类驱动,
也可以使用 Prisma、Sequelize 等 ORM 框架..."
↓
拿这段话做向量检索(效果更好,因为更接近真实文档的表达方式)
原理:假答案的向量 比短问题的向量更接近真实文档的向量。
⚠️ 注意:HyDE 生成的长文本适合用于向量检索,用于 BM25 时需要先提取关键词:
ini
// 向量检索:直接用假设答案的完整语义
const vectorResults = await vectorStore.similaritySearch(hypotheticalAnswer, 20);
// BM25检索:先提取关键词再检索
const keywords = ["mysql2", "Prisma", "连接池", "ORM", "pg"];
const bm25Results = await bm25.search(keywords.join(" "), 20);
做法三:问题分解(复杂问题)
arduino
原始问题: "Prisma 和 Sequelize 哪个更适合 Node.js 新项目"
↓ LLM 拆解
[
"Prisma ORM 的优缺点",
"Sequelize ORM 的优缺点",
"Prisma 和 Sequelize 性能对比",
"2024年 Node.js ORM 选型建议"
]
每个子问题单独检索 → 汇总结果 → LLM 综合回答
在 AI Agent 中的应用场景
在 Agent 架构中,RAG 不再是简单的"查一次",而是作为 Tool(工具) 被 Agent 按需调用,赋予 Agent 动态访问外部知识的能力。
场景一:企业知识库问答
最经典的场景,RAG 作为 Agent 的知识外挂:
css
用户: "我们公司的年假政策是什么?"
Agent: 调用 search_knowledge_base("年假政策")
→ 检索 HR 内部文档
→ 生成准确答案(而非 LLM 瞎猜)
场景二:代码库智能助手
代码向量化后,Agent 能理解整个代码仓库的语义:
css
用户: "帮我找到处理用户登录的函数"
Agent: 调用 search_codebase("用户登录认证")
→ 返回相关代码片段及文件位置
→ 分析并解释代码逻辑
场景三:Multi-step Research Agent
Agent 自主决定多次检索,综合多个来源:
css
用户: "帮我分析竞争对手的产品策略"
Agent:
1. search_web("competitor A product 2024") → 检索网页内容
2. search_internal_reports("市场分析") → 检索内部报告
3. 综合两次检索结果 → 生成完整分析报告
场景四:长期记忆(Long-term Memory)
Agent 将历史对话存入向量库,实现真正的"记住用户":
csharp
// 存储用户偏好
await memoryStore.save({
userId: "user_123",
content: "用户偏好用 TypeScript,不喜欢 callback 风格",
embedding: await embed("用户偏好TypeScript...")
});
// 下次对话时检索相关记忆,实现个性化响应
const memories = await memoryStore.recall(userId, currentQuestion);
代表框架:mem0。
场景五:动态工具文档检索(Tool RAG)
当 Agent 有数百个可用工具时,全部塞进 Prompt 会超出上下文长度限制:
用户意图
↓ RAG 检索最相关的 10 个工具定义
注入 Prompt
↓ LLM 选择调用哪个工具
技术实现
框架(Node.js 生态)
LangChain.js
目前 Node.js 生态中最成熟的 RAG/Agent 框架,提供了完整的工具链:
javascript
import { ChatOpenAI } from "@langchain/openai";
import { OpenAIEmbeddings } from "@langchain/openai";
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
import { EnsembleRetriever } from "langchain/retrievers/ensemble";
- 优点:文档丰富、社区活跃、集成全面(向量库、LLM、工具全覆盖)
- 缺点:抽象层较多,调试复杂,学习曲线较陡
LlamaIndex.TS
专注于数据索引和检索的框架,对 RAG 场景优化更深:
- 优点:RAG 专用设计,对复杂检索场景支持好
- 缺点:社区比 LangChain 小,文档相对少
入门推荐 :先用 LangChain.js,生态最完整,遇到问题最容易找到答案。
向量数据库对比
Chroma --- 本地开发专用
javascript
import { Chroma } from "@langchain/community/vectorstores/chroma";
// 零配置,像 SQLite 一样嵌入式运行
const vectorStore = await Chroma.fromDocuments(docs, embeddings, {
collectionName: "my-docs",
persistDirectory: "./chroma-data",
});
- 定位:嵌入式数据库,直接跑在进程里,数据存本地文件
- 优点 :零配置,
npm install即用 - 缺点:无集群、无高可用,不适合生产
- 适合:学习阶段、本地原型验证
pgvector --- 已有 PostgreSQL 时的最优解
sql
-- 开启扩展
CREATE EXTENSION vector;
-- 普通 PG 表,多了一个向量列
CREATE TABLE documents (
id SERIAL PRIMARY KEY,
content TEXT,
metadata JSONB,
embedding vector(1536)
);
-- 向量查询和业务查询混用,支持 JOIN
SELECT d.content, u.username
FROM documents d
JOIN users u ON d.author_id = u.id
WHERE u.plan = 'premium'
ORDER BY d.embedding <=> $1
LIMIT 5;
- 定位:PostgreSQL 插件,不是独立数据库
- 优点:向量查询与业务数据同库,事务/JOIN/权限全部复用,运维成本低
- 缺点 :没有原生 BM25,混合检索需自己实现(使用
ts_rank近似替代) - 适合:团队已有 PG、数据量 < 500 万、不想引入新服务
Weaviate --- 功能最全的开源方案
csharp
// 原生混合检索,一行搞定
const results = await weaviate.graphql.get()
.withClassName("Document")
.withHybrid({
query: "Node.js 连接数据库",
alpha: 0.6, // 0=纯BM25, 1=纯向量
})
.withLimit(5)
.do();
- 定位:专为 RAG/AI 场景设计的向量数据库
- 内置能力:向量检索 + BM25 + 混合检索 + RRF 融合 + Re-ranking + 多租户 + 多模态
- 优点:功能最全,原生混合检索开箱即用,相比自己实现省大量代码
- 缺点:需要自托管或使用 Weaviate Cloud,有一定运维成本
- 适合:需要混合检索、对功能要求高、可以接受自托管
Pinecone --- 托管云服务,最省心
ini
// 配置最简单,没有任何服务器要管
const pinecone = new Pinecone({ apiKey: process.env.PINECONE_API_KEY });
const index = pinecone.index("my-index");
await index.upsert(vectors);
const results = await index.query({ vector: queryEmbedding, topK: 10 });
- 定位:纯云托管 Serverless 向量数据库
- 优点:零运维、自动扩容、按量付费
- 缺点:不支持原生混合检索(需外挂 Elasticsearch)、数据存境外
- 适合:不想管运维、快速上线、预算充足
选型决策
学习 / 做原型 → Chroma(零成本零配置)
项目已用 PostgreSQL → pgvector(复用现有基础设施)
需要混合检索且能自托管 → Weaviate(功能最全)
不想管运维且预算充足 → Pinecone(最省心)
| Chroma | pgvector | Weaviate | Pinecone | |
|---|---|---|---|---|
| 部署方式 | 本地嵌入 | PG 插件 | 自托管/云 | 纯云托管 |
| 原生混合检索 | ❌ | ❌ | ✅ | ❌ |
| 运维成本 | 零 | 低 | 中 | 零 |
| 适合数据量 | 小 | 中 | 大 | 大 |
| 费用 | 免费 | 免费 | 免费/付费 | 按量付费 |
| 推荐阶段 | 学习 | 生产 | 生产 | 生产 |
Embedding 模型
Embedding 模型负责将文本转换为向量,是 RAG 质量的基础。
| 模型 | 提供方 | 特点 |
|---|---|---|
text-embedding-3-small |
OpenAI | 性价比高,速度快,推荐默认选择 |
text-embedding-3-large |
OpenAI | 精度更高,价格更贵 |
embed-multilingual-v3.0 |
Cohere | 多语言支持好,中文效果佳 |
BGE 系列 |
BAAI(智源) | 开源,中文效果极佳,可本地部署 |
| 本地模型(via Ollama) | --- | 完全免费,数据不出本地,适合隐私要求高的场景 |
选型建议:
- 快速上手:OpenAI text-embedding-3-small,API 简单,效果稳定
- 中文场景:优先考虑 Cohere 多语言模型 或 BGE 中文模型
- 数据隐私要求高:本地部署 BGE via Ollama
总结
技术全景图
sql
用户提问
↓
Query Rewriting 将模糊问题扩展为多个精准查询
↓
Hybrid Search 向量检索(语义)+ BM25(关键词)→ RRF 融合
↓
Re-ranking Cross-encoder 精准排序,淘汰低质量结果
↓
Prompt 增强 将 Top K 文档注入上下文
↓
LLM 生成 基于真实资料生成有据可查的答案
各技术作用一览
| 技术 | 解决的问题 | 何时引入 |
|---|---|---|
| 基础 RAG | LLM 知识截止/幻觉 | 项目起步 |
| Chunking 优化 | 检索到的内容质量差 | 发现答案不准时 |
| Hybrid Search | 专有名词/精确词匹配差 | 有代码、型号等精确词时 |
| Re-ranking | 检索结果排序不够精准 | 需要提升答案质量时 |
| Query Rewriting | 用户问题模糊导致漏检 | 召回率不足时 |
| RAGAS 评估 | 无法量化系统好坏 | 需要持续优化时 |
Node.js 开发者学习路径
- 跑通最小 Demo:LangChain.js + Chroma + OpenAI,本地搭一个知识库问答
- 理解 Chunking:同一份文档用不同分块策略,观察检索质量差异
- 引入混合检索:用 Weaviate 体验原生 Hybrid Search
- 加入 Re-ranking:接入 Cohere Rerank API,对比前后效果
- Agentic RAG:将 RAG 封装成 Tool,让 Agent 自主决定何时检索
- 评估与迭代:建立测试集,用 RAGAS 量化每次改动的效果
RAG 是目前 AI Agent 落地最成熟的技术之一。掌握它,就是给你的 Agent 装上了一个可以随时扩展、随时更新的"外挂大脑"。