本文 AI 產出,尚未審核

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 介面的簽名,就可以直接在 ConversationalRetrievalChainRetrievalQAChain 等高階鏈中使用,無需再額外寫轉接程式。


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.jselasticlunr)取代簡易的字串 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 設定過高,導致相關文件被過濾。 調整 kminScore,或在自訂 Retriever 時返回 原始分數,讓上層鏈自行決策。
Hybrid Retrieval 產生重複文件 不同檢索器返回同一文件,最終結果中會有重複。 在合併階段使用 去重 (deduplication),如上例的 Map 實作。
忘記設定 metadata 若文件沒有 metadata.source,後續除錯或追蹤來源會非常困難。 在建立 Document 時,務必加入 metadata: { source: "檔案路徑或 ID" }

最佳實踐總結

  1. 統一 Embedding:全系統使用同一套向量模型,避免維度不匹配。
  2. 批次寫入:大量文件一次寫入向量庫,可減少 API 呼叫成本。
  3. 分層檢索:先用 BM25 做粗檢索,再以向量相似度做精排,提升效能。
  4. 記錄分數:將檢索分數寫入 metadata,方便後續排序或分析。
  5. 測試與監控:使用小樣本驗證 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 使用。
  • 內建的 VectorStoreRetrieverBM25RetrieverSQLRetriever 已能滿足大多數常見需求,若有特殊需求則可 自行實作,只要遵循 getRelevantDocuments 方法即可。
  • 透過 Hybrid Retrieval,我們可以彈性結合多種檢索策略,兼顧速度與語意準確度。
  • 實務上要注意 向量維度一致、即時同步、去重與分數管理,並遵循「批次寫入、分層檢索、記錄分數」的最佳實踐。
  • 以上概念與範例可直接套用於企業內部知識庫、客服機器人、學術搜尋等多種場景,讓開發者快速打造具備 Retrieval‑Augmented Generation 能力的智慧應用。

掌握了 Retriever 介面的核心後,你就能在 LangChain 中自由組合各式檢索資源,為 LLM 注入最新、最相關的外部知識,提升模型的實用性與可靠度。祝你開發順利,玩得開心! 🚀