AI4C

LangChain tool 등록

bde574786 2025. 5. 14. 18:18

Creating tools from functions

@tool decorator

multiply 함수를 LangChain이 쓸 수 있도록 툴로 등록한다. LangChain은 함수를 내부적으로 Tool 객체로 감싸면서 이름, 설명, 입력 파라미터 정보를 자동으로 추출한다. @tool 데코레이터는 기본적으로 함수명을 툴 이름으로 사용하고, 함수에 붙은 docstring을 툴의 description으로 사용한다. 또한 LangChain은 이 함수의 파라미터 시그니처를 읽어서, 각 인자의 이름, 타입, 설명 등을 JSON-like 구조로 자동 분석하여 툴의 입력 스키마를 구성한다.

from langchain_core.tools import tool


@tool
def multiply(a: int, b: int) -> int:
    """Multiply two numbers."""
    return a * b


# Let's inspect some of the attributes associated with the tool.
print(multiply.name)
print(multiply.description)
print(multiply.args)

# multiply
# Multiply two numbers.
# {'a': {'title': 'A', 'type': 'integer'}, 'b': {'title': 'B', 'type': 'integer'}}

 

LangChain은 동기든 비동기든 둘 다 wrapping 가능하다. 단, 비동기 함수로 LangChain 툴을 정의했다면 에이전트를 실행하는 환경도 비동기 코드로 맞춰야 한다.

from langchain_core.tools import tool


@tool
async def amultiply(a: int, b: int) -> int:
    """Multiply two numbers."""
    return a * b

 

Annotated는 인자에 대해 추가적인 메타 정보를 붙이기 위한 Python 문법이다. LangChain은 내부적으로 이 툴의 인자 정보를 Pydantic 모델로 바꿔서 사용하며 model_json_schema()는 Pydantic 모델의 입력 스키마를 JSON Schema 형식으로 출력한다.

from langchain_core.tools import tool
from typing import Annotated, List


@tool
def multiply_by_max(
    a: Annotated[int, "scale factor"],
    b: Annotated[List[int], "list of ints over which to take maximum"],
) -> int:
    """Multiply a by the maximum of b."""
    return a * max(b)


print(multiply_by_max.args_schema.model_json_schema())

# {
#     'description': 'Multiply a by the maximum of b.',
#     'properties': {
#         'a': {
#             'description': 'scale factor',
#             'title': 'A',
#             'type': 'integer'
#         },
#         'b': {
#             'description': 'list of ints over which to take maximum',
#             'items': {
#                 'type': 'integer'
#             },
#             'title': 'B',
#             'type': 'array'
#         }
#     },
#     'required': ['a', 'b'],
#     'title': 'multiply_by_max',
#     'type': 'object'
# }

 

tool 데코레이터에 전달하여 툴 이름과 JSON 인수를 사용자 정의할 수도 있다.

from langchain_core.tools import tool
from pydantic import BaseModel, Field


class CalculatorInput(BaseModel):
    a: int = Field(description="first number")
    b: int = Field(description="second number")


@tool("multiplication-tool", args_schema=CalculatorInput, return_direct=True)
def multiply(a: int, b: int) -> int:
    """Multiply two numbers."""
    return a * b


# Let's inspect some of the attributes associated with the tool.
print(multiply.name)
print(multiply.description)
print(multiply.args)
print(multiply.return_direct)

# multiplication-tool
# Multiply two numbers.
# {'a': {'description': 'first number', 'title': 'A', 'type': 'integer'}, 'b': {'description': 'second number', 'title': 'B', 'type': 'integer'}}
# True

 

LangChain의 @tool 데코레이터는 함수의 docstring을 분석해서 각 파라미터 설명까지 자동으로 툴 스키마에 반영할 수 있다. docstring이 제대로 된 형식으로 작성되지 않으면 @tool(parse_docstring=True)를 사용했을 때 구문 분석 중 오류가 발생하거나, 설명이 잘못 들어갈 수 있다.

from langchain_core.tools import tool


@tool(parse_docstring=True)
def foo(bar: str, baz: int) -> str:
    """The foo.

    Args:
        bar: The bar.
        baz: The baz.
    """
    return bar


print(foo.args_schema.model_json_schema())

# {
#     'description': 'The foo.',
#     'properties': {
#         'bar': {
#             'description': 'The bar.',
#             'title': 'Bar',
#             'type': 'string'
#         },
#         'baz': {
#             'description': 'The baz.',
#             'title': 'Baz',
#             'type': 'integer'
#         }
#     },
#     'required': ['bar', 'baz'],
#     'title': 'foo',
#     'type': 'object'
# }

 

Structured Tool

StructuredTool.from_function은 함수를 툴 객체로 만들 수 있는 클래스 메서드인데, @tool 데코레이터보다 많은 설정을 제공하면서도 코드 양은 많지 않다.

from langchain_core.tools import StructuredTool


def multiply(a: int, b: int) -> int:
    """Multiply two numbers."""
    return a * b


async def amultiply(a: int, b: int) -> int:
    """Multiply two numbers."""
    return a * b


calculator = StructuredTool.from_function(func=multiply, coroutine=amultiply)

print(calculator.invoke({"a": 2, "b": 3}))
print(await calculator.ainvoke({"a": 2, "b": 5}))

# 6
# 10

 

함수 하나와 입력 스키마(BaseModel)만 준비하면 툴을 설명까지 포함하여 완성도 있게 구성할 수 있다.

from langchain_core.tools import StructuredTool
from pydantic import BaseModel, Field

class CalculatorInput(BaseModel):
    a: int = Field(description="first number")
    b: int = Field(description="second number")


def multiply(a: int, b: int) -> int:
    """Multiply two numbers."""
    return a * b


calculator = StructuredTool.from_function(
    func=multiply,
    name="Calculator",
    description="multiply numbers",
    args_schema=CalculatorInput,
    return_direct=True,
    # coroutine= ... <- you can specify an async method if desired as well
)

print(calculator.invoke({"a": 2, "b": 3}))
print(calculator.name)
print(calculator.description)
print(calculator.args)

# 6
# Calculator
# multiply numbers
# {'a': {'description': 'first number', 'title': 'A', 'type': 'integer'}, 'b': {'description': 'second number', 'title': 'B', 'type': 'integer'}}

 

Creating tools from Runnables

문자열 또는 딕셔너리 입력을 받는 LangChain Runnable 객체는 as_tool 메서드를 사용해 툴로 변환할 수 있으며, 이 과정에서 이름, 설명, 인자에 대한 추가적인 스키마 정보도 지정할 수 있다.

from langchain_core.language_models import GenericFakeChatModel
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate

prompt = ChatPromptTemplate.from_messages(
    [("human", "Hello. Please respond in the style of {answer_style}.")]
)

# Placeholder LLM
llm = GenericFakeChatModel(messages=iter(["hello matey"]))

chain = prompt | llm | StrOutputParser()

as_tool = chain.as_tool(
    name="Style responder", description="Description of when to use tool."
)

print(as_tool.args)

# C:\Users\alstn\Desktop\langgraph-mcp\test.py:14: LangChainBetaWarning: This API is in beta and may change in the future.
#   as_tool = chain.as_tool(
# {'answer_style': {'title': 'Answer Style', 'type': 'string'}}

 

Subclass BaseTool

BaseTool 클래스를 상속하여 커스텀 툴을 직접 정의할 수도 있다. 툴의 동작을 세밀하게 제어할 수 있지만 코드 양이 많아진다.

from typing import Optional

from langchain_core.callbacks import (
    AsyncCallbackManagerForToolRun,
    CallbackManagerForToolRun,
)
from langchain_core.tools import BaseTool
from langchain_core.tools.base import ArgsSchema
from pydantic import BaseModel, Field


class CalculatorInput(BaseModel):
    a: int = Field(description="first number")
    b: int = Field(description="second number")


# Note: It's important that every field has type hints. BaseTool is a
# Pydantic class and not having type hints can lead to unexpected behavior.
class CustomCalculatorTool(BaseTool):
    name: str = "Calculator"
    description: str = "useful for when you need to answer questions about math"
    args_schema: Optional[ArgsSchema] = CalculatorInput
    return_direct: bool = True

    def _run(
        self, a: int, b: int, run_manager: Optional[CallbackManagerForToolRun] = None
    ) -> int:
        """Use the tool."""
        return a * b

    async def _arun(
        self,
        a: int,
        b: int,
        run_manager: Optional[AsyncCallbackManagerForToolRun] = None,
    ) -> int:
        """Use the tool asynchronously."""
        # If the calculation is cheap, you can just delegate to the sync implementation
        # as shown below.
        # If the sync calculation is expensive, you should delete the entire _arun method.
        # LangChain will automatically provide a better implementation that will
        # kick off the task in a thread to make sure it doesn't block other async code.
        return self._run(a, b, run_manager=run_manager.get_sync())
    
multiply = CustomCalculatorTool()
print(multiply.name)
print(multiply.description)
print(multiply.args)
print(multiply.return_direct)

print(multiply.invoke({"a": 2, "b": 3}))
print(await multiply.ainvoke({"a": 2, "b": 3}))

# Calculator
# useful for when you need to answer questions about math
# {'a': {'description': 'first number', 'title': 'A', 'type': 'integer'}, 'b': {'description': 'second number', 'title': 'B', 'type': 'integer'}}
# True
# 6
# 6

 

How to create Async Tools

LangChain 툴은 모두 Runnable 인터페이스를 구현한다. 모든 Runnable은 invoke 및 ainvoke 메서드(그리고 batch, abatch, astream 같은 다른 메서드들도)를 제공한다. 따라서, 동기(synchronous) 구현만 제공하더라도 ainvoke 인터페이스를 사용할 수 있지만, 중요한 사항들을 알아야 한다.

from langchain_core.tools import StructuredTool


def multiply(a: int, b: int) -> int:
    """Multiply two numbers."""
    return a * b


calculator = StructuredTool.from_function(func=multiply)

print(calculator.invoke({"a": 2, "b": 3}))
print(
    await calculator.ainvoke({"a": 2, "b": 5})
)  # Uses default LangChain async implementation incurs small overhead

# 6
# 10

 

LangChain은 기본적으로, 해당 함수가 연산 비용이 클 것이라고 가정하고, 이를 다른 스레드로 위임하는 기본 비동기 구현을 제공한다. 만약 비동기 코드베이스에서 작업 중이라면, 이러한 오버헤드를 피하기 위해 동기 툴보다는 비동기 툴을 직접 만드는 것이 좋다. 동기와 비동기 구현을 모두 제공해야 한다면, StructuredTool.from_function을 사용하거나 BaseTool을 상속하여 구현하는 것이 좋다. 동기와 비동기를 모두 구현하면서, 동기 함수가 실행 시간이 매우 짧다면 LangChain의 기본 비동기 처리 방식 대신 직접 동기 함수를 호출하도록 재정의할 수도 있다.

from langchain_core.tools import StructuredTool


def multiply(a: int, b: int) -> int:
    """Multiply two numbers."""
    return a * b


async def amultiply(a: int, b: int) -> int:
    """Multiply two numbers."""
    return a * b


calculator = StructuredTool.from_function(func=multiply, coroutine=amultiply)

print(calculator.invoke({"a": 2, "b": 3}))
print(
    await calculator.ainvoke({"a": 2, "b": 5})
)  # Uses use provided amultiply without additional overhead

# 6
# 10

 

단, 비동기 툴에서는 invoke(동기 실행)를 사용할 수 없다.

from langchain_core.tools import tool


@tool
async def multiply(a: int, b: int) -> int:
    """Multiply two numbers."""
    return a * b


try:
    multiply.invoke({"a": 2, "b": 3})
except NotImplementedError:
    print("Raised not implemented error. You should not be doing this.")

# Raised not implemented error. You should not be doing this.

 

Reference

https://python.langchain.com/docs/how_to/custom_tools/#creating-tools-from-functions