LangchainにおけるLLMツールフレームワークの実装

環境構築と準備

公式ドキュメントに基づくツール開発の基本的な流れ:

  • ツール作成:name、description、args_schema、return_directのパラメータを定義
  • 開発環境の初期化

基本ライブラリのインポート

import asyncio
from typing import Annotated, List, Union

from langchain.agents import initialize_agent, AgentType
from langchain_core.messages import HumanMessage

from utils import *
from langchain_core.tools import tool, BaseTool, ToolException, Tool

1. デコレーターによるツール作成

1.1 基本的なデコレーターツール

@tool
def compute_product_1(x: int, y: int) -> int:
    """Two numbers multiplication operation."""
    # デスクリプションが提供されない場合、docstringが必要
    # ないとエラー:ValueError: Function must have a docstring if description not provided.
    return x * y


print(compute_product_1.name)
print(compute_product_1.description)
print(compute_product_1.args)
print(compute_product_1.invoke({"x": 5, "y": 2}))
print("---------------基本デコレーターツールの実行---------------")

# 出力結果:
# compute_product_1
# Two numbers multiplication operation.
# {'x': {'title': 'X', 'type': 'integer'}, 'y': {'title': 'Y', 'type': 'integer'}}
# 10

1.2 注釈付きパラメータの使用

@tool
def multiply_with_largest_value_2(
        base_num: Annotated[int, "ベースとなる数値"],
        num_list: Annotated[List[int], "数値リストから最大値を選択して計算"],
) -> int:
    """Multiply base number by the largest value in the list."""
    return base_num * max(num_list)


print(multiply_with_largest_value_2.args_schema.model_json_schema())
print(multiply_with_largest_value_2.invoke({"base_num": 3, "num_list": [2, 7, 4, 8, 1]}))
print("---------------注釈付きパラメータの実装---------------")

# 出力:
# {'description': 'Multiply base number by the largest value in the list.', 'properties': {'base_num': {'description': 'ベースとなる数値', 'title': 'Base Num', 'type': 'integer'}, 'num_list': {'description': '数値リストから最大値を選択して計算', 'items': {'type': 'integer'}, 'title': 'Num List', 'type': 'array'}}, 'required': ['base_num', 'num_list'], 'title': 'multiply_with_largest_value_2', 'type': 'object'}
# 24

1.3 カスタムJSONスキーマの実装

# v1バージョンでは互換性問題が発生する可能性あり
from pydantic import BaseModel, Field


class ProductCalculatorSchema_3(BaseModel):
    first_val: int = Field(description="最初の数値")
    second_val: int = Field(description="二番目の数値")


@tool("product-calculator-tool", args_schema=ProductCalculatorSchema_3, return_direct=True)
def calculate_product_3(first_val: int, second_val: int) -> int:
    """Calculate product of two numbers."""
    return first_val * second_val


print(calculate_product_3.name)
print(calculate_product_3.description)
print(calculate_product_3.args)
print(calculate_product_3.return_direct)
print("---------------カスタムJSONスキーマの利用---------------")

# 出力:
# product-calculator-tool
# Calculate product of two numbers.
# {'first_val': {'description': '最初の数値', 'title': 'First Val', 'type': 'integer'}, 'second_val': {'description': '二番目の数値', 'title': 'Second Val', 'type': 'integer'}}
# True

2. 構造化ツールの作成

from langchain_core.tools import StructuredTool


class ProductSchema_4(BaseModel):
    first_num: int = Field(description="first number")
    second_num: int = Field(description="second number")


def compute_multiplication_4(first_num: int, second_num: int) -> int:
    """Multiply two numbers together."""
    return first_num * second_num


structured_calculator_4 = StructuredTool.from_function(
    func=compute_multiplication_4,
    name="MultiplicationCalculator",
    description="perform multiplication operations",
    args_schema=ProductSchema_4,
    return_direct=True,
)
print(structured_calculator_4.invoke({"first_num": 4, "second_num": 5}))
print(structured_calculator_4.name)
print(structured_calculator_4.description)
print(structured_calculator_4.args)
print("---------------構造化ツールの実装---------------")

# 出力:
# 20
# MultiplicationCalculator
# perform multiplication operations
# {'first_num': {'description': 'first number', 'title': 'First Num', 'type': 'integer'}, 'second_num': {'description': 'second number', 'title': 'Second Num', 'type': 'integer'}}

3. チェーンからツールを作成

from langchain_core.language_models import GenericFakeChatModel
from langchain_core.output_parsers import StrOutputParser, PydanticOutputParser
from langchain_core.prompts import ChatPromptTemplate
from utils import *

# チェーンをツールとして変換
greeting_prompt_5 = ChatPromptTemplate.from_messages(
    [("human", "こんにちは、私の名前は {user_name}")]
)

llm_5 = get_llm('ollama')
response_chain_5 = greeting_prompt_5 | llm_5 | StrOutputParser()

converted_tool_5 = response_chain_5.as_tool(name="GreetingResponder", description="ユーザー名を受け取り、中国風の挨拶を返す")
print(converted_tool_5.invoke({"user_name": "田中"}))
print(converted_tool_5.args)
print("---------------チェーンベースのツール作成---------------")

# 出力:
# こんにちは!私は田中です、何かお手伝いできますか?😊
# {'user_name': {'title': 'User Name', 'type': 'string'}}

4. カスタムツールの実装(同期・非同期対応)

from typing import Optional, Type

from langchain_core.callbacks import (
    AsyncCallbackManagerForToolRun,
    CallbackManagerForToolRun,
)


class MathOperationSchema_6(BaseModel):
    operand_a: int = Field(description="first number")
    operand_b: int = Field(description="second number")


class CustomMathTool_6(BaseTool):
    name: str = "CustomMathCalculator"
    description: str = "数学演算が必要な場合に使用するツール"
    args_schema: Type[BaseModel] = MathOperationSchema_6
    return_direct: bool = True

    def _run(self, operand_a: int, operand_b: int, run_manager: Optional[CallbackManagerForToolRun] = None) -> str:
        """Execute the mathematical operation."""
        print("同期ツール実行...")
        return operand_a * operand_b

    async def _arun(self, operand_a: int, operand_b: int, run_manager: Optional[AsyncCallbackManagerForToolRun] = None) -> str:
        """Asynchronous execution of the tool."""
        print("非同期ツール実行...")
        return self._run(operand_a, operand_b, run_manager=run_manager.get_sync())


math_operation_6 = CustomMathTool_6()
print(math_operation_6.name)
print(math_operation_6.description)
print(math_operation_6.args)
print(math_operation_6.return_direct)

print(math_operation_6.invoke({"operand_a": 3, "operand_b": 4}))

# 非同期実行
async def execute_async_6():
    result = await math_operation_6.ainvoke({"operand_a": 3, "operand_b": 5})
    return result

asyncio.run(execute_async_6())

# 出力:
# CustomMathCalculator
# 数学演算が必要な場合に使用するツール
# {'operand_a': {'description': 'first number', 'title': 'Operand A', 'type': 'integer'}, 'operand_b': {'description': 'second number', 'title': 'Operand B', 'type': 'integer'}}
# True
# 同期ツール実行...
# 12
# 非同期ツール実行...
# 同期ツール実行...
# 15

5. 非同期ツール操作

from langchain_core.tools import StructuredTool


def calculate_product_7(x: int, y: int) -> int:
    """Calculate product of two numbers."""
    return x * y


product_tool_7 = StructuredTool.from_function(func=calculate_product_7)
print(product_tool_7.invoke({"x": 3, "y": 4}))


# 非同期呼び出し
async def async_execution_7():
    async_result = await product_tool_7.ainvoke({"x": 3, "y": 6})
    print(async_result)

asyncio.run(async_execution_7())

# 出力:
# 12
# 18

6. エラーハンドリングと戻り値型の定義

ツールエラーハンドリング

import random
from typing import List, Tuple

from langchain_core.tools import tool


def fetch_climate_data_8(location: str) -> int:
    """都市の気象情報を取得する"""
    raise ToolException(f"Error: This error is defined via handle_tool_error=True for {location}.")
    
climate_tool_8 = StructuredTool.from_function(
    func=fetch_climate_data_8,
    handle_tool_error=True,
)

# 文字列ベースのエラーメッセージ
climate_tool_8_1 = StructuredTool.from_function(
    func=fetch_climate_data_8,
    handle_tool_error="This is defined through string-based handle_tool_error...",
)

# カスタムエラーハンドリング関数
def custom_error_handler_8(error: ToolException) -> str:
    return f"Custom error message via function: `{error.args[0]}`"
    
climate_tool_8_2 = StructuredTool.from_function(
    func=fetch_climate_data_8,
    handle_tool_error=custom_error_handler_8,
)

# 各種エラーハンドリングの結果
print(climate_tool_8.invoke({"location": "Tokyo"}))
print(climate_tool_8_1.invoke({"location": "Osaka"}))
print(climate_tool_8_2.invoke({"location": "Kyoto"}))

Tuple型の戻り値の実装

import random
from typing import List, Tuple

from langchain_core.tools import tool

# Tuple型の戻り値を持つツール
@tool(response_format="content_and_artifact")
def create_random_numbers_8(min_val: int, max_val: int, count: int) -> Tuple[str, List[int]]:
    """Generate random numbers within specified range."""
    numbers_array = [random.randint(min_val, max_val) for _ in range(count)]
    result_message = f"Successfully created array of {count} random numbers in [{min_val}, {max_val}]."
    return result_message, numbers_array

print(f"Basic invocation: {create_random_numbers_8.invoke({'min_val': 1, 'max_val': 10, 'count': 5})}")

tool_invocation_8 = create_random_numbers_8.invoke(
    {
        "name": "create_random_numbers",
        "args": {"min_val": 1, "max_val": 10, "count": 5},
        "id": "456",
        "type": "tool_call",
    }
)
print(f"Tool call result: {tool_invocation_8}")


# BaseToolによるカスタム実装
class GenerateRandomDecimals_8(BaseTool):
    name: str = "generate_random_decimals"
    description: str = "Generate specified count of random decimals within range."
    response_format: str = "content_and_artifact"
    decimal_places: int = 2

    def _run(self, min_decimal: float, max_decimal: float, count: int) -> Tuple[str, List[float]]:
        range_size = max_decimal - min_decimal
        decimal_array = [
            round(min_decimal + (range_size * random.random()), ndigits=self.decimal_places)
            for _ in range(count)
        ]
        message = f"Created {count} decimals in [{min_decimal}, {max_decimal}], rounded to {self.decimal_places} places."
        return message, decimal_array
        
custom_decimal_generator_8 = GenerateRandomDecimals_8(decimal_places=3)
custom_result_8 = custom_decimal_generator_8.invoke(
    {
        "name": "generate_random_decimals",
        "args": {"min_decimal": 0.5, "max_decimal": 2.5, "count": 4},
        "id": "456",
        "type": "tool_call",
    }
)
print(f"Custom tool result: {custom_result_8}")

7. 組み込みツールの利用

詳細なツール情報:

from utils import *
os.environ['serpapi_api_key'] = 'your_api_key_here'
os.environ["TAVILY_API_KEY"] = 'tvly-your_tavily_key'

# 天気検索機能
weather_search = TavilySearchResults(max_results=2)

# 検索機能
search_wrapper = SerpAPIWrapper()

print(weather_search.invoke("2025年4月5日東京の天気"))
print(search_wrapper.run("Langchainとは何か"))

8. 総合的なツール統合:LLM + Tool + パラメータテンプレート

# ツール名とクラス名の一致が重要
@tool("SpecialOperationA_10")
def operation_a_10(input_x: int, input_y: int) -> int:
    """XとYの特別な計算(A演算)
    Args:
        input_x: First parameter
        input_y: Second parameter
    """
    print(f'A operation executed: [{input_x},{input_y}]')
    return input_x + input_y


@tool("SpecialOperationB_10")
def operation_b_10(input_x: int, input_y: int) -> int:
    """XとYの特別な計算(B演算)
    Args:
        input_x: First parameter
        input_y: Second parameter
    """
    print(f'B operation executed: [{input_x},{input_y}]')
    return input_x * input_y

# ツールリストの作成
tool_collection_10 = [operation_b_10, operation_a_10]

# LLMの初期化
llm_10 = ChatOpenAI(base_url='https://open.bigmodel.cn/api/paas/v4/',
                    api_key='your_api_key',
                    model='glm-4-plus',
                    temperature=0.7,
                    verbose=True)

# ツールバインディング
enhanced_llm_10 = llm_10.bind_tools(tool_collection_10)
request_query = "What is 10 A 5? What is 7 B 8?"

# ツール呼び出しの解析
response_message = enhanced_llm_10.invoke(request_query)
print(f"Detected tool calls: {response_message.tool_calls}")

# Pydanticモデルの定義
class SpecialOperationA_10(BaseModel):
    """Add two integers."""
    input_x: int = Field(..., description="First integer")
    input_y: int = Field(..., description="Second integer")


class SpecialOperationB_10(BaseModel):
    """Multiply two integers."""
    input_x: int = Field(..., description="First integer")
    input_y: int = Field(..., description="Second integer")

# Pydanticツールパーサーの実装
from langchain_core.output_parsers import PydanticToolsParser
processing_chain_10 = (enhanced_llm_10
                      | PydanticToolsParser(tools=[SpecialOperationA_10, SpecialOperationB_10]))

print(f"Pydantic parsing result: {processing_chain_10.invoke(request_query)}")

# 最終メッセージ処理
conversation_messages = [HumanMessage(content=request_query)]
conversation_messages.append(response_message)

for tool_call in response_message.tool_calls:
    selected_operation = {"specialoperationa_10": operation_a_10, "specialoperationb_10": operation_b_10}[
        tool_call["name"].lower()]
    tool_response = selected_operation.invoke(tool_call)
    conversation_messages.append(tool_response)
    print(f"Tool execution result: {tool_response}")

final_response = enhanced_llm_10.invoke(conversation_messages).content
print(f"[{request_query}] Final processed response: {final_response}")

タグ: LangChain LLM tools ai-framework Python

7月22日 16:22 投稿