LangChain ToolsとFunction Calling:自然言語による柔軟な制御の仕組み
はじめに
LangChainのToolsとFunction Callingは、LLMが外部ツールを呼び出すための強力なメカニズムです。特に、Descriptionに自然言語でプロンプトを記述することで、複雑なルールをコーディングすることなく、LLMにツールの使用方法を理解させることができます。この記事では、この仕組みがなぜ有効で優れているのか、従来のアプローチとの比較を通じて詳しく解説します。
ToolsとFunction Callingの基本概念
Function Callingとは
Function Callingは、LLMが外部の関数(ツール)を呼び出すための仕組みです。LLMは、自然言語で記述された関数の説明を理解し、適切なタイミングで関数を呼び出します。
基本的な構造
import { DynamicStructuredTool } from "@langchain/core/tools";
import { z } from "zod";
// ツールの定義
const tool = new DynamicStructuredTool({
name: "get_weather",
description: "指定された都市の現在の天気情報を取得します。都市名は完全な名前で指定してください。",
schema: z.object({
city: z.string().describe("天気を取得する都市名(例: 東京、New York)"),
}),
func: async ({ city }) => {
// 実際の実装
return `${city}の天気: 晴れ、気温22度`;
},
});
Descriptionにプロンプトを記述する重要性
従来のアプローチとの比較
従来のアプローチ:複雑なルールをコーディング
// 従来のアプローチ:複雑な条件分岐とルールをコーディング
class WeatherService {
async getWeather(city) {
// 都市名の正規化
const normalizedCity = this.normalizeCityName(city);
// バリデーション
if (!this.isValidCity(normalizedCity)) {
throw new Error("無効な都市名です");
}
// キャッシュチェック
if (this.cache.has(normalizedCity)) {
return this.cache.get(normalizedCity);
}
// API呼び出し
const weather = await this.apiClient.getWeather(normalizedCity);
// キャッシュに保存
this.cache.set(normalizedCity, weather);
return weather;
}
normalizeCityName(city) {
// 複雑な正規化ロジック
const cityMap = {
"tokyo": "Tokyo",
"toukyou": "Tokyo",
"東京": "Tokyo",
// ... 数百のマッピング
};
return cityMap[city.toLowerCase()] || city;
}
isValidCity(city) {
// 複雑なバリデーションロジック
const validCities = ["Tokyo", "Osaka", "Kyoto", /* ... */];
return validCities.includes(city);
}
}
// LLMに渡す際の複雑なプロンプト
const prompt = `
天気を取得するには、以下のルールに従ってください:
1. 都市名を正規化する必要があります
2. 以下の都市のみサポートされています: ${validCities.join(", ")}
3. キャッシュを確認してからAPIを呼び出してください
4. エラーハンドリングが必要です
...
`;
LangChainのアプローチ:Descriptionに自然言語で記述
import { DynamicStructuredTool } from "@langchain/core/tools";
import { z } from "zod";
// Descriptionに自然言語でルールを記述
const weatherTool = new DynamicStructuredTool({
name: "get_weather",
description: `
天気情報を取得するツールです。
使用方法:
- 都市名は完全な名前で指定してください(例: "Tokyo", "New York")
- 日本語の都市名も使用可能です(例: "東京", "大阪")
- 都市名が不明確な場合は、最も一般的な表記を使用してください
- エラーが発生した場合は、エラーメッセージをそのまま返してください
注意事項:
- このツールは現在の天気情報のみを返します
- 過去や未来の天気情報は取得できません
- キャッシュ機能が自動的に動作します
`,
schema: z.object({
city: z.string().describe("天気を取得する都市名。完全な名前で指定してください。"),
}),
func: async ({ city }) => {
// シンプルな実装(LLMが適切な入力を提供するため、複雑な正規化が不要)
return await fetchWeather(city);
},
});
なぜDescriptionが重要なのか
1. LLMが自然言語を理解する
// LLMは自然言語の説明を理解し、適切にツールを呼び出す
const tool = new DynamicStructuredTool({
name: "calculate_discount",
description: `
割引を計算するツールです。
ルール:
- 通常価格と割引率(パーセント)を受け取ります
- 割引率は0から100の間の数値である必要があります
- 割引後の価格を計算して返します
- 割引率が100を超える場合は、エラーを返します
- 割引率が負の数の場合も、エラーを返します
計算式: 割引後価格 = 通常価格 × (1 - 割引率 / 100)
例:
- 通常価格: 1000円、割引率: 10% → 割引後価格: 900円
- 通常価格: 5000円、割引率: 20% → 割引後価格: 4000円
`,
schema: z.object({
originalPrice: z.number().describe("通常価格(円)"),
discountRate: z.number().describe("割引率(0-100のパーセント)"),
}),
func: async ({ originalPrice, discountRate }) => {
// LLMが適切な入力を提供するため、バリデーションが簡素化
if (discountRate < 0 || discountRate > 100) {
return `エラー: 割引率は0から100の間である必要があります`;
}
const discountedPrice = originalPrice * (1 - discountRate / 100);
return `割引後価格: ${discountedPrice}円(割引額: ${originalPrice - discountedPrice}円)`;
},
});
2. 柔軟性と拡張性
// Descriptionを更新するだけで、動作を変更できる
const searchTool = new DynamicStructuredTool({
name: "search_products",
description: `
商品を検索するツールです。
検索方法:
- キーワード検索: 商品名、説明、カテゴリから検索
- 価格範囲検索: minPriceとmaxPriceを指定
- カテゴリ検索: 特定のカテゴリのみを検索
- 複合検索: 上記の条件を組み合わせて検索
検索の優先順位:
1. 商品名の完全一致
2. 商品名の部分一致
3. 説明文の一致
4. カテゴリの一致
注意事項:
- 検索結果は最大50件まで返します
- 結果は関連性の高い順にソートされます
- 価格範囲を指定する場合、両方の値を指定してください
`,
schema: z.object({
keyword: z.string().optional().describe("検索キーワード"),
category: z.string().optional().describe("カテゴリ名"),
minPrice: z.number().optional().describe("最小価格"),
maxPrice: z.number().optional().describe("最大価格"),
}),
func: async ({ keyword, category, minPrice, maxPrice }) => {
// Descriptionに記述されたルールに基づいて、LLMが適切なパラメータを提供
return await searchProducts({ keyword, category, minPrice, maxPrice });
},
});
なぜこのアプローチが優れているのか
1. コードの簡素化
従来のアプローチ
// 複雑なルールをコーディングする必要がある
class ProductSearch {
async search(params) {
// バリデーション
if (params.minPrice && params.maxPrice && params.minPrice > params.maxPrice) {
throw new Error("最小価格は最大価格以下である必要があります");
}
// キーワードの正規化
const normalizedKeyword = this.normalizeKeyword(params.keyword);
// 検索ロジック
let results = [];
if (normalizedKeyword) {
results = await this.searchByKeyword(normalizedKeyword);
}
// カテゴリフィルタ
if (params.category) {
results = this.filterByCategory(results, params.category);
}
// 価格フィルタ
if (params.minPrice || params.maxPrice) {
results = this.filterByPrice(results, params.minPrice, params.maxPrice);
}
// ソート
results = this.sortByRelevance(results);
// 結果の制限
return results.slice(0, 50);
}
normalizeKeyword(keyword) {
// 複雑な正規化ロジック
return keyword.trim().toLowerCase();
}
// ... 多くのヘルパーメソッド
}
LangChainのアプローチ
// Descriptionにルールを記述するだけで、LLMが適切に処理
const searchTool = new DynamicStructuredTool({
name: "search_products",
description: `
商品を検索します。
検索条件:
- keyword: 検索キーワード(オプション)
- category: カテゴリ名(オプション)
- minPrice: 最小価格(オプション、maxPriceと一緒に使用)
- maxPrice: 最大価格(オプション、minPriceと一緒に使用)
ルール:
- minPriceを指定する場合、maxPriceも指定してください
- minPriceはmaxPrice以下である必要があります
- キーワードは自動的に正規化されます
- 結果は関連性の高い順にソートされ、最大50件返されます
`,
schema: z.object({
keyword: z.string().optional().describe("検索キーワード"),
category: z.string().optional().describe("カテゴリ名"),
minPrice: z.number().optional().describe("最小価格(maxPriceと一緒に使用)"),
maxPrice: z.number().optional().describe("最大価格(minPriceと一緒に使用)"),
}),
func: async (params) => {
// LLMが適切なパラメータを提供するため、バリデーションが簡素化
return await searchProducts(params);
},
});
2. 保守性の向上
// ルールを変更する場合、Descriptionを更新するだけ
const orderTool = new DynamicStructuredTool({
name: "create_order",
description: `
注文を作成します。
注文ルール(2024年12月更新):
- 最小注文金額: 1,000円
- 最大注文金額: 100,000円
- 配送料: 購入金額が5,000円以上の場合無料、それ以外は500円
- 支払い方法: クレジットカード、銀行振込、コンビニ決済
- 在庫確認: 注文前に在庫を自動確認します
- キャンセル: 注文後24時間以内はキャンセル可能
注意事項:
- 在庫がない場合は、代替商品を提案します
- 配送先が離島の場合は、追加料金が発生する場合があります
`,
schema: z.object({
items: z.array(z.object({
productId: z.string(),
quantity: z.number(),
})).describe("注文する商品のリスト"),
paymentMethod: z.enum(["credit_card", "bank_transfer", "convenience_store"]).describe("支払い方法"),
shippingAddress: z.string().describe("配送先住所"),
}),
func: async (params) => {
// Descriptionに記述されたルールに基づいて、LLMが適切な入力を提供
return await createOrder(params);
},
});
// ルールが変更された場合、Descriptionを更新するだけで対応可能
// コードの変更が最小限で済む
3. エラーハンドリングの簡素化
// Descriptionにエラーハンドリングのルールを記述
const dataProcessingTool = new DynamicStructuredTool({
name: "process_data",
description: `
データを処理します。
処理ルール:
- 入力データはJSON形式である必要があります
- データが無効な場合、エラーメッセージを返します
- データが大きすぎる場合(10MB以上)、エラーを返します
- 処理に時間がかかる場合(30秒以上)、タイムアウトエラーを返します
エラーハンドリング:
- エラーが発生した場合、エラーの種類と原因を明確に返します
- リトライ可能なエラーの場合、自動的にリトライします
- リトライ不可能なエラーの場合、エラーメッセージを返します
`,
schema: z.object({
data: z.string().describe("処理するデータ(JSON形式)"),
operation: z.enum(["filter", "sort", "aggregate", "transform"]).describe("実行する操作"),
}),
func: async ({ data, operation }) => {
// LLMが適切な入力を提供し、エラーハンドリングが簡素化
try {
const parsedData = JSON.parse(data);
return await processData(parsedData, operation);
} catch (error) {
return `エラー: 無効なJSON形式です。${error.message}`;
}
},
});
4. 動的な動作の実現
// Descriptionに動的なルールを記述することで、柔軟な動作を実現
const recommendationTool = new DynamicStructuredTool({
name: "get_recommendations",
description: `
商品のレコメンデーションを取得します。
レコメンデーションアルゴリズム:
- ユーザーの過去の購入履歴を分析
- 類似ユーザーの購入履歴を参照
- 商品の評価とレビューを考慮
- 季節やトレンドを反映
パラメータ:
- userId: ユーザーID(必須)
- category: カテゴリ(オプション、指定するとそのカテゴリのみ)
- limit: 返す件数(デフォルト: 10、最大: 50)
注意事項:
- ユーザーの好みに基づいてパーソナライズされます
- 新規ユーザーの場合、人気商品を推薦します
- 在庫がない商品は除外されます
`,
schema: z.object({
userId: z.string().describe("ユーザーID"),
category: z.string().optional().describe("カテゴリ名(オプション)"),
limit: z.number().optional().describe("返す件数(デフォルト: 10、最大: 50)"),
}),
func: async ({ userId, category, limit = 10 }) => {
// Descriptionに記述されたルールに基づいて、LLMが適切なパラメータを提供
return await getRecommendations({ userId, category, limit });
},
});
実践的な例
例1:複雑なビジネスルールの実装
// 複雑なビジネスルールをDescriptionに記述
const bookingTool = new DynamicStructuredTool({
name: "book_appointment",
description: `
予約を作成します。
予約ルール:
1. 営業時間: 平日 9:00-18:00、土曜 9:00-13:00、日曜・祝日は休業
2. 予約可能時間: 30分単位(例: 9:00, 9:30, 10:00)
3. 予約のキャンセル: 24時間前まで可能
4. 予約の変更: 48時間前まで可能
5. 最大予約数: 1人あたり1日3件まで
6. 予約の重複: 同じ時間帯に複数の予約は不可
特別ルール:
- 新規顧客の場合、初回予約は10%割引
- リピーター(3回以上)の場合、5%割引
- キャンセル料: 24時間以内のキャンセルは50%のキャンセル料
エラーハンドリング:
- 営業時間外の予約: エラーメッセージを返す
- 予約可能時間外: 最も近い予約可能時間を提案
- 予約の重複: 代替時間を提案
`,
schema: z.object({
customerId: z.string().describe("顧客ID"),
serviceId: z.string().describe("サービスID"),
dateTime: z.string().describe("予約希望日時(YYYY-MM-DD HH:mm形式)"),
}),
func: async ({ customerId, serviceId, dateTime }) => {
// Descriptionに記述されたルールに基づいて、LLMが適切な入力を提供
// 複雑なバリデーションロジックが不要
return await bookAppointment({ customerId, serviceId, dateTime });
},
});
例2:データ変換ツール
// データ変換のルールをDescriptionに記述
const dataTransformTool = new DynamicStructuredTool({
name: "transform_data",
description: `
データを変換します。
変換ルール:
- 日付形式: YYYY-MM-DD形式に統一
- 数値形式: カンマ区切りを削除し、数値に変換
- 文字列: 前後の空白を削除、大文字小文字を統一
- null値: 空文字列に変換
- 配列: 重複を削除、ソート
変換タイプ:
- normalize: データの正規化(デフォルト)
- format: フォーマットの統一
- validate: バリデーションとエラー検出
- clean: 不要なデータの削除
注意事項:
- 変換前のデータ形式を自動検出
- 変換できないデータはエラーとして返す
- 変換ログを保持
`,
schema: z.object({
data: z.string().describe("変換するデータ(JSON形式)"),
transformType: z.enum(["normalize", "format", "validate", "clean"]).describe("変換タイプ"),
}),
func: async ({ data, transformType }) => {
// Descriptionに記述されたルールに基づいて、LLMが適切な入力を提供
return await transformData(data, transformType);
},
});
例3:複数のツールを組み合わせた例
// 各ツールのDescriptionに詳細なルールを記述
const tools = [
new DynamicStructuredTool({
name: "search_products",
description: `
商品を検索します。
検索方法:
- キーワード検索: 商品名、説明、カテゴリから検索
- 価格範囲: minPriceとmaxPriceを指定(両方必要)
- カテゴリ: 特定のカテゴリのみ検索
- 在庫: 在庫がある商品のみ検索
検索の優先順位:
1. 商品名の完全一致
2. 商品名の部分一致
3. 説明文の一致
注意事項:
- 検索結果は最大50件
- 結果は関連性順にソート
- 価格範囲を指定する場合、両方の値を指定
`,
schema: z.object({
keyword: z.string().optional().describe("検索キーワード"),
category: z.string().optional().describe("カテゴリ名"),
minPrice: z.number().optional().describe("最小価格"),
maxPrice: z.number().optional().describe("最大価格"),
inStock: z.boolean().optional().describe("在庫がある商品のみ"),
}),
func: async (params) => await searchProducts(params),
}),
new DynamicStructuredTool({
name: "get_product_details",
description: `
商品の詳細情報を取得します。
取得する情報:
- 商品名、説明、価格
- 在庫状況
- 評価とレビュー
- 関連商品
- 配送情報
注意事項:
- 商品IDは必須
- 存在しない商品IDの場合、エラーを返す
- 在庫がない場合、入荷予定日を表示
`,
schema: z.object({
productId: z.string().describe("商品ID"),
}),
func: async ({ productId }) => await getProductDetails(productId),
}),
new DynamicStructuredTool({
name: "add_to_cart",
description: `
カートに商品を追加します。
追加ルール:
- 商品IDと数量を指定
- 在庫がない場合はエラー
- 数量は1以上、在庫数以下
- 既にカートにある場合、数量を更新
注意事項:
- カートの最大商品数: 50件
- カートの最大合計金額: 100,000円
- これらの制限を超える場合、エラーを返す
`,
schema: z.object({
productId: z.string().describe("商品ID"),
quantity: z.number().describe("数量(1以上)"),
}),
func: async ({ productId, quantity }) => await addToCart(productId, quantity),
}),
];
// LLMは各ツールのDescriptionを理解し、適切にツールを選択・使用
const agent = await createOpenAIFunctionsAgent({
llm,
tools,
prompt,
});
なぜこのアプローチが機能するのか
1. LLMの自然言語理解能力
// LLMは自然言語の説明を理解し、適切にツールを呼び出す
const tool = new DynamicStructuredTool({
name: "calculate_tax",
description: `
税金を計算します。
計算ルール:
- 消費税率: 10%(2024年現在)
- 軽減税率: 8%(食品、新聞など)
- 計算式: 税込価格 = 税抜価格 × (1 + 税率 / 100)
- 端数処理: 切り捨て
例:
- 税抜価格: 1000円、税率: 10% → 税込価格: 1100円
- 税抜価格: 500円、税率: 8% → 税込価格: 540円
注意事項:
- 税率は0から100の間である必要があります
- 負の税率は使用できません
`,
schema: z.object({
price: z.number().describe("税抜価格"),
taxRate: z.number().describe("税率(0-100のパーセント)"),
}),
func: async ({ price, taxRate }) => {
// LLMがDescriptionを理解し、適切な入力を提供
// 複雑なバリデーションロジックが不要
if (taxRate < 0 || taxRate > 100) {
return "エラー: 税率は0から100の間である必要があります";
}
const totalPrice = Math.floor(price * (1 + taxRate / 100));
return `税込価格: ${totalPrice}円(税額: ${totalPrice - price}円)`;
},
});
2. プロンプトエンジニアリングの効果
// 効果的なDescriptionの書き方
const effectiveTool = new DynamicStructuredTool({
name: "process_order",
description: `
注文を処理します。
【重要】処理の流れ:
1. 在庫確認 → 在庫がない場合はエラー
2. 価格計算 → 割引適用、送料計算
3. 支払い処理 → 支払い方法に応じた処理
4. 注文確定 → 在庫を減らし、注文を確定
【ルール】
- 最小注文金額: 1,000円
- 最大注文金額: 100,000円
- 送料: 5,000円以上で無料、それ以外は500円
- 支払い方法: クレジットカード、銀行振込、コンビニ決済
【エラーハンドリング】
- 在庫不足: 在庫数を表示し、代替商品を提案
- 金額制限超過: エラーメッセージと制限内容を表示
- 支払いエラー: 支払い方法の変更を提案
【例】
入力: { items: [{id: "P001", qty: 2}], payment: "credit_card" }
出力: 注文確定、合計金額: 2,000円、送料: 500円、総額: 2,500円
`,
schema: z.object({
items: z.array(z.object({
productId: z.string(),
quantity: z.number(),
})),
paymentMethod: z.enum(["credit_card", "bank_transfer", "convenience_store"]),
}),
func: async (params) => await processOrder(params),
});
3. スキーマとDescriptionの連携
// Schemaの説明とDescriptionを連携させる
const tool = new DynamicStructuredTool({
name: "analyze_sentiment",
description: `
テキストの感情分析を行います。
分析内容:
- 感情の種類: ポジティブ、ネガティブ、ニュートラル
- 感情の強度: 0.0から1.0のスコア
- キーワード: 感情に影響を与えるキーワードを抽出
分析ルール:
- 日本語と英語の両方に対応
- スラングや略語も認識
- 文脈を考慮した分析
出力形式:
- sentiment: 感情の種類
- score: 感情の強度(0.0-1.0)
- keywords: 影響を与えるキーワードのリスト
`,
schema: z.object({
text: z.string().describe("分析するテキスト(日本語または英語)"),
language: z.enum(["auto", "ja", "en"]).describe("言語(auto: 自動検出、ja: 日本語、en: 英語)"),
}),
func: async ({ text, language }) => {
// DescriptionとSchemaの説明が連携し、LLMが適切な入力を提供
return await analyzeSentiment(text, language);
},
});
ベストプラクティス
1. Descriptionの書き方
// ✅ 良い例:明確で詳細な説明
const goodTool = new DynamicStructuredTool({
name: "calculate_shipping",
description: `
配送料を計算します。
計算ルール:
- 地域別の基本料金を適用
- 重量に応じた追加料金を計算
- サイズが大きい場合は追加料金
地域区分:
- 本州: 基本料金 500円
- 北海道・沖縄: 基本料金 1,000円
- 離島: 基本料金 1,500円
重量料金:
- 1kg以下: 無料
- 1-5kg: +200円
- 5-10kg: +500円
- 10kg以上: +1,000円
注意事項:
- 重量とサイズの両方を考慮
- 複数の商品がある場合、合計重量で計算
`,
// ...
});
// ❌ 悪い例:曖昧で不十分な説明
const badTool = new DynamicStructuredTool({
name: "calculate_shipping",
description: "配送料を計算します", // 情報が不足
// ...
});
2. エラーメッセージの記述
// Descriptionにエラーハンドリングのルールを記述
const tool = new DynamicStructuredTool({
name: "validate_email",
description: `
メールアドレスを検証します。
検証ルール:
- メールアドレスの形式をチェック
- ドメインの存在を確認
- 無効なメールアドレスの場合、エラーメッセージを返す
エラーメッセージ:
- 形式エラー: "メールアドレスの形式が正しくありません"
- ドメインエラー: "ドメインが存在しません"
- その他: "メールアドレスが無効です"
有効な例:
- user@example.com
- test.user@example.co.jp
無効な例:
- user@ (ドメインがない)
- @example.com (ユーザー名がない)
- user example.com (@がない)
`,
schema: z.object({
email: z.string().describe("検証するメールアドレス"),
}),
func: async ({ email }) => {
// Descriptionに記述されたルールに基づいて、LLMが適切に処理
return await validateEmail(email);
},
});
3. 例の提供
// Descriptionに具体例を含める
const tool = new DynamicStructuredTool({
name: "format_date",
description: `
日付をフォーマットします。
フォーマット形式:
- YYYY-MM-DD: 2024-12-09
- DD/MM/YYYY: 09/12/2024
- YYYY年MM月DD日: 2024年12月09日
- 相対日付: 今日、明日、昨日
使用例:
- 入力: "2024-12-09", 形式: "YYYY-MM-DD" → 出力: "2024-12-09"
- 入力: "2024-12-09", 形式: "DD/MM/YYYY" → 出力: "09/12/2024"
- 入力: "today", 形式: "YYYY-MM-DD" → 出力: 今日の日付
注意事項:
- 無効な日付形式の場合、エラーを返す
- 相対日付は現在の日付を基準に計算
`,
schema: z.object({
date: z.string().describe("フォーマットする日付"),
format: z.enum(["YYYY-MM-DD", "DD/MM/YYYY", "YYYY年MM月DD日"]).describe("フォーマット形式"),
}),
func: async ({ date, format }) => await formatDate(date, format),
});
まとめ
LangChainのToolsとFunction Callingは、Descriptionに自然言語でプロンプトを記述することで、複雑なルールをコーディングすることなく、LLMにツールの使用方法を理解させることができます。
主要な利点
- コードの簡素化: 複雑なバリデーションやルールをコーディングする必要がない
- 保守性の向上: Descriptionを更新するだけで動作を変更できる
- 柔軟性: 自然言語でルールを記述できるため、柔軟な動作が可能
- エラーハンドリングの簡素化: Descriptionにエラーハンドリングのルールを記述できる
- LLMの理解: LLMが自然言語を理解し、適切にツールを呼び出す
なぜこのアプローチが優れているのか
- 自然言語の力: LLMは自然言語を理解するため、複雑なルールを自然言語で記述できる
- プロンプトエンジニアリング: 効果的なDescriptionにより、LLMの動作を制御できる
- スキーマとの連携: Schemaの説明とDescriptionを連携させることで、より正確な入力を得られる
- 拡張性: 新しいルールを追加する場合、Descriptionを更新するだけで対応可能
このアプローチにより、従来の複雑なコーディングを避けながら、柔軟で保守性の高いAIエージェントアプリケーションを構築できます。