Tools란 무엇인가?
Tools(도구)란 무엇일까? 처음 들으면 생소하게 느껴질 수 있다. Tools를 이용하면 외부 API를 호출하고 응답결과를 받을 수 있다는데, 그럼 외부 API를 호출하게 해주는걸까? 예를 들면, 현재 날씨 정보는 LLM이 모르니 Tools를 이용하면 날씨 API를 LLM이 호출해서 결과를 우리에게 알려주는 것일까? 내부적으로 어떻게 실행되는 것인지 차근차근 알아보자.
프로그램을 만들 때 일반적으로 함수의 매개변수를 넘기고 함수 실행 후 결과를 받는다. 곱셈을 하는 함수를 예로 들면 아래와 같다.
def multiply(x: int, y: int) -> int :
return x * y
print(multiply(2, 3)) # 6
이런 방식은 프로그램 로직에서 x와 y를 정확히 int 타입으로 값을 넘기면 결과를 리턴해준다. 하지만 LLM에서는 기본으로 입력이 프롬프트이기 때문에 문장으로 입력을 해야 한다. 그래서 사용자가 프롬프트(질문) 상에서 x와 y를 정확히 값으로 넘길 수 있는 방법이 없다.
예를 들면 "a=4, b=3, a*b=?"라고 프롬프트에 던지면 ChatGPT가 12이라고 답을 해주지만 복잡한 곱셈의 경우 계산하지 못하는 경우도 생길 수도 있다. 또는 현재 날씨정보를 물어봐도 모른다고 한다.
프롬프트를 사용하는 목적이 특정 기능을 수행하기 위해서 입력하는 경우가 대부분이다.
- 예: 4 곱하기 3은 뭐야? ==> 곱셈 (x*y)
- 오늘 서울 날씨 어때? ==> 날씨조회 (date, location)
그래서 이 기능은 LLM이 아니라 우리가 만든 함수에서 실행을 하고 LLM은 함수를 실행하기 위한 매개변수만 추출하는 역할을 하는 것이다.
기본적인 프롬프트에서 이런 방식을 사용할 수는 없고 이를 가능하게 하는 것이 Tools(도구)이다. 즉, 프롬프트 내에서 사용할 파라미터를 뽑아내서 특정 함수를 호출할 수 있게 해준다. 즉 LLM이 a=4, b=3이라는 값을 추출해서 우리가 만든 곱셈함수(multiply)를 호출하게 된다.
여기서 함수의 실행은 LLM에서 이루어지지 않고, 우리 애플리케이션에서 실행된다.
일반적으로 도구는 사용자가 정의하거나 외부 API를 호출할 때 필요하다. LLM 사용 패턴에서는 프롬프트(prompt)와 응답(completion) 구조를 통해 특정 내용에서 값을 추출하여 특정 함수를 실행하거나 외부 API를 호출할 때 도구를 사용할 수 있다.
Tool은 다음과 같은 요소로 구성되어 있다.
- name - 툴의 이름
- description - 무슨 일을 하는가?
- JSON schema - 입력값을 정의
- function - (선택, 함수의 비동기 변수)
도구가 모델에 바인딩되면, 이름, 설명, 그리고 JSON 스키마가 모델의 컨텍스트로 제공된다. 도구 목록과 지침이 주어지면, 모델은 특정 입력값과 함께 하나 이상의 도구 호출을 요청할 수 있다. 일반적인 사용 방식은 다음과 같다.
from dotenv import load_dotenv
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
load_dotenv()
llm = ChatOpenAI(model="gpt-4o-mini")
@tool
def multiply(a: int, b: int) -> int:
"""Multiply two numbers.""" # docstring은 이 함수가 어떤 역할을 하는지 알려준다. 반드시 필요하다.
return a * b
tools = [multiply] # 다양한 도구를 정의한다.
llm_with_tools = llm.bind_tools(tools) # llm에 도구들을 bind한다.
ai_msg = llm_with_tools.invoke("4 곱하기 3은 얼마인가?") # bind된 도구들을 실행한다.
print(ai_msg.tool_calls)
실행결과
[
{
"name": "multiply",
"args": {
"a": 4,
"b": 3
},
"id": "call_lYc3VnTLw1bF5f3P2KwfM9gx",
"type": "tool_call"
}
]
"4 곱하기 3은 얼마인가?"에서 llm은 곱하기를 하기 위해 multiply
tool을 사용하기로 결정한다. 이 툴(multiply)을 사용하기 위해 파라미터 2개를 추출해야 하는데 a: 4, b: 3이라는 값을 추출한다. 추출한 결과가 args에 dict으로 표시된다. name에는 사용할 함수명, type에는 tool_call을 정의하고 id를 임의로 부여한다.
tool_calls의 응답내용은 아래와 같이 3가지로 구성이 된다.
- name: 툴 이름
- args: 툴의 인수
- id: 툴 호출 아이디
선택된 도구가 호출되면 그 결과를 모델에 다시 전달하여 모델이 수행 중인 작업을 완료할 수 있도록 할 수 있다. 도구를 호출하고 응답을 다시 전달하는 방법에는 일반적으로 두 가지가 있다.
- 단순 인수로 호출하기
- ToolCall로 호출하기
[@tool 실행하기]
여기서 잠깐, LLM을 사용하지 않고 우리가 @tool로 정의해 놓은 함수를 바로 실행하는 방법을 알아보자.
@tool 데코레이션이 있는 함수는 내부적으로 인수를 dict을 통해 주입하면 invoke 메소드가 실행이 된다. 위에서 만든 multiply를 예제로 작성해보자.result = multiply.invoke({"a": 2, "b": 3}) print(result) # 6
또는 invoke에 ToolCall로 호출할수도 있다.
result = multiply.invoke( { "name": "multiply", "args": {"a": 2, "b": 3}, "id": "123", "type": "tool_call", } ) print(result)
실행결과
content='6' name='multiply' tool_call_id='123'
다시 도구를 호출하고 응답을 받는 2가지 방법을 알아보자.
단순 인수로 호출하기
단순 인수로 도구를 호출하면, 도구의 원시 출력값(일반적으로 문자열)을 얻을 수 있다. 일반적으로 다음과 같다.
tool_call = ai_msg.tool_calls[0]
# {'name': 'multiply', 'args': {'a': 4, 'b': 3}, 'id': 'call_ykl4BiibLAB21n2adSnbsnnt', 'type': 'tool_call'}
tool_output = tool.invoke(tool_call["args"]) # tool_call["args"]: {'a': 4, 'b': 3}
tool_message = ToolMessage(
content=tool_output,
tool_call_id=tool_call["id"],
name=tool_call["name"]
)
print(tool_message)
실행결과
content='12' name='multiply' tool_call_id='call_6aIlzKcpFAYkwi7I1o9Ifdb8'
ToolCall로 호출하기
도구를 호출하는 다른 방법은 모델에 의해 생성된 전체 ToolCall
을 사용하여 호출하는 것이다. 이렇게 하면 도구는 ToolMessage
를 반환한다. 이 방법의 이점은 도구 출력을 ToolMessage
로 변환하는 로직을 직접 작성할 필요가 없다는 것이다. 일반적으로 다음과 같다.
tool_call = ai_msg.tool_calls[0] # {'name': 'multiply', 'args': {'a': 4, 'b': 3}, 'id': 'call_ykl4BiibLAB21n2adSnbsnnt', 'type': 'tool_call'}
tool_message = multiply.invoke(tool_call)
print(tool_message)
이 방식으로 도구를 호출하고 ToolMessage
에 대한 아티팩트를 포함하려면, 도구가 두 가지를 반환하도록 해야 한다. 아티팩트를 반환하는 도구를 정의하는 방법에 대한 자세한 내용은 여기를 참고하면 된다.
모델이 사용할 도구를 설계할 때, 다음 사항을 염두에 두는 것이 중요하다.
- 도구 호출 API가 명시적으로 제공되는 챗 모델은 비세밀 조정된 모델보다 도구 호출에 더 능숙하다.
- 도구의 이름, 설명, 그리고 JSON 스키마가 잘 선택되면 모델의 성능이 향상된다. 이것은 또 다른 형태의 프롬프트 엔지니어링이다.
- 간단하고 좁은 범위의 도구가 복잡한 도구보다 모델이 사용하기 쉽다.
Tool에서 artifact 리턴하는 방법
도구는 모델에 의해 호출될 수 있는 유틸리티로, 출력은 모델에 피드백되도록 설계되었다. 그러나 때때로 도구 실행의 결과물 중에서 체인이나 에이전트의 하위 구성 요소에 접근할 수 있도록 하고 싶지만 모델 자체에는 노출하고 싶지 않은 것들이 있다. 예를 들어, 도구가 사용자 정의 객체, 데이터프레임, 또는 이미지를 반환하는 경우, 실제 출력을 모델에 전달하지 않고 이 출력에 대한 메타데이터만 모델에 전달하고 싶을 수 있다. 동시에, 이러한 전체 출력을 다른 곳, 예를 들어 하위 도구에서 접근할 수 있기를 원할 수도 있다.
Tool
및 ToolMessage
인터페이스는 도구 출력의 모델을 위한 부분(이것이 ToolMessage.content
이다)과 모델 외부에서 사용하기 위한 부분(ToolMessage.artifact
)을 구별할 수 있게 해준다.
툴 정의
메시지 내용과 기타 아티팩트를 구별하기 위해 도구를 정의할 때 response_format="content_and_artifact"
를 지정해야 하며, (content, artifact) 형태의 튜플을 반환해야 한다.
이 기능은 langchain-core >= 0.2.19 이상에서 사용 가능하다.
from typing import Tuple
from langchain_core.tools import tool
@tool(response_format="content_and_artifact")
def multiply(a: int, b: int) -> Tuple[str, int]:
"""Multiply two numbers."""
content = f"{a} 곱하기 {b}은 {a * b}이다."
artifact = a * b
return content, artifact
if __name__ == "__main__":
result = multiply.invoke({"a": 2, "b": 3})
print(result)
실행결과
2 곱하기 3은 6이다.
content와 artifact 둘다 얻기 위해서 ToolCall로 실행을 해야 한다.
result = multiply.invoke(
{
"name": "multiply",
"args": {"a": 2, "b": 3},
"id": "123", # required
"type": "tool_call", # required
}
)
print(result)
실행결과
content='2 곱하기 3은 6이다.' name='multiply' tool_call_id='123' artifact=6
모델과 함께 사용
tool-calling 모델과 함께 Tool과 ToolMessage를 손쉽게 만들 수 있다.
from typing import Tuple
from dotenv import load_dotenv
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
load_dotenv()
@tool(response_format="content_and_artifact")
def multiply(a: int, b: int) -> Tuple[str, int]:
"""Multiply two numbers."""
content = f"{a} 곱하기 {b}은 {a * b}이다."
artifact = a * b
return content, artifact
llm = ChatOpenAI(model="gpt-4o-mini")
tools = [multiply]
llm_with_tools = llm.bind_tools(tools)
ai_msg = llm_with_tools.invoke("4 곱하기 3은 얼마인가?")
print(ai_msg.tool_calls)
실행결과
[
{
"name": "multiply",
"args": {
"a": 4,
"b": 3
},
"id": "call_4JWBzRdoEFDYf0jvzyyCe9Uy",
"type": "tool_call"
}
]
result = multiply.invoke(ai_msg.tool_calls[0])
print(result)
실행결과
content='4 곱하기 3은 12이다.' name='multiply' tool_call_id='call_4JWBzRdoEFDYf0jvzyyCe9Uy' artifact=12
여기서 tool call의 인수(args)만 사용하면 content만 응답결과로 내려온다.
result = multiply.invoke(ai_msg.tool_calls[0]["args"])
print("tool_calls args", result)
실행결과
tool_calls args 4 곱하기 3은 12이다.
chain에서 사용하려면 다음과 같이 사용하면 된다.
from operator import attrgetter
chain = llm_with_tools | attrgetter("tool_calls") | multiply.map()
result = chain.invoke("4 곱하기 3은 얼마인가?")
print(result)
실행결과
[ToolMessage(content='4 곱하기 3은 12이다.', name='multiply', tool_call_id='call_hrh6wppFX9zI4IdYhoD4isuB', artifact=12)]
BaseTool 클래스에서 생성
@tool
로 함수를 데코레이팅하는 대신 BaseTool로 직접 만들려면 아래와 같이 하면 된다.
class Multiply(BaseTool):
name: str = "multiply"
description: str = "Multiply two numbers."
response_format: str = "content_and_artifact"
def _run(self, a: int, b: int) -> Tuple[str, int]:
content = f"{a} 곱하기 {b}은 {a * b}이다."
artifact = a * b
return content, artifact
multiply = Multiply()
result = multiply.invoke({"a": 4, "b": 3})
print(result) # 4 곱하기 3은 12이다.
# 또는 Tool call을 주입하면
result = multiply.invoke(
{
"name": "multiply",
"args": {"a": 4, "b": 3},
"id": "123",
"type": "tool_call",
}
)
print(result) # ToolMessage(content='4 곱하기 3은 12이다.' name='multiply' tool_call_id='123' artifact=12)