本文 AI 產出,尚未審核

LangChain 教學:自訂工具 Class 與回呼(Callbacks)

簡介

在 LangChain 的 Tools(工具)系統 中,工具是讓 LLM(大型語言模型)執行外部動作的橋樑,例如查詢資料庫、呼叫 API、或是觸發自訂程式邏輯。預設提供的工具雖然已經相當好用,但在真實專案裡,我們常常需要 自訂工具,以符合業務需求或整合既有系統。

此外,回呼(Callback) 機制讓開發者可以在工具執行的前後插入自訂程式碼,實作日誌、計時、錯誤處理或是動態調整參數。掌握工具與回呼的設計,等於掌握了將 LLM 與外部資源安全、彈性地結合的關鍵。

本篇文章將從概念說明、實作範例到最佳實踐,完整帶你打造 自訂工具 Class 並配合 回呼 使用,適合剛入門 LangChain 的新手,也能為中階開發者提供可直接上線的參考範例。


核心概念

1. LangChain 中的「工具」是什麼?

  • 工具 (Tool):一段可被 LLM 呼叫的程式碼,通常遵守 Tool 介面,至少包含 namedescriptionrun(input: string): Promise<string> 三個屬性/方法。
  • 作用:把自然語言指令轉換成具體的程式行為,讓模型不必直接寫程式碼,而是透過「工具」完成任務。

舉例SearchTool 讓模型可以在網路上搜尋關鍵字;CalculatorTool 讓模型執行算術運算。

2. 為什麼需要自訂工具?

  • 業務需求:例如公司內部的 ERP 系統、CRM API、或是特殊的資料清理流程,這些都不是 LangChain 預設的工具。
  • 安全與授權:自訂工具可以內建驗證、權限檢查,避免模型直接執行危險指令。
  • 彈性擴充:透過自訂工具,我們可以把任何 Node.js / TypeScript 函式包裝成 LLM 可呼叫的介面。

3. 回呼(Callback)機制

LangChain 提供 CallbackManager,允許開發者在工具的 onStartonSuccessonError 等階段掛鉤自訂函式。常見用途包括:

  • 記錄執行時間與參數
  • 監控錯誤並自動重試
  • 動態改變工具的行為(例如根據使用者身份切換資料庫)

核心概念:回呼不會改變工具本身的實作,只是「觀察」或「增強」它的執行流程。


程式碼範例

以下範例使用 LangChainJS(Node.js)編寫,示範如何:

  1. 建立自訂工具 Class
  2. 為工具掛上回呼
  3. 在 Chain 中使用自訂工具

:在實務專案中,請先安裝 langchain 套件

npm install langchain

1. 基本自訂工具範例:呼叫公司內部 API

// file: MyApiTool.js
import { Tool } from "langchain/tools";

/**
 * MyApiTool - 讓 LLM 能呼叫公司內部的 /users/:id API
 */
export class MyApiTool extends Tool {
  constructor() {
    super();
    this.name = "MyApiTool";
    this.description = "取得指定使用者的詳細資訊,參數為使用者 ID。";
  }

  /**
   * @param {string} input - 需傳入的使用者 ID
   * @returns {Promise<string>}
   */
  async _call(input) {
    // 假設有一個已封裝好的 HTTP client
    const response = await fetch(`https://api.mycompany.com/users/${input}`, {
      headers: { Authorization: `Bearer ${process.env.API_TOKEN}` },
    });
    const data = await response.json();
    // 回傳簡化的文字描述,方便 LLM 處理
    return `使用者 ${data.name}(${data.email}),職稱 ${data.title}`;
  }
}

說明

  • 只需要繼承 Tool 並實作 _call 方法。
  • namedescription 會被 LLM 用來理解工具的用途。
  • 使用環境變數保護 API 金鑰,提升安全性。

2. 加入回呼:記錄執行時間與錯誤

// file: callbackExample.js
import { CallbackManager } from "langchain/callbacks";
import { MyApiTool } from "./MyApiTool";

// 建立 CallbackManager
const callbackManager = new CallbackManager();

// onStart:工具執行前
callbackManager.addHandler({
  async onToolStart(tool, input) {
    console.log(`[${new Date().toISOString()}] Tool "${tool.name}" start with input: ${input}`);
    // 可放入計時器
    tool._startTime = Date.now();
  },

  // onSuccess:工具成功返回結果
  async onToolEnd(tool, output) {
    const duration = Date.now() - tool._startTime;
    console.log(`[${new Date().toISOString()}] Tool "${tool.name}" succeeded in ${duration}ms`);
    console.log(`Output: ${output}`);
  },

  // onError:工具拋出例外
  async onToolError(tool, error) {
    console.error(`[${new Date().toISOString()}] Tool "${tool.name}" error:`, error);
  },
});

// 把回呼注入工具
const apiTool = new MyApiTool();
apiTool.callbackManager = callbackManager;

// 測試呼叫
(async () => {
  const result = await apiTool.run("12345");
  console.log("最終結果:", result);
})();

關鍵點

  • callbackManager 可以同時管理多個回呼,且每個工具只要指派 callbackManager 即可。
  • 透過 tool._startTime 暫存資料,示範如何在不同階段共享狀態。

3. 結合 LLM 與自訂工具:簡易 QA Chain

// file: qaChain.js
import { OpenAI } from "langchain/llms/openai";
import { initializeAgentExecutorWithOptions } from "langchain/agents";
import { MyApiTool } from "./MyApiTool";
import { CallbackManager } from "langchain/callbacks";

const llm = new OpenAI({ temperature: 0 });
const apiTool = new MyApiTool();

// 共享同一個 CallbackManager(可同時監控 LLM 與工具)
const callbackManager = new CallbackManager();
callbackManager.addHandler({
  async onLLMStart(llm, prompts) {
    console.log("LLM 開始生成回應");
  },
  async onLLMEnd(llm, response) {
    console.log("LLM 完成回應");
  },
});
apiTool.callbackManager = callbackManager;

// 建立 Agent(工具驅動的對話代理)
const executor = await initializeAgentExecutorWithOptions(
  [apiTool],
  llm,
  {
    agentType: "zero-shot-react-description",
    callbackManager,
  }
);

// 使用者提問
const userQuestion = "請告訴我 John Doe 的職稱與電子郵件。";

(async () => {
  const result = await executor.run(userQuestion);
  console.log("最終答案:", result);
})();

說明

  • initializeAgentExecutorWithOptions 會自動根據工具的 description 生成 React 風格的思考過程(思考 → 呼叫工具 → 回答)。
  • 透過同一個 callbackManager,我們可以同時觀察 LLM 與工具的執行,達到完整的除錯與監控。

4. 動態參數:根據使用者角色切換不同 API

// file: RoleBasedTool.js
import { Tool } from "langchain/tools";

export class RoleBasedTool extends Tool {
  constructor() {
    super();
    this.name = "RoleBasedTool";
    this.description = "根據使用者角色取得對應的資料。輸入格式:<role>::<id>";
  }

  async _call(input) {
    const [role, id] = input.split("::");
    // 依角色切換不同的 endpoint
    const endpoint = role === "admin"
      ? `/admin/users/${id}`
      : `/users/${id}`;

    const response = await fetch(`https://api.mycompany.com${endpoint}`, {
      headers: { Authorization: `Bearer ${process.env.API_TOKEN}` },
    });
    const data = await response.json();
    return JSON.stringify(data);
  }
}

應用:在回呼的 onToolStart 中,我們甚至可以檢查使用者的權限,若不符合則直接拋出錯誤,避免工具被濫用。

5. 重試機制:利用回呼自動重試失敗的 API 呼叫

// file: retryCallback.js
import { CallbackManager } from "langchain/callbacks";

const MAX_RETRY = 3;

const retryHandler = {
  async onToolError(tool, error) {
    if (!tool._retryCount) tool._retryCount = 0;
    if (tool._retryCount < MAX_RETRY) {
      tool._retryCount += 1;
      console.warn(`工具 ${tool.name} 發生錯誤,嘗試第 ${tool._retryCount} 次重試...`);
      // 重新呼叫工具
      const lastInput = tool._lastInput;
      try {
        const output = await tool.run(lastInput);
        console.log(`第 ${tool._retryCount} 次重試成功`);
        // 觸發成功的回呼
        await tool.callbackManager?.handleToolEnd?.(tool, output);
      } catch (e) {
        await tool.callbackManager?.handleToolError?.(tool, e);
      }
    } else {
      console.error(`工具 ${tool.name} 重試次數已達上限,拋出錯誤`);
    }
  },

  async onToolStart(tool, input) {
    tool._lastInput = input; // 記錄最後一次的輸入供重試使用
  },
};

export const retryCallbackManager = new CallbackManager();
retryCallbackManager.addHandler(retryHandler);

使用方式

const apiTool = new MyApiTool();
apiTool.callbackManager = retryCallbackManager;
// 接下來的呼叫若失敗會自動重試最多 3 次

常見陷阱與最佳實踐

常見問題 可能原因 解決方案
工具回傳空字串或 null _call 沒有正確 return,或 API 回傳非 200 狀態 確認 await response.ok,必要時拋出錯誤讓回呼捕捉
回呼無法觸發 工具未設定 callbackManager,或回呼名稱寫錯 (onToolStart vs onToolStart) 在工具實例化後必須設定 tool.callbackManager = yourManager
回呼中拋出例外導致整個 Chain 中斷 回呼本身的程式碼錯誤 在回呼內部使用 try/catch 包裹,或在 CallbackManager 中加入 onError 處理
工具執行時間過長 後端 API 延遲、缺少 Timeout 設定 使用 AbortController 設定請求超時,或在 onToolStart 記錄開始時間、在 onToolEnd 計算耗時
安全風險:工具被濫用 run 直接接受使用者原始字串 _call 前先 驗證正規化 輸入,並在回呼中檢查使用者權限

最佳實踐

  1. 明確的 description:LLM 依賴描述來決定何時呼叫工具,描述越精確,誤呼叫的機率越低。
  2. 分層錯誤處理:在工具內部先捕捉 API 錯誤,然後拋出自訂的 ToolError,讓回呼負責統一記錄與重試。
  3. 回呼最小化副作用:回呼的職責應該是觀察增強,避免在回呼裡直接改變工具的業務邏輯,保持程式碼可讀性。
  4. 統一的 CallbackManager:在大型應用中,將 LLM、工具、Chain 都掛上同一個 CallbackManager,方便集中監控與除錯。
  5. 環境變數與密鑰管理:絕對不要把 API 金鑰硬寫在程式碼,使用 .env 或雲端密鑰管理服務(如 AWS Secrets Manager)載入。

實際應用場景

場景 為什麼需要自訂工具 & 回呼
客服機器人 需要呼叫 CRM 系統查詢客戶訂單,並在每次查詢後記錄操作日誌供稽核。
內部資料分析平台 LLM 產生的分析指令必須透過自訂 SQL 執行工具,回呼負責計算查詢耗時並自動寫入監控系統。
自動化報表產出 工具負責從 ERP 抓取月度銷售資料,回呼在成功取得資料後觸發 Slack 通知與檔案上傳。
安全稽核 在金融系統中,工具必須先驗證使用者角色,回呼可即時將異常呼叫寫入 SIEM 系統。
多語言翻譯服務 LLM 產生翻譯需求,工具呼叫外部翻譯 API,回呼負責統計每日翻譯字數,避免超額。

總結

  • 自訂工具 Class 讓 LangChain 能夠無縫整合公司內部或第三方服務,只要遵守 Tool 介面即可。
  • 回呼(Callback) 是觀察、增強與保護工具執行的核心機制,透過 CallbackManager 我們可以在工具前後加入日誌、計時、重試與安全檢查。
  • 實作時,務必 提供完整且精確的 description做好輸入驗證,以及 在回呼中處理錯誤與資源釋放,才能讓 LLM 與外部系統的互動既安全又可靠。
  • 透過本文的範例,你已經能夠在自己的專案中 快速打造監控重試安全地使用 自訂工具,為 LLM 應用開啟更多可能性。

祝你在 LangChain 的開發旅程中玩得開心,打造出更智慧、更可靠的 AI 服務! 🚀