在前幾篇文章中,我們已經學會了如何載入資料、分割文件、建立向量資料庫,並透過相似度搜尋找出最相關的內容。不過,這些檢索流程目前仍是各自獨立的操作,尚未真正融入到 AI 應用之中。
今天我們要更進一步,將向量資料庫的查詢功能封裝成 檢索器(Retriever),建立一個統一的檢索介面,讓系統能具備可重複使用的檢索能力。同時,還要把它轉換成 LLM 可直接呼叫的 工具(Tool),使 AI 不再只是被動接收資料,而能在需要時主動發起檢索,自行找到答案。
在 LangChain 中,檢索器 是一個專門處理「接收查詢 → 回傳相關文件」的介面。它的角色就像應用程式與底層向量資料庫之間的橋樑,將語意搜尋的細節封裝起來,讓開發者能透過一致的方式進行檢索,而不必關心不同資料庫的實作差異。
Retriever
的主要特點包括:
Retriever
會自動完成向量化並執行檢索。Document[]
的形式回傳,每份文件都能附帶來源與 metadata,方便後續使用或追溯。相較於直接呼叫 VectorStore
的 similaritySearch
,Retriever
提供了更高層級的抽象,也更容易替換不同的向量資料庫。
同時,檢索器本質上就是一個「接收輸入 → 產生輸出」的可執行單元,因此在 LangChain 中也實作了 Runnable
介面。這意味著你可以用統一的 .invoke()
方法呼叫它:
const docs = await retriever.invoke('退貨政策');
這種方式讓檢索器能像 LangChain 其他元件一樣,無縫串接進 LCEL 流程,使檢索與生成自然結合,形成高度模組化且可擴充的應用架構。
如果偏好更直觀的寫法,也可以使用:
const docs = await retriever.getRelevantDocuments('退貨政策');
無論哪種方式,最後都會回傳一組 Document[]
,其中包含最符合查詢的內容與相關資訊,供後續流程使用。
VectorStore
建立 Retriever
在 LangChain 中,VectorStore
介面本身雖然能提供檢索功能,但通常我們會將它轉換成 Retriever
,以便在更高層級的流程中使用。這樣一來,檢索功能就能以統一的介面被串接到 Chain 或 Agent,而不受限於底層資料庫的細節。
假設我們已經建立好一個 VectorStore
,可以直接透過 .asRetriever()
方法轉換成檢索器:
import { OpenAIEmbeddings } from '@langchain/openai';
import { MemoryVectorStore } from 'langchain/vectorstores/memory';
// 建立一個簡單的向量資料庫
const vectorStore = await MemoryVectorStore.fromTexts(
[
'Node.js 是一個後端 JavaScript 執行環境',
'LangChain 提供 LLM 開發工具'
],
[
{ source: 'doc1' },
{ source: 'doc2' }
],
new OpenAIEmbeddings()
);
// 將 VectorStore 轉換成 Retriever
const retriever = vectorStore.asRetriever();
// 使用 Retriever 進行檢索
const docs = await retriever.invoke('什麼是 LangChain?');
console.log(docs);
在這個範例中:
MemoryVectorStore
建立了一個簡單的向量資料庫,並存入兩筆文件。.asRetriever()
轉換成 Retriever
。retriever.invoke()
並輸入自然語言查詢,就能獲得相關文件結果。這裡的 Retriever
其實扮演著資料存取抽象層的角色。換句話說,不管背後是用記憶體、Postgres、Pinecone 或其他向量資料庫,對外的檢索方式都可以統一使用 Retriever
介面,因此能大幅降低系統整合的複雜度。
.asRetriever()
其實可以接收一些參數,用來調整檢索的方式:
// 取回前 3 筆文件,並根據 metadata 過濾
const retriever = vectorStore.asRetriever({
k: 3,
filter: (doc) => doc.metadata.source === 'doc2',
});
常見參數包括:
k
:指定每次查詢要回傳的文件數量。filter
:過濾條件,可以依照文件的 metadata 來篩選,例如只查詢來源為 doc2
的資料。這讓我們能更靈活地控制檢索結果,避免一次回傳過多或不相關的文件。
單純的 Retriever 只能回傳相關文件,但在實際應用中,我們通常希望能將檢索到的內容交給 LLM,讓模型在回答時能同時參考外部知識,而不是只依靠訓練資料。這種結合方式能讓 AI 回覆更貼近真實需求,也能即時更新知識來源。
在 LangChain 中,我們可以透過 LCEL 將 Retriever 與 LLM 串接成一個完整流程。典型的步驟如下:
以下是一個簡單範例:
import { ChatOpenAI } from '@langchain/openai';
import { PromptTemplate } from '@langchain/core/prompts';
import { RunnableSequence } from '@langchain/core/runnables';
import { StringOutputParser } from '@langchain/core/output_parsers';
// 假設我們已經有 retriever
import { retriever } from './retriever.js';
const llm = new ChatOpenAI({
model: 'gpt-4o-mini',
temperature: 0,
});
const prompt = PromptTemplate.fromTemplate(`
你是一個知識型助理,請根據以下文件內容回答問題。
如果文件中沒有相關資訊,請直接回答「文件中沒有提到」。
文件內容:
{context}
問題:
{question}
`);
const parser = new StringOutputParser();
const chain = RunnableSequence.from([
{
context: retriever,
question: (input) => input.question,
},
prompt,
llm,
parser,
]);
const result = await chain.invoke({ question: '什麼是 LangChain?' });
console.log(result);
在這個流程中,Retriever 專注於「找到相關內容」,而 LLM 負責「根據上下文生成答案」。透過 LCEL,這兩個部分可以被組合成可重複使用的模組,構成 RAG(Retrieval-Augmented Generation) 的基礎架構。
這樣,我們的 AI 助理就能具備檢索增強能力,回答問題時不再只依靠模型本身的知識,而是能即時引用外部資料庫中的內容,讓回覆更完整、更可靠。
在前面的範例中,我們是由流程明確呼叫 Retriever,將查詢結果傳遞給 LLM。但在某些應用場景下,我們會希望讓 LLM 自行決定何時需要檢索,而不是每次都固定查詢。
為了達成這個目標,可以將 Retriever 封裝成一個 Tool,並且為它提供清楚的使用說明。如此一來,LLM 在推理過程中,能夠依據上下文判斷是否需要使用該 Tool,取得外部知識後再繼續生成回覆。
將 Retriever 封裝為 Tool 的目的包括:
createRetrieverTool()
封裝LangChain 提供了 createRetrieverTool()
方法,可以將任何 Retriever 轉換成 Tool 物件。建立時只需要提供三個關鍵參數:
retriever
:實際的檢索器實例。name
:Tool 的名稱。description
:告訴 LLM 這個工具的用途,方便它決定何時使用。範例如下:
import { createRetrieverTool } from '@langchain/core/tools';
const retrieverTool = createRetrieverTool(retriever, {
name: 'knowledge_search',
description: '查詢內部知識庫以獲取與問題相關的資訊'
});
接著,就能像綁定其他工具一樣,把這個 Tool 綁定到 LLM:
import { ChatOpenAI } from '@langchain/openai';
import { retrieverTool } from './retrieverTool.js';
const llmWithTools = new ChatOpenAI({
model: 'gpt-4o-mini',
temperature: 0,
}).bindTools([retrieverTool]);
當 LLM 判斷需要額外知識時,它會在回應過程中觸發對 knowledge_search
工具的呼叫。這個過程並不是 LLM 直接回傳最終答案,而是先生成一個 Tool Calling 請求,其中包含工具名稱與查詢內容。
LangChain 收到這個請求後,就可以執行對應的 Retriever,實際進行查詢(例如 retriever.invoke(query)
),並將檢索結果回傳給 LLM。接著,LLM 再根據這些檢索到的文件內容,繼續生成最終的回答。
換句話說,整體流程是一個「呼叫工具 → 執行檢索 → 回傳結果 → 產生回覆」的閉環迭代。透過這樣的設計,LLM 不再只是依靠訓練語料作答,而是能在需要時主動引入外部知識,使最終回應更完整、更可靠。
今天我們把向量資料庫的查詢流程進一步封裝成 檢索器(Retriever),並讓它成為 LLM 可直接呼叫的 工具(Tool),讓 AI 能主動檢索並引用外部知識:
.invoke()
或 .getRelevantDocuments()
直接檢索。.asRetriever()
將 VectorStore
轉換成 Retriever
,並支援 k
、filter
等參數控制檢索行為。createRetrieverTool()
,我們能把 Retriever 封裝為 Tool,讓 LLM 自行判斷何時需要使用檢索。透過這一步,我們不只是把檢索功能加進應用,而是讓 AI 擁有「何時檢索」的自主判斷,成為能靈活運用知識的智慧助理。