iT邦幫忙

2025 iThome 鐵人賽

DAY 18
0
生成式 AI

用 Node.js 打造生成式 AI 應用:從 Prompt 到 Agent 開發實戰系列 第 18

Day 18 - 檢索器封裝:讓向量檢索成為可呼叫的工具

  • 分享至 

  • xImage
  •  

在前幾篇文章中,我們已經學會了如何載入資料、分割文件、建立向量資料庫,並透過相似度搜尋找出最相關的內容。不過,這些檢索流程目前仍是各自獨立的操作,尚未真正融入到 AI 應用之中。

今天我們要更進一步,將向量資料庫的查詢功能封裝成 檢索器(Retriever),建立一個統一的檢索介面,讓系統能具備可重複使用的檢索能力。同時,還要把它轉換成 LLM 可直接呼叫的 工具(Tool),使 AI 不再只是被動接收資料,而能在需要時主動發起檢索,自行找到答案。

什麼是檢索器?

在 LangChain 中,檢索器 是一個專門處理「接收查詢 → 回傳相關文件」的介面。它的角色就像應用程式與底層向量資料庫之間的橋樑,將語意搜尋的細節封裝起來,讓開發者能透過一致的方式進行檢索,而不必關心不同資料庫的實作差異。

Retriever 的主要特點包括:

  • 封裝搜尋邏輯:將向量資料庫的相似度計算與查詢流程隱藏在介面內,使用者不必直接操作資料庫 API。
  • 自然語言輸入:只需傳入一段查詢文字,Retriever 會自動完成向量化並執行檢索。
  • 結構化輸出:結果以 Document[] 的形式回傳,每份文件都能附帶來源與 metadata,方便後續使用或追溯。

相較於直接呼叫 VectorStoresimilaritySearchRetriever 提供了更高層級的抽象,也更容易替換不同的向量資料庫。

同時,檢索器本質上就是一個「接收輸入 → 產生輸出」的可執行單元,因此在 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);

在這個範例中:

  1. 先使用 MemoryVectorStore 建立了一個簡單的向量資料庫,並存入兩筆文件。
  2. 接著透過 .asRetriever() 轉換成 Retriever
  3. 最後,呼叫 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 回答流程

單純的 Retriever 只能回傳相關文件,但在實際應用中,我們通常希望能將檢索到的內容交給 LLM,讓模型在回答時能同時參考外部知識,而不是只依靠訓練資料。這種結合方式能讓 AI 回覆更貼近真實需求,也能即時更新知識來源。

在 LangChain 中,我們可以透過 LCEL 將 Retriever 與 LLM 串接成一個完整流程。典型的步驟如下:

  1. 使用者提問:輸入自然語言問題。
  2. Retriever 檢索:找到最相關的文件片段。
  3. 組合提示詞:把檢索結果與使用者問題一起放入 LLM。
  4. 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 可呼叫的 Tool

在前面的範例中,我們是由流程明確呼叫 Retriever,將查詢結果傳遞給 LLM。但在某些應用場景下,我們會希望讓 LLM 自行決定何時需要檢索,而不是每次都固定查詢。

為了達成這個目標,可以將 Retriever 封裝成一個 Tool,並且為它提供清楚的使用說明。如此一來,LLM 在推理過程中,能夠依據上下文判斷是否需要使用該 Tool,取得外部知識後再繼續生成回覆。

將 Retriever 封裝為 Tool 的目的包括:

  • 彈性更高:模型可以在需要時才調用檢索工具,而不是每次都強制檢索。
  • 多工具協作:除了檢索工具,LLM 還能同時使用其他工具(例如計算器、API 查詢、翻譯器),組成更完整的應用場景。
  • 模組化設計:檢索邏輯與 LLM 主流程解耦,讓系統更容易維護與擴展。

使用 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 能主動檢索並引用外部知識:

  • 檢索器(Retriever)是應用程式與向量資料庫的橋樑,提供統一介面,隱藏搜尋細節。
  • Retriever 支援自然語言輸入與結構化輸出,並能透過 .invoke().getRelevantDocuments() 直接檢索。
  • 可以透過 .asRetriever()VectorStore 轉換成 Retriever,並支援 kfilter 等參數控制檢索行為。
  • 在 LangChain 流程中,Retriever 能與 LLM 串接成 RAG 流程:提問 → 檢索文件 → 組合提示詞 → 生成答案。
  • 透過 createRetrieverTool(),我們能把 Retriever 封裝為 Tool,讓 LLM 自行判斷何時需要使用檢索。
  • 封裝後的好處:工具化、模組化、彈性更高,並能與其他工具協作,提升 AI 的可用性與擴展性。

透過這一步,我們不只是把檢索功能加進應用,而是讓 AI 擁有「何時檢索」的自主判斷,成為能靈活運用知識的智慧助理。


上一篇
Day 17 - 嵌入模型與向量資料庫:建構可語意檢索的 AI 知識庫
下一篇
Day 19 - RAG 實戰應用:打造可檢索公司年報的 AI 問答系統
系列文
用 Node.js 打造生成式 AI 應用:從 Prompt 到 Agent 開發實戰22
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言