AI

[AI] LangChain - Tool Calling 사용법

usingsystem 2025. 4. 4. 14:49
728x90

[Tool Calling 이란?]

Tool Calling(도구 호출)은 LLM이 단순 텍스트 응답을 넘어서, 등록된 함수나 프로그램을 직접 실행하고 그 결과를 응답에 반영하는 기능이다. 즉, LLM이 사용자의 질문을 분석해 필요한 툴을 스스로 판단하고 실행하는 구조이다.

 

예시 질문필요한 툴

LLM에게 질문 LLM이 선택한 필요한 툴
“서울 오늘 날씨 어때?” 날씨 API
“파스타 요리법 알려줘” 위키백과 검색 도구
“삼성전자 주가 알려줘” 주식 데이터 API
“23 + 45는?” 계산기 함수
“내 회사 문서에서 찾은 요약 보여줘” 벡터 DB 툴 (RAG)

 

  • Tool Calling 흐름
Tool Calling 흐름

[User 입력]
   ↓
[LLM 판단: "이건 툴이 필요하군!"]
   ↓
[툴 이름 + 인자 → Tool 호출]
   ↓
[결과를 받아서 → 다시 자연어 응답 생성]
구성요소 역할
@tool 또는 .as_tool() 툴 등록
args_schema 툴이 요구하는 입력값 스키마 정의
description 어떤 상황에서 이 툴을 쓸지 설명
llm.bind_tools() LLM에게 툴들을 연결해주는 함수
ToolCall, ToolMessage 툴 호출 요청 / 응답 메시지 포맷

 


[Tool 만들고 사용하는 방법]

방법 1. RunnableLambda(...).as_tool(...) 사용

  • RunnableLambda는 일반 Python 함수를 LangChain에서 사용할 수 있도록 Runnable 객체로 감싸주는 기능입니다.
  • RunnableConfig는 LangChain에서 어떤 Runnable 객체 (예: 체인, 툴, 람다 등)를 실행할 때, "이 실행은 어떻게 할 건지" 를 설정해주는 "실행 옵션" 객체입니다.

1. search_wiki: 툴의 로직 정의

def search_wiki(input_data: dict):
  • 입력으로 {"query": "검색어"} 형태의 딕셔너리를 받아
  • 한국어 위키백과에서 관련 문서 2개를 검색해서 반환합니다.
  • WikipediaLoader를 사용한 기본적인 문서 검색 함수입니다.

2. WikiSearchSchema: 툴 인자 스키마

class WikiSearchSchema(BaseModel): query: str = Field(..., description="검색어")
  • pydantic을 사용해서 툴에 전달할 인자를 명확하게 정의합니다.
  • LLM이 툴을 호출할 때 어떤 키를 넣어야 하는지 알려주는 입력 명세 역할

3. RunnableLambda(...): 함수 → 체인 실행 객체로 래핑

runnable = RunnableLambda(search_wiki)
  • LangChain의 체인 시스템에서 실행할 수 있도록 일반 함수를 감쌉니다.
  • .invoke() 등 체인 관련 메서드가 사용 가능해집니다.

4. .as_tool(...): 툴 등록

wiki_tool = runnable.as_tool( name="wiki_search", description="위키백과 문서를 검색합니다.", args_schema=WikiSearchSchema )
  • 툴의 이름, 설명, 입력 구조를 지정해서 LLM이 사용할 수 있게 등록합니다.
  • 이 wiki_tool은 이후에 llm.bind_tools([wiki_tool])로 연결해 주면
    GPT처럼 "필요할 때 직접 검색 툴을 호출"할 수 있게 됩니다.

전체소스

from langchain_core.runnables import RunnableLambda
from pydantic import BaseModel, Field

# WikipediaLoader를 사용하여 위키피디아 문서를 검색하는 함수 
def wiki_search(input_data: dict) :
    """Search Wikipedia documents based on user input (query) and return k documents"""
    query = input_data["query"]
    wiki_loader = WikipediaLoader(query=query, load_max_docs=2, lang="ko")
    wiki_docs = wiki_loader.load()
    return wiki_docs

# 도구 호출에 사용할 입력 스키마 정의 
class WikiSearchSchema(BaseModel):
	"""Input schema for Wikipedia search."""
    query: str = Field(..., description="검색어")

runnable = RunnableLambda(wiki_search)

wiki_tool = runnable.as_tool(
    name="wiki_search",
    description="위키백과 문서를 검색합니다.",
    args_schema=WikiSearchSchema
)

wiki_tool.invoke({"query":"서울에 대해 알려줘"})

 

방법 2. @tool decorator 사용

@tool 데코레이터를 사용해 LLM이 호출할 수 있는 툴을 만든 것

 

전체소스

from langchain_core.tools import tool
from langchain_community.document_loaders import WikipediaLoader
from pydantic import BaseModel, Field
from typing import List
from langchain_core.documents import Document

# 입력 스키마 정의 (같이 재사용 가능!)
class WikiSearchSchema(BaseModel):
    """Input schema for Wikipedia search."""
    query: str = Field(..., description="검색어")

# @tool 데코레이터로 툴 등록
@tool(args_schema=WikiSearchSchema)
def wiki_search(query: str) -> List[Document]:
    """Search Wikipedia documents based on a query."""
    wiki_loader = WikipediaLoader(query=query, load_max_docs=2, lang="ko")
    wiki_docs = wiki_loader.load()
    return wiki_docs

wiki_search.invoke({"query":"서울에 대해 알려줘"})

3. Tool 도구 속성 보는 방법

print("자료형: ")
print(type(wiki_search))
print("-"*100)

print("name: ")
print(wiki_search.name)
print("-"*100)

print("description: ")
pprint(wiki_search.description)
print("-"*100)

print("schema: ")
pprint(wiki_search.args_schema.schema())
print("-"*100)

 

4. LLM에 tool 바인딩 방법 - bind_tools 사용

bind_tools는 LLM에게 "너는 이 도구들을 사용할 수 있어!" 하고 툴 목록을 등록해 주는 메서드입니다.

llm에 bind_toos를 객체를 invoke 하여 실행시키면 LLM이 필요한 툴을 판단하고 langchain_community.tools를 반환하는데 해당 tools안에 tool_calls는 실질적인 반환 tool을 확인할 수 있습니다. tool은 한 개 이상일 수 있습니다.

단계 설명 content 값 too_calls값
1단계 LLM이 툴 필요하다고 판단함 '' (비어있음) ✅ 있음
2단계 툴 실행함 (아직 없음)
3단계 툴 결과와 함께 다시 LLM에 넣음 ✅ 최종 응답 생김 ❌ 없음
from langchain_ollama.chat_models import ChatOllama
from pprint import pprint

llm = ChatOllama(model= "llama3.1:latest")
llm_with_tools = llm.bind_tools(tools=[wiki_tool, wiki_search])

# 도구 호출이 필요한 LLM 호출을 수행
query = "서울에 대해 알려줘. "
ai_msg = llm_with_tools.invoke(query)

# LLM의 전체 출력 결과 출력
pprint(ai_msg)
print("-" * 100)

# 메시지 content 속성 (텍스트 출력)
pprint(ai_msg.content)
print("-" * 100)

# LLM이 호출한 도구 정보 출력
pprint(ai_msg.tool_calls)
print("-" * 100)

5. LLM에 tool_calls 결과 실행하기

LLM이 Tool Calling을 통해 도구를 호출했을 때 이름으로 나옵니다. 어떤 툴을 호출했는지 이름을 기준으로 분기해서 실행 시킬 수 있습니다.

 

tool_calls에 args를 툴에 전달하여 실행시킨다.

tool_call = ai_msg.tool_calls[0]
tool_args = tool_call["args"]

if tool_call["name"] == "wiki_search":
    tool_result = wiki_search.invoke(tool_args)     # args만 전달
else:
    tool_result = wiki_tool.invoke(tool_args)

print(tool_result)

[LCEL체인을 Tool로 래핑 하여 사용하는 방법]

LCEL( LangChain Expression Language )이란 : LangChain에서 체인을 쉽게 연결하는 방법으로 한 줄로 프롬프트, 모델, 도구, 결과처리까지 쭉 이어서 연결할 수 있게 만든 LangChain 문법입니다.

 

툴 구성 함수 (LCEL 체인을 툴로 변환)

def wiki_search_and_summarize(input_data: dict): ...
  • 입력된 query로 위키백과에서 2개 문서 검색
  • 문서를 포맷팅 해서 반환

wiki_search_and_summarize 함수를 기반으로 아래 LCEL 체인을 구성합니다:

summary_chain = (
    {"context": RunnableLambda(wiki_search_and_summarize)} 
    | summary_prompt 
    | llm 
    | StrOutputParser()
)
  • 검색 결과 → 프롬프트에 삽입 → 요약 요청 → 문자열로 결과 파싱

이 체인을 .as_tool(...)을 통해 툴로 바꿉니다:

wiki_summary = summary_chain.as_tool(...)

 

  • 이름, 설명, 입력 스키마가 포함된 툴 객체가 됩니다.
  • 이제 LLM이 이 툴을 자동으로 호출할 수 있게 됨

user_input → LLM → 툴 결정(tool_calls) → 툴 실행 → 결과

from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnableLambda
from langchain_community.document_loaders import WikipediaLoader
from langchain_ollama.chat_models import ChatOllama

llm = ChatOllama(model= "llama3.1:latest")

# 도구 호출에 사용할 입력 스키마 정의 
class WikiSummarySchma(BaseModel):
    """위키백과 검색을 위한 입력 스키마."""
    query: str = Field(..., description="위키백과에서 검색할 쿼리")


def wiki_search_and_summarize(input_data:dict):
    wiki_loader = WikipediaLoader(input_data["query"], load_max_docs=2, lang="ko")
    wiki_docs = wiki_loader.load()

    format_docs =[
         f'<Document source="{doc.metadata["source"]}"/>\n{doc.page_content}\n</Document>'
         for doc in wiki_docs
    ]

    return format_docs

summary_prompt  = ChatPromptTemplate.from_template(
    "다음 텍스트를 요약해줘. :\n\n{context}\n\n요약:"
)
summary_chain = ({"context" : RunnableLambda(wiki_search_and_summarize)}
                 | summary_prompt | llm | StrOutputParser())

# summarized_text = summary_chain.invoke({"query":"서울에 대해 설명해줘"})
# pprint(summarized_text)

# as_tool 메소드를 사용하여 도구 객체로 변환
wiki_summary = summary_chain.as_tool(
    name="wiki_summary",
    description=dedent("""위키백과에서 정보를 검색해야 할 때 이 도구를 사용하세요.
사용자의 쿼리와 관련된 위키백과 문서를 검색하여 반환합니다
요약된 텍스트입니다. 이 도구는 일반적인 지식이 있을 때 유용합니다
또는 배경 정보가 필요합니다."""),
    args_schema=WikiSummarySchma,
)
# wiki_summary.invoke({"query":"서울에 대해 설명해줘"})

# LLM에 도구를 바인딩
llm_with_tools = llm.bind_tools(tools=[wiki_summary])
query = "서울에 대해 설명해줘 "
ai_msg = llm_with_tools.invoke(query)
pprint(ai_msg.tool_calls)
tool_message = wiki_summary.invoke(ai_msg.tool_calls[0])
pprint(tool_message)

[LCEL체인을 @chain 데코레이터 사용]

  • llm_chain.invoke(...) 첫 번째: 어떤 툴 쓸지 판단
  • 툴 실행 결과 (tool_msgs)
  • 그 결과들을 다시 넣어서 두 번째 llm_chain.invoke(...) 호출 → 최종 응답

user_input → LLM(tool 선택) → 툴 실행 → 툴 결과와 함께 다시 LLM 호출 → 최종 응답

from datetime import datetime
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnableConfig, chain

# 프롬프트 템플릿 
prompt = ChatPromptTemplate([
    ("system", f"You are a helpful AI assistant."),
    ("human", "{user_input}"),
    ("placeholder", "{messages}"),
])

llm_width_tools = llm.bind_tools(tools=[wiki_summary])

llm_chain = prompt | llm_width_tools

@chain
def wiki_summary_chain(user_input:str, config: RunnableConfig):
    input = {"user_input" : user_input}
    ai_msg = llm_chain.invoke(input, config=config)
    print("ai_msg: \n", ai_msg)
    print("-"*100)
    tool_msgs = wiki_summary.batch(ai_msg.tool_calls, config=config)
    print("tool_msgs: \n", tool_msgs)
    print("-"*100)

    return llm_chain.invoke({**input, "messages": [ai_msg, *tool_msgs]}, config=config)


response = wiki_summary_chain.invoke("서울에 대해 설명해줘")

pprint(response)

 

728x90