DevLog/LangChain

LangChain #4 – LangChain 도구(tool)란 무엇인가?

archive-log 2025. 4. 15. 20:17

LangChain은 LLM을 중심으로 다양한 기능을 조합할 수 있는 프레임워크로
앞선 글들에서는 주로 프롬프트, 문서, 체인 중심으로 구성했지만 LangChain에서 진짜 유용한 기능 중 하나는 바로 도구(tool)이다.
 
이번 글에서는 도구(tool)에 대한 개념과 사용하는 방법을 정리해보려 한다.


LangChain 구성요소

구성요소 설명
LLM 텍스트 응답 생성
Prompt 입력 포맷 정의
Chain 여러 구성요소 연결 흐름
Retriever 유사한 문서 검색
Tool LLM이 사용할 수 있는 외부 기능
Agent 여러 도구를 상황에 따라 선택적으로 실행

 

 
도구(tool)란?

도구(tool)는 함수, API, 시스템 명령어 등 '행동 가능한 것들'을 래핑한 구성이다.
LangChain에서는 @tool 데코레이터를 통해 함수를 도구로 만들 수 있다.
정의된 도구는 LLM이 사용할 수 있으며, 프롬프트에서 도구 호출이 필요한 상황이 오면 Agent를 통해 자동으로 실행된다.

 

✅  도구를 정의할 때 주의할 점
이름과 설명을 통해 LLM 은 도구가 어떤 역할을 하는지 파악하기 때문에 이름과 설명을 잘 정의하는게 중요하다.
 
 

도구는 언제 사용하나?

- LLM이 지식 외부의 기능을 호출해야 할 때
- LLM이 어떤 선택지를 가지고 의사결정해야 할 때
- 외부 시스템과 연동(날씨, DB, API 등)이 필요할 때
 
 

도구 없이도 가능하지 않나?

도구 없이도 많은 작업이 가능하지만, LLM이 자체적으로 알 수 없는 정보에 접근하거나,
정확한 계산, 구조적 API 응답, 외부 시스템 연동이 필요한 경우 Tool은 사실상 필수다.
 
 

도구 어떻게 사용하나?

LangChain에서는 상황에 따라 여러 방식으로 도구를 실행할 수 있으며, 아래 정리된 표를 참고하면 가장 적절한 방법을 선택할 수 있다.

더 복잡한 흐름이 필요할 경우에는 Runnable 체인 구성이나 LangGraph 기반의 상태 관리형 Agent로 확장할 수 있다.


방식 구조 자동 실행 특징 상황
직접 실행 invoke Python 함수처럼 호출 X 가장 단순, Agent 사용 안 함 테스트
llm.bind_tools LLM + 도구 연결
→ 도구 호출은 LLM 판단, 실행은 수동
X 경량 구성, LLM이 도구 호출 지시 경량 챗봇,
흐름 확인
create_openai_functions_agent + AgentExecutor LLM + 도구 + Prompt + Scratchpad
→ Agent 구성
O 도구 판단 + 실행 + 응답 자동화 서비스용 챗봇, 완성형 구조

LangChain 도구 실행 방식 비교

LangChain에서 도구를 사용하는 대표적인 세 가지 방식에 따라 동일한 질문을 각각 다르게 처리해본다.

 

 

1. 준비

도구 실행 결과를 자세히 알아보기 위해 사용했던 환불 정책 문서(/data/policy.txt)를 준비한다.

# data/policy.txt
환불은 결제일로부터 7일 이내에 요청이 가능합니다.
환불은 상품이 사용되지 않았을 경우에만 처리됩니다.
고객센터를 통해 접수해야 하며, 처리까지는 영업일 기준 3일이 소요됩니다.

 

문서를 문장 단위(마침표 + 줄바꿈을 기준)로 분할하여 벡터 저장소를 새로 구축한다.

import re
from langchain_community.document_loaders import TextLoader
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import FAISS
from langchain.schema import Document

# 문서 로딩
loader = TextLoader("data/policy.txt", encoding="utf-8")
documents = loader.load()

# 문서 분할
def split_policy(document):
    """
    정책 항목을 분리하는 함수
    """

    # 정규표현식 정의
    pattern = r'(?<=\.)[\s\r\n]*'
    policy_items = re.split(pattern, document.page_content)

    # 각 항목을 Document 객체로 변환
    policy_documents = []
    for i, policy_item in enumerate(policy_items, 1):
        if policy_item.strip():
            policy_document = Document(
                page_content=policy_item.strip(),
                metadata={
                    "source": document.metadata['source'],
                    "policy_number": i,
                    "policy": policy_item
                }
            )
            policy_documents.append(policy_document)
        
    return policy_documents
    

# 항목 분리 실행
policy_documents = []
for doc in documents:
    policy_documents += split_policy(doc)

# 임베딩 생성 + 벡터 저장소 구축
embeddings = OpenAIEmbeddings()
vectorstore = FAISS.from_documents(policy_documents, embeddings)
vectorstore.save_local("policy_split_index")

 

구축된 벡터 저장소의 데이터를 출력하면 다음과 같다.

from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import FAISS

# 로컬 인덱스 불러오기
embeddings = OpenAIEmbeddings()
vectorstore = FAISS.load_local("policy_split_index", embeddings, allow_dangerous_deserialization=True)

# 출력
for i, (doc_id, document) in enumerate(vectorstore.docstore._dict.items(), start=1):
    print(f"[{i}] 문서 ID: {doc_id}")
    print(f"source: {document.metadata['source']}")
    print(f"번호: {document.metadata['policy_number']}")
    print(f"내용: {document.metadata['policy']}")
    print("-" * 40)

 

 

2. 도구 정의

from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import FAISS
from langchain.schema import Document
from langchain_core.tools import tool

# 로컬 인덱스 불러오기
embeddings = OpenAIEmbeddings()
vectorstore = FAISS.load_local("policy_index", embeddings, allow_dangerous_deserialization=True)

# 도구 정의하기
@tool
def search_policy(query: str) -> Document:
    """
    Securely search and access refund policy information from an encrypted database. 
    To keep your data confidential, use this tool only for refund-related inquiries.
    """
    doc = vectorstore.similarity_search(query, k=1)
    if len(doc) > 0:
        return doc
    
    return Document(page_content="관련 정보를 찾을 수 없습니다.")

 

 

3-1. 직접 호출 (invoke 또는 함수 실행)

query = "환불 신청은 언제까지 가능한가요?"
response = search_policy(query)
print(response)
직접 호출 결과

 

3-2. LLM에 도구 바인딩 (llm.bind_tools())

# ChatOpenAI 모델 초기화
llm = ChatOpenAI(model="gpt-4o-mini")

# 도구를 직접 LLM에 바인딩
llm_with_tools = llm.bind_tools(tools=[search_policy])

# 실행
query = "환불 신청은 언제까지 가능한가요?"
response = llm_with_tools.invoke(query)
print(response)

도구 바인딩 결과

 

llm.bind_tools() 로 도구를 LLM에 바인딩한 경우, LLM은 질문을 받고 즉시 응답을 생성하지 않는다.
대신, 먼저 해당 질문을 처리하기 위해 어떤 도구를 실행해야 하는지를 판단하고, tool_calls 라는 구조로 도구 호출 명령만 반환한다.

→ 실제로 결과값 중 tool_calls 를 확인해보면 '환불 신청 기간' 이라는 질문으로 search_policy 도구 호출한것을 확인할 수 있다.

이 단계에서 반환되는 AIMessage 객체는 content가 비어 있으며, 실제 응답은 도구 실행 결과를 받은 후에 생성된다.

따라서 최종 응답을 생성하기 위해서는 도구를 실행한 결과를 전달해주는 단계가 추가적으로 필요하다.

 

# ChatOpenAI 모델 초기화
llm = ChatOpenAI(model="gpt-4o-mini")

# 도구를 직접 LLM에 바인딩
llm_with_tools = llm.bind_tools(tools=[search_policy])

# LLM이 도구 호출 요청
query = "한번 사용한 상품도 환불이 가능한가요?"
response = llm_with_tools.invoke(query)
tool_call = response.tool_calls[0]
tool_result = search_policy.invoke(tool_call["args"])
print(response)
print("-"*100)

# 도구 결과 전달 → 최종 응답 생성
final = llm_with_tools.invoke([
    HumanMessage(content=query),
    AIMessage(content="", tool_calls=response.tool_calls),
    ToolMessage(tool_call_id=tool_call["id"], content={"output":tool_result})
])
print(final)

도구 바인딩 + 도구 결과 전달 결과

 

도구 바인딩 후 실행한것과 달리 도구 결과를 전달해준 최종 응답에서는 질문에 대한 답변이 잘 생성된것을 확인할 수 있다.

 

 

3-3. Agent 사용

# ChatOpenAI 모델 초기화
llm = ChatOpenAI(model="gpt-4o-mini")

# Agent 프롬프트 정의
# 시스템 역할 정의 + 사용자 입력 + 에이전트 내부 scratchpad 삽입
agent_prompt = ChatPromptTemplate.from_messages([
    # 시스템 역할: AI의 태도 및 목적 설명
    ("system", dedent("""
        당신은 고객의 질문을 해결하기 위해 정책 문서를 검색하고, 필요한 경우 도구를 사용하는 AI입니다.
        명확하고 정확한 답변을 생성하세요.
        """)),
    # 선택적 채팅 히스토리 (이전 대화 맥락 유지용, 없어도 실행 가능)
    MessagesPlaceholder(variable_name="chat_history", optional=True),
    # 사용자 입력
    ("human", "{input}"),
    # 도구 호출 이력 (필수: agent가 도구를 판단하고 이어서 실행하기 위해 필요)
    MessagesPlaceholder(variable_name="agent_scratchpad"),
])

# 사용할 도구 정의
tools = [search_policy]

# OpenAI Function 기반 Agent 생성
agent = create_openai_functions_agent(llm=llm, tools=tools, prompt=agent_prompt)

# 실행기(Executor)로 감싸 실제 실행 가능하게 구성
executor = AgentExecutor(agent=agent, tools=tools, verbose=True)

# 실행
query = "한번 사용한 상품도 환불이 가능한가요?"
response = executor.invoke({"input":query})
print(response["output"])

Agent 사용 결과

 

프롬프트에는 {input}과 {agent_scratchpad}를 반드시 포함해야 하는것을 유의해야한다.


이번 글에서는 LangChain에서의 도구(tool) 개념을 살펴보고, 이를 직접 정의하고 사용하는 3가지 방법까지 정리해보았다.

 

  • @tool로 정의한 함수를 직접 호출하는 방식은 가장 단순하며, 도구의 동작을 빠르게 확인할 때 유용하다.
  • llm.bind_tools() 방식은 LLM이 도구를 언제 쓸지 판단하고, 우리는 그 지시에 따라 결과를 직접 실행해 응답을 구성한다.
  • create_openai_functions_agent() 방식은 LLM이 도구 호출과 응답 생성을 모두 자동으로 처리하는 구조로, 실서비스에 적용할 수 있을 만큼 완성도 높은 흐름을 제공한다.

다음 글에서는 이러한 구조를 바탕으로 백엔드 API로 감싸고, 간단한 챗 UI를 붙여 실제 서비스에 가까운 형태로 구성할 예정이다.