LangChain 課程 – Retrieval 與資料查詢
主題:Retriever 介面
簡介
在 LLM(大型語言模型)與外部知識結合的應用中,檢索(Retrieval) 扮演關鍵角色。沒有適當的檢索機制,模型只能依賴自身的參數,難以提供即時、正確且具體的資訊。LangChain 為開發者提供了一套統一的 Retriever 介面,讓不同類型的資料來源(向量資料庫、全文搜尋、資料庫等)可以以相同的方式被呼叫,進而與 LLM 結合形成 Retrieval‑Augmented Generation(RAG)流程。
本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,帶領讀者一步步掌握 LangChain 中的 Retriever 介面,並瞭解它在真實專案中的應用方式。即使你是剛接觸 LangChain 的初學者,亦能在閱讀完後自行建立、客製化或組合不同的檢索器,快速為自己的聊天機器人或問答系統注入外部知識。
核心概念
1. Retriever 介面的定位
在 LangChain(以 JavaScript / TypeScript 版 langchainjs 為例)中,Retriever 是一個 抽象類別,定義了以下兩個最重要的方法:
| 方法 | 說明 | 回傳型別 |
|---|---|---|
getRelevantDocuments(query: string): Promise<Document[]> |
依據使用者的查詢文字,回傳一組相關的 Document 物件。 |
Promise<Document[]> |
addDocument(document: Document): Promise<void>(可選) |
動態加入新文件到檢索資料庫。 | Promise<void> |
所有具體的檢索器(如向量檢索、BM25、SQL 檢索)都必須 實作 這兩個方法,LangChain 之所以能提供一致的 API,正是因為它們遵循同一套介面。
重點:只要你的類別符合
Retriever介面的簽名,就可以直接在ConversationalRetrievalChain、RetrievalQAChain等高階鏈中使用,無需再額外寫轉接程式。
2. 內建的 Retriever 種類
LangChain 已經為常見的資料來源實作了多種 Retriever,以下列出最常使用的三種:
| Retriever | 資料來源 | 特色 |
|---|---|---|
VectorStoreRetriever |
向量資料庫(如 Pinecone、Weaviate、FAISS) | 透過向量相似度搜尋,適合語意搜尋。 |
BM25Retriever |
本機或遠端的全文索引(如 Elasticsearch) | 基於詞頻-逆文頻(TF‑IDF)與 BM25 計分,對於關鍵字搜尋表現佳。 |
SQLRetriever |
關聯式資料庫(如 PostgreSQL、MySQL) | 可直接以 SQL 查詢語句取得相關記錄,適合結構化資料。 |
下面的程式碼示範如何快速建立這三種 Retriever(使用 langchainjs):
// 安裝必要套件
// npm install langchain @pinecone-database/pinecone elasticsearch pg
2.1 VectorStoreRetriever(以 Pinecone 為例)
import { PineconeClient } from "@pinecone-database/pinecone";
import { OpenAIEmbeddings } from "langchain/embeddings/openai";
import { PineconeStore } from "langchain/vectorstores/pinecone";
import { VectorStoreRetriever } from "langchain/retrievers/vectorstore";
// 1. 建立 Pinecone 客戶端
const pinecone = new PineconeClient();
await pinecone.init({
environment: "us-west1-gcp",
apiKey: process.env.PINECONE_API_KEY,
});
// 2. 建立向量存儲(Vector Store)
const embeddings = new OpenAIEmbeddings({ openAIApiKey: process.env.OPENAI_API_KEY });
const vectorStore = await PineconeStore.fromExistingIndex(embeddings, {
pineconeIndex: pinecone.Index("my-index"),
namespace: "langchain-demo",
});
// 3. 建立 Retriever
const retriever = new VectorStoreRetriever({
vectorStore,
k: 4, // 只取最相近的 4 筆文件
});
2.2 BM25Retriever(以 Elasticsearch 為例)
import { ElasticsearchStore } from "langchain/documentstores/elasticsearch";
import { BM25Retriever } from "langchain/retrievers/bm25";
// 1. 建立 Elasticsearch 連線
const esStore = new ElasticsearchStore({
node: "http://localhost:9200",
indexName: "langchain-docs",
});
// 2. 建立 BM25 Retriever
const bm25Retriever = new BM25Retriever({
documentStore: esStore,
k: 5, // 取前 5 筆相關文件
});
2.3 SQLRetriever(以 PostgreSQL 為例)
import { PostgresDocumentStore } from "langchain/documentstores/postgres";
import { SQLRetriever } from "langchain/retrievers/sql";
// 1. 建立 PostgreSQL 文件存儲
const pgStore = new PostgresDocumentStore({
connectionString: process.env.POSTGRES_URL,
tableName: "documents",
});
// 2. 建立 SQL Retriever
const sqlRetriever = new SQLRetriever({
documentStore: pgStore,
// 這裡的 queryBuilder 讓你自訂 SQL 語句的生成方式
queryBuilder: (question) => ({
text: `SELECT content FROM documents WHERE content ILIKE $1 LIMIT 5`,
values: [`%${question}%`],
}),
});
3. 客製化 Retriever:從頭實作一個簡易檔案系統檢索器
有時候你可能想要 自行實作 Retriever,像是從本機檔案系統讀取 Markdown 檔,或是結合自訂的演算法。下面示範如何繼承 BaseRetriever(LangChain 提供的抽象基礎類別)來完成這件事。
import { BaseRetriever } from "langchain/retrievers/base";
import { Document } from "langchain/document";
import fs from "fs/promises";
import path from "path";
/**
* SimpleFileRetriever
* - 讀取指定資料夾內的所有 .md 檔案
* - 使用關鍵字匹配(簡易版)回傳相關文件
*/
export class SimpleFileRetriever extends BaseRetriever {
/** 資料夾路徑 */
private folderPath: string;
constructor(folderPath: string) {
super();
this.folderPath = folderPath;
}
/** 讀取所有 markdown 檔並轉成 Document 陣列 */
private async loadAllDocuments(): Promise<Document[]> {
const files = await fs.readdir(this.folderPath);
const mdFiles = files.filter((f) => f.endsWith(".md"));
const docs: Document[] = [];
for (const file of mdFiles) {
const content = await fs.readFile(path.join(this.folderPath, file), "utf-8");
docs.push(new Document({ pageContent: content, metadata: { source: file } }));
}
return docs;
}
/** 依關鍵字過濾文件 */
async getRelevantDocuments(query: string): Promise<Document[]> {
const allDocs = await this.loadAllDocuments();
// 簡易關鍵字匹配:只要文件內容包含 query 即回傳
const lowerQuery = query.toLowerCase();
const filtered = allDocs.filter((doc) =>
doc.pageContent.toLowerCase().includes(lowerQuery)
);
// 只回傳前 3 筆(避免過多回傳)
return filtered.slice(0, 3);
}
/** 可選:動態新增文件 */
async addDocument(document: Document): Promise<void> {
const targetPath = path.join(this.folderPath, `${Date.now()}.md`);
await fs.writeFile(targetPath, document.pageContent, "utf-8");
}
}
// 使用範例
const fileRetriever = new SimpleFileRetriever("./knowledge-base");
const results = await fileRetriever.getRelevantDocuments("LangChain");
console.log(results.map((d) => d.metadata.source));
小技巧:在正式專案中,建議使用 倒排索引(如
lunr.js、elasticlunr)取代簡易的字串includes,效能會提升數十倍。
4. 組合多個 Retriever:Hybrid Retrieval
單一檢索器往往只能捕捉到資料的某一面向。LangChain 提供 MultiRetriever(或自行寫一個簡易的 wrapper)讓你 同時查詢多個來源,再依分數或策略合併結果。
以下示範把 向量檢索 與 BM25 檢索 結合,先各自取前 3 筆,再依相似度分數排序:
import { BaseRetriever } from "langchain/retrievers/base";
/**
* HybridRetriever
* - 接收任意數量的 Retriever
* - 以 async 並行方式同時搜尋
* - 合併結果並依自訂分數排序
*/
export class HybridRetriever extends BaseRetriever {
private retrievers: BaseRetriever[];
private k: number; // 最終返回的文件數量
constructor(retrievers: BaseRetriever[], k = 5) {
super();
this.retrievers = retrievers;
this.k = k;
}
async getRelevantDocuments(query: string): Promise<Document[]> {
// 並行呼叫所有 Retriever
const promises = this.retrievers.map((r) => r.getRelevantDocuments(query));
const results = await Promise.all(promises);
// 將多個陣列扁平化
const allDocs = results.flat();
// 以簡易的「出現次數」作為分數(實務上可使用向量相似度或 BM25 分數)
const docMap = new Map<string, { doc: Document; score: number }>();
for (const doc of allDocs) {
const key = doc.metadata.source || doc.pageContent.slice(0, 30);
const entry = docMap.get(key);
if (entry) {
entry.score += 1; // 同一文件被多個檢索器命中,分數加 1
} else {
docMap.set(key, { doc, score: 1 });
}
}
// 依分數排序,取前 k 筆
const sorted = Array.from(docMap.values())
.sort((a, b) => b.score - a.score)
.slice(0, this.k)
.map((e) => e.doc);
return sorted;
}
}
// 組合向量與 BM25 檢索器
const hybrid = new HybridRetriever([retriever, bm25Retriever], 5);
const hyDocs = await hybrid.getRelevantDocuments("什麼是 RAG");
console.log(hyDocs.map((d) => d.metadata.source));
實務建議:若要在大型系統裡使用 Hybrid Retrieval,請將每個 Retriever 的
k設得較小(例如 3~5),再在合併階段做排序。這樣可以大幅降低每次查詢的計算成本。
常見陷阱與最佳實踐
| 陷阱 | 為何會發生 | 解決方式 / 最佳實踐 |
|---|---|---|
| 向量維度不一致 | 不同文件在嵌入時使用了不同的模型或參數,導致向量長度不同,向量資料庫會拋出錯誤。 | 統一使用同一套 Embedding(如 OpenAIEmbeddings)或在寫入前先 檢查向量長度。 |
| 檔案更新未即時同步 | 使用離線向量索引(FAISS)時,新增文件未重新建索引,檢索不到最新資料。 | 在新增文件後呼叫 vectorStore.addDocuments([...]) 並 re‑index,或改用支援即時寫入的雲端向量服務(Pinecone、Weaviate)。 |
| BM25 分數過低導致被過濾 | 預設 k 太小或 minScore 設定過高,導致相關文件被過濾。 |
調整 k、minScore,或在自訂 Retriever 時返回 原始分數,讓上層鏈自行決策。 |
| Hybrid Retrieval 產生重複文件 | 不同檢索器返回同一文件,最終結果中會有重複。 | 在合併階段使用 去重 (deduplication),如上例的 Map 實作。 |
忘記設定 metadata |
若文件沒有 metadata.source,後續除錯或追蹤來源會非常困難。 |
在建立 Document 時,務必加入 metadata: { source: "檔案路徑或 ID" }。 |
最佳實踐總結:
- 統一 Embedding:全系統使用同一套向量模型,避免維度不匹配。
- 批次寫入:大量文件一次寫入向量庫,可減少 API 呼叫成本。
- 分層檢索:先用 BM25 做粗檢索,再以向量相似度做精排,提升效能。
- 記錄分數:將檢索分數寫入
metadata,方便後續排序或分析。 - 測試與監控:使用小樣本驗證
Retriever正確性,並在生產環境加入查詢延遲與錯誤率監控。
實際應用場景
| 場景 | 使用的 Retriever | 為何選擇此組合 |
|---|---|---|
| 企業內部文件問答系統 | VectorStoreRetriever(FAISS) + SQLRetriever(公司資料庫) |
向量檢索處理非結構化文件(報告、手冊),SQL 檢索即時抓取結構化資料(客戶資訊、訂單)。 |
| 客服聊天機器人 | BM25Retriever(Elasticsearch) |
關鍵字搜尋速度快,且支援分詞與同義詞擴展,適合即時回應常見問題。 |
| 開發者文件輔助 | HybridRetriever(向量 + BM25) |
向量捕捉語意,BM25 確保關鍵字精準,提升搜尋正確率。 |
| 學術論文搜尋平台 | SimpleFileRetriever(本機 Markdown) + VectorStoreRetriever(Pinecone) |
本機檔案快速載入,向量檢索提供跨語言、跨領域的語意匹配。 |
| 多語言客服 | VectorStoreRetriever(多語言模型) + SQLRetriever(語言代碼表) |
向量模型支援多語言嵌入,SQL 用於根據語言代碼切換回應模板。 |
案例說明:假設要為一家金融公司建置「風控報告問答」系統,資料來源包括 PDF 報告(非結構化)與 MySQL 中的交易紀錄(結構化)。我們可以先使用
PyPDFLoader讀取 PDF,轉成向量存入 Pinecone,然後以HybridRetriever同時查詢 Pinecone(語意)與 MySQL(交易細節),最終把結果交給 LLM 產生自然語言回覆。這樣的設計不僅提升資訊完整性,也讓模型的回覆更具可信度。
總結
- Retriever 介面 為 LangChain 提供了統一的檢索抽象,使得 向量檢索、全文搜尋、結構化查詢 都能以相同的方式被 LLM 使用。
- 內建的
VectorStoreRetriever、BM25Retriever、SQLRetriever已能滿足大多數常見需求,若有特殊需求則可 自行實作,只要遵循getRelevantDocuments方法即可。 - 透過 Hybrid Retrieval,我們可以彈性結合多種檢索策略,兼顧速度與語意準確度。
- 實務上要注意 向量維度一致、即時同步、去重與分數管理,並遵循「批次寫入、分層檢索、記錄分數」的最佳實踐。
- 以上概念與範例可直接套用於企業內部知識庫、客服機器人、學術搜尋等多種場景,讓開發者快速打造具備 Retrieval‑Augmented Generation 能力的智慧應用。
掌握了 Retriever 介面的核心後,你就能在 LangChain 中自由組合各式檢索資源,為 LLM 注入最新、最相關的外部知識,提升模型的實用性與可靠度。祝你開發順利,玩得開心! 🚀