====================
大家好,我是雙越。wangEditor 作者,前百度、滴滴 資深前端工程師,慕課網金牌講師,PMP,前端面試派 作者。
我正在致力於兩個專案的開發與升級,有興趣可以私訊我加入專案小組。
本文是一篇面向 Node.js 開發者的 RAG 學習筆記,涵蓋核心概念、技術細節、工程實作與 Agent 應用場景。
RAG(Retrieval-Augmented Generation,檢索增強生成)是一種將資訊檢索與大型語言模型生成相結合的技術架構。
大型語言模型(LLM)在實際應用中存在幾個核心痛點:
RAG 的核心思路是:先查資料,再回答問題。就像員工在回答主管問題前先去查閱相關文件一樣。透過在生成答案前動態檢索相關知識,RAG 讓 LLM 的回答有據可查、可驗證。
使用者提問
↓
[Retrieval 檢索階段]
將問題向量化 → 在向量資料庫中搜尋 → 回傳相關文件片段
↓
[Augmentation 增強階段]
將檢索到的內容拼入 Prompt(作為上下文)
↓
[Generation 生成階段]
LLM 基於上下文生成最終答案
RAG 系統分為兩條流水線:索引流水線(離線處理文件)和查詢流水線(線上回應使用者)。
這是離線階段,負責將原始文件轉換為可檢索的向量索引:
原始文件(PDF / Word / 網頁 / 資料庫)
↓ 解析(抽取純文字)
純文字
↓ Chunking(分塊)
文本片段(通常 200~1000 tokens)
↓ Embedding 模型(如 text-embedding-3-small)
向量(float[] 陣列)
↓ 儲存
向量資料庫(Pinecone / Weaviate / pgvector / Chroma)
這是線上階段,負責即時回應使用者提問:
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);
}
分塊方式直接影響檢索品質,是 RAG 系統中最容易被忽略、卻影響最大的環節之一。
策略說明與適用場景:
核心原則:Chunk 不可太大(引入雜訊)也不可太小(缺失上下文),通常 512 tokens 左右是比較合適的起點,需根據實際效果調整。
生產級 RAG 系統的完整檢索流程如下:
使用者輸入模糊問題
↓ Query Rewriting (擴展查詢,提升召回率)
多個查詢並行檢索
↓ Hybrid Search (混合檢索,粗篩 Top 50)
Top 50 候選文件
↓ Re-ranking (精準排序,篩出 Top 5)
Top 5 最相關文件
↓
注入 Prompt → LLM 回答
三個技術各司其職,組合使用才能達到生產級效果。
單一檢索方式各有缺陷:
向量檢索(Vector Search)將文字轉成數值向量,語義相近的內容向量距離相近。它能理解同義詞、近義詞,但對精確關鍵字不敏感 —— 搜尋 "GPT-4o" 可能找不到含有 "GPT-4o" 字樣的文件。
關鍵字檢索(BM25)基於詞頻統計打分,是傳統搜尋引擎(Elasticsearch)的核心演算法。它精準命中專有名詞、程式碼、型號,但完全不懂語意 —— 搜 "汽車" 找不到只寫了 "轎車" 的文件。
範例:
使用者搜尋: "蘋果手機拍照虛化效果怎麼弄"
兩個檢索系統各自回傳一個排序列表,透過 RRF(Reciprocal Rank Fusion)合併:
RRF 得分 = Σ 1 / (k + rank) (k 通常取 60)
舉例說明:
向量檢索結果: 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
RRF 的精髓是獎勵在多個系統中都表現好的文件。
BM25 基於詞頻統計,並不適合輸入長句子,否則會適得其反:
兩種檢索的最佳輸入策略截然相反:
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 模組怎麼用");
初始檢索用 Bi-encoder(雙塔模型):Query 和文件各自獨立編碼,提前存好向量,查詢時只計算距離,速度極快,但兩者從未「見過彼此」,理解不夠深。
Re-ranking 用 Cross-encoder(交叉模型):Query 和文件拼在一起讓模型讀,能看到兩者完整關係,理解深度遠超 Bi-encoder,但無法提前計算,只能即時處理少量文件。
假設有 100 萬份文件:
最優方案:粗篩縮小範圍 → 精排提升品質
100 萬文件
↓ 向量/混合檢索(毫秒級,粗篩)
Top 20~50 候選文件
↓ Cross-encoder Re-ranking(精排,只處理少量文件)
Top 5 高品質文件
↓ 注入 Prompt → LLM 生成答案
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 個。
使用者輸入的問題往往口語化、模糊、資訊量不足,直接檢索效果很差:
使用者輸入: "nodejs怎麼連資料庫"
可能會漏掉這些相關文件:
❌ "使用 Prisma ORM 操作 PostgreSQL"
❌ "Sequelize 設定連線池最佳實踐"
❌ "mysql2 驅動安裝與初始化"
根本原因:使用者問的方式和文件寫的方式之間存在表達鴻溝。
做法一:同義擴展(生成多個查詢)
// 讓 LLM 將一個問題改寫成多個不同角度的查詢
const queries = [
"Node.js 資料庫連線方法",
"Prisma Sequelize 使用教學",
"mysql2 pg 驅動設定",
"Node.js connection pool 連線池"
];
// 用這四個查詢分別檢索,合併結果 → 召回率大幅提升
做法二:HyDE(假設性文件生成)
不改寫問題,而是讓 LLM 先假裝回答一遍,再拿這個「假答案」去檢索:
原始問題: "nodejs怎麼連資料庫"
↓ LLM 生成假設答案
"在 Node.js 中連接資料庫通常使用 mysql2 或 pg 這類驅動,
也可以使用 Prisma、Sequelize 等 ORM 框架..."
↓
拿這段話做向量檢索(效果更好,因為更接近真實文件的表達方式)
原理:假答案的向量比短問題的向量更接近真實文件的向量。
注意:HyDE 生成的長文本適合用於向量檢索,若用於 BM25 時需要先提取關鍵字:
// 向量檢索:直接用假設答案的完整語意
const vectorResults = await vectorStore.similaritySearch(hypotheticalAnswer, 20);
// BM25 檢索:先提取關鍵字再檢索
const keywords = ["mysql2", "Prisma", "連線池", "ORM", "pg"];
const bm25Results = await bm25.search(keywords.join(" "), 20);
做法三:問題分解(複雜問題)
原始問題: "Prisma 和 Sequelize 哪個更適合 Node.js 新專案"
↓ LLM 拆解
[
"Prisma ORM 的優缺點",
"Sequelize ORM 的優缺點",
"Prisma 和 Sequelize 效能比較",
"2024 年 Node.js ORM 選型建議"
]
每個子問題單獨檢索 → 匯總結果 → LLM 綜合回答
在 Agent 架構中,RAG 不再是簡單的「查一次」,而是作為 Tool(工具)被 Agent 按需呼叫,賦予 Agent 動態存取外部知識的能力。
最經典的場景,RAG 作為 Agent 的知識外挂:
使用者: "我們公司的年假政策是什麼?"
Agent: 呼叫 search_knowledge_base("年假政策")
→ 檢索 HR 內部文件
→ 生成準確答案(而非 LLM 瞎猜)
程式碼向量化後,Agent 能理解整個程式碼倉庫的語意:
使用者: "幫我找到處理使用者登入的函式"
Agent: 呼叫 search_codebase("使用者登入驗證")
→ 回傳相關程式碼片段及檔案位置
→ 分析並解釋程式碼邏輯
Agent 自主決定多次檢索,整合多個來源:
使用者: "幫我分析競爭對手的產品策略"
Agent:
1. search_web("competitor A product 2024") → 檢索網頁內容
2. search_internal_reports("市場分析") → 檢索內部報告
3. 綜合兩次檢索結果 → 生成完整分析報告
Agent 將歷史對話存入向量庫,實現真正的「記住使用者」:
// 儲存使用者偏好
await memoryStore.save({
userId: "user_123",
content: "使用者偏好用 TypeScript,不喜歡 callback 風格",
embedding: await embed("使用者偏好 TypeScript...")
});
// 下次對話時檢索相關記憶,實現個性化回應
const memories = await memoryStore.recall(userId, currentQuestion);
代表框架:mem0。
當 Agent 有數百個可用工具時,全部塞進 Prompt 會超出上下文長度限制:
使用者意圖
↓ RAG 檢索最相關的 10 個工具定義
注入 Prompt
↓ LLM 選擇呼叫哪個工具
目前 Node.js 生態中最成熟的 RAG/Agent 框架,提供完整工具鏈:
import { ChatOpenAI } from "@langchain/openai";
import { OpenAIEmbeddings } from "@langchain/openai";
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
import { EnsembleRetriever } from "langchain/retrievers/ensemble";
專注於資料索引與檢索的框架,對 RAG 場景優化更深:
入門建議:先用 LangChain.js,生態最完整,遇到問題最容易找到答案。
import { Chroma } from "@langchain/community/vectorstores/chroma";
// 零配置,像 SQLite 一樣嵌入式運行
const vectorStore = await Chroma.fromDocuments(docs, embeddings, {
collectionName: "my-docs",
persistDirectory: "./chroma-data",
});
-- 開啟擴充
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;
// 原生混合檢索,一行搞定
const results = await weaviate.graphql.get()
.withClassName("Document")
.withHybrid({
query: "Node.js 連接資料庫",
alpha: 0.6, // 0=純BM25, 1=純向量
})
.withLimit(5)
.do();
// 配置最簡單,沒有任何伺服器要管
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 });
比較概要(簡化):
Embedding 模型負責將文字轉為向量,是 RAG 品質的基礎。
模型提供方與特性:
選型建議:
使用者提問
↓
Query Rewriting 將模糊問題擴展為多個精準查詢
↓
Hybrid Search 向量檢索(語意) + BM25(關鍵字)→ RRF 融合
↓
Re-ranking Cross-encoder 精準排序,淘汰低品質結果
↓
Prompt 增強 將 Top K 文件注入上下文
↓
LLM 生成 基於真實資料生成有據可查的答案