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"])
프롬프트에는 {input}과 {agent_scratchpad}를 반드시 포함해야 하는것을 유의해야한다.
이번 글에서는 LangChain에서의 도구(tool) 개념을 살펴보고, 이를 직접 정의하고 사용하는 3가지 방법까지 정리해보았다.
- @tool로 정의한 함수를 직접 호출하는 방식은 가장 단순하며, 도구의 동작을 빠르게 확인할 때 유용하다.
- llm.bind_tools() 방식은 LLM이 도구를 언제 쓸지 판단하고, 우리는 그 지시에 따라 결과를 직접 실행해 응답을 구성한다.
- create_openai_functions_agent() 방식은 LLM이 도구 호출과 응답 생성을 모두 자동으로 처리하는 구조로, 실서비스에 적용할 수 있을 만큼 완성도 높은 흐름을 제공한다.
다음 글에서는 이러한 구조를 바탕으로 백엔드 API로 감싸고, 간단한 챗 UI를 붙여 실제 서비스에 가까운 형태로 구성할 예정이다.
'DevLog > LangChain' 카테고리의 다른 글
LangChain #5 – 챗봇 구현 (0) | 2025.04.17 |
---|---|
LangChain #3 – 문서 기반 챗봇 구조 이해와 RAG 실습 (0) | 2025.04.15 |
LangChain #2 - 개발 환경 세팅 + LLM + 문서 기반 답변 (1) | 2025.04.12 |
LangChain #1 - 어디에 쓰이는 물건인가 (0) | 2025.04.11 |