도구
- 4.1 랭체인 기초
- 4.2 도구 개발 자동화
- 4.3 도구 사용 설정
의사가 청진기, 혈압계, MRI 같은 도구 없이 진단할 수 없듯이, 에이전트도 도구 없으면 그냥 수다쟁이 챗봇이야. 도구는 에이전트가 텍스트 생성 너머로 실제 행동을 할 수 있게 해주는 모듈형 컴포넌트거든. 데이터베이스 조회, API 호출, 파일 조작, 외부 시스템 연동 — 이런 걸 가능하게 하는 게 전부 도구야. 그리고 도구는 독립적으로 개발, 테스트, 최적화할 수 있어서 복잡한 시스템을 레고 블록처럼 조립할 수 있지.
도구는 다섯 가지로 분류돼. 먼저 로컬 도구(Local Tools) — 가장 단순한 형태로, 에이전트가 돌아가는 같은 환경에서 직접 실행되는 도구야. LangChain에서 로컬 도구 만드는 방법이 세 가지 있어.
@tool 데코레이터 — 가장 간단해. 함수 위에 @tool 붙이면 끝이야. 함수의 docstring이 도구 설명이 되고, 타입 힌트가 입력 스키마가 돼.
from langchain_core.tools import tool
@tool
def calculate_bmi(weight_kg: float, height_m: float) -> str:
"""체중(kg)과 키(m)를 받아 BMI를 계산합니다."""
bmi = weight_kg / (height_m ** 2)
if bmi < 18.5:
return f"BMI: {bmi:.1f} — 저체중"
elif bmi < 25:
return f"BMI: {bmi:.1f} — 정상"
else:
return f"BMI: {bmi:.1f} — 과체중"
StructuredTool — 입력 파라미터가 여러 개이고 유효성 검증이 필요할 때 써. Pydantic의 BaseModel로 입력 스키마를 명시적으로 정의할 수 있어서 복잡한 입력 구조를 다루기 좋지.
BaseTool 서브클래스 — 가장 유연한 방식이야. 클래스를 상속해서 _run 메서드를 구현하면 되는데, 상태 관리, 비동기 처리, 고급 에러 핸들링이 필요할 때 쓰거든.
from langchain.tools import BaseTool
class DatabaseQueryTool(BaseTool):
name = "database_query"
description = "SQL 쿼리를 실행하고 결과를 반환합니다"
def _run(self, query: str) -> str:
# DB 연결, 쿼리 실행, 결과 반환
results = self.db.execute(query)
return str(results)
어떤 방식을 쓸지는 복잡도에 따라 결정하면 돼. 프로토타입은 @tool로 빠르게, 프로덕션은 BaseTool로 견고하게.
API 기반 도구는 외부 서비스의 API를 호출하는 도구야. 로컬 도구와 근본적으로 다른 점이 있는데 — 네트워크 의존성이야. API가 느리면 에이전트도 느려지고, API가 죽으면 에이전트도 멈추거든. 그래서 반드시 타임아웃, 재시도, 폴백(fallback) 전략을 넣어야 해. 인증/인가 관리, 속도 제한 대응, 응답 파싱, 에러 핸들링 — 이런 것들이 API 도구 설계의 핵심 포인트야.
플러그인 도구는 서드파티가 만들어놓은 도구를 갖다 쓰는 방식이야. LangChain 생태계에 이미 수백 개가 있거든 — DuckDuckGo 검색, Wikipedia 조회, YouTube 자막 추출, Slack 메시지 전송 등. 장점은 빠른 프로토타이핑이지. 직접 구현하면 반나절 걸릴 걸 import 한 줄로 끝낼 수 있어. 단점은 블랙박스라는 거야. 내부 구현을 모르니까 디버깅이 어렵고, 버전 업데이트로 동작이 바뀔 수 있고, 보안 검증도 직접 해야 해.
**MCP(Model Context Protocol)**가 이 챕터에서 가장 무게감 있는 부분이야. Anthropic이 2024년 11월에 공개한 오픈 표준으로, AI 모델이 외부 도구/데이터와 연결되는 방식을 표준화한 프로토콜이거든. 쉽게 말하면 — 지금까지 도구 연동은 프레임워크마다, 모델마다 다 달랐는데 MCP는 USB-C 같은 범용 포트를 만들겠다는 거야.
MCP의 아키텍처는 세 계층으로 구성돼. 호스트(Host) — 사용자가 실제로 쓰는 앱, 클라이언트(Client) — 호스트 안에서 MCP 서버 하나와 1:1로 연결을 관리하는 놈, 서버(Server) — 실제 도구, 리소스, 프롬프트를 노출하는 외부 프로그램.
동작 흐름은 이래. 초기화 — 호스트가 클라이언트를 생성하고, 클라이언트가 서버랑 핸드셰이크해서 프로토콜 버전과 지원 기능을 교환해. 발견(Discovery) — 클라이언트가 서버한테 "뭐 할 수 있어?" 하고 물어보면 서버가 도구 목록과 설명을 반환하지. 실행 — LLM이 도구가 필요하다고 판단하면, 호스트가 클라이언트를 통해 서버에 요청 보내고 결과 받아와.
MCP가 제공하는 세 가지 기본 요소는 도구(Tools) — 모델이 호출할 수 있는 함수, 리소스(Resources) — 모델이 읽을 수 있는 데이터(REST의 GET 같은 것, 부작용 없음), 프롬프트(Prompts) — 도구나 리소스를 최적으로 쓰기 위한 미리 정의된 템플릿이야. 전송 방식은 Stdio(로컬 프로세스, 간단하고 빠르지만 같은 머신에서만 가능)와 HTTP(원격 서버와 통신, 확장성은 좋지만 인증과 보안 신경 써야 함) 두 가지가 있어. 2025년 3월에 OpenAI도 MCP를 공식 채택했고, 사실상 업계 표준이 됐지.
**상태 유지 도구(Stateful Tools)**도 있어. 대부분의 도구는 상태가 없는데(stateless), 어떤 도구는 상태를 유지해야 의미가 있거든. 데이터베이스 연결을 유지하면서 여러 쿼리를 순차적으로 날리거나, 세션 기반 API 호출 흐름(로그인 → 조회 → 수정 → 로그아웃) 같은 것들이지. LangGraph가 여기서 빛을 발해 — 그래프 기반으로 상태(State)를 노드 간에 공유할 수 있게 설계됐거든. 상태 격리(여러 사용자가 동시에 쓸 때 상태가 섞이면 안 돼), 상태 정리(세션 끝나면 리소스 반환), 복구 전략(중간에 터졌을 때) — 이런 것들이 설계할 때 주의점이야.
도구를 사람이 일일이 만드는 게 아니라, 파운데이션 모델한테 도구를 만들게 시키는 것도 가능해. 자연어로 "날씨 조회하는 도구 만들어줘"라고 하면 LLM이 함수 시그니처, 파라미터 스키마, 설명까지 생성하고, 구현 코드까지 작성하고, 단위 테스트와 엣지 케이스 테스트도 같이 생성하지. 이게 되는 이유는 도구라는 게 결국 잘 정의된 입출력을 가진 함수이기 때문이야. 다만 LLM이 생성한 도구를 검증 없이 프로덕션에 넣으면 안 돼 — 특히 보안이랑 에지 케이스 처리는 사람이 반드시 리뷰해야 하거든.
더 과격한 아이디어도 있어 — 에이전트가 작업 중에 필요한 도구가 없으면 그 자리에서 도구를 만들어서 쓰는 거야. 기존 패러다임이 개발자가 미리 도구 세트를 정의하고 에이전트가 그 안에서 골라 쓰는 거였다면, 새로운 패러다임은 에이전트가 필요에 따라 즉석에서 도구를 생성하고 실행하고 결과를 활용하는 거지. 목표 분해, 다단계 실행, 피드백 기반 적응 — 이런 과정을 거치면서 복잡한 작업을 수행해. CrewAI, AutoGen 같은 프레임워크에서 이미 구현되고 있어. 다만 보안 리스크가 커 — 에이전트가 임의 코드를 생성하고 실행한다는 건 잘못되면 시스템 전체를 날릴 수 있다는 거거든. 샌드박싱, 실행 권한 제한, 코드 리뷰 게이트가 필수야.
도구를 만드는 것과 에이전트가 그 도구를 제대로 쓰게 만드는 것은 전혀 다른 문제야. LangChain에서는 bind_tools() 메서드로 LLM에 도구를 연결해.
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4o")
llm_with_tools = llm.bind_tools([search_tool, calculator_tool, weather_tool])
이 과정에서 실제로 일어나는 건 — 각 도구의 이름, 설명, 파라미터 스키마가 모델의 시스템 프롬프트 또는 function calling 스키마로 주입되는 거야. 모델은 이 정보를 보고 어떤 도구를 써야 할지 판단하지.
여기서 반복적으로 강조되는 건 — 도구 설명(description)의 품질이 도구 사용 정확도를 결정한다는 거야.
나쁜 예:
@tool
def search(q: str) -> str:
"""검색"""
...
좋은 예:
@tool
def search_knowledge_base(query: str, max_results: int = 5) -> str:
"""내부 기술 문서 데이터베이스를 검색합니다.
사용자가 기술적 질문을 하거나 문서를 찾을 때 사용하세요.
일반적인 웹 검색에는 사용하지 마세요."""
...
좋은 도구 설명의 조건은 — 언제 쓰는지 명확하게, 언제 쓰면 안 되는지도 명확하게, 파라미터 이름이 직관적이고 설명적이고, 반환값이 뭔지도 설명하는 거야.
도구 사용 방식은 두 가지로 나뉘어. Function Calling(네이티브) — 주요 모델 제공자가 API 레벨에서 지원하고, 모델이 구조화된 JSON으로 도구 호출을 반환해서 안정적이야. 프롬프트 기반 — function calling 지원 안 하는 모델에서 프롬프트로 형식을 지정하는 방식인데, 불안정하고 파싱이 깨지기 쉽지.
에이전트가 도구를 쓰는 전체 흐름은 이래: 사용자 입력 → LLM이 도구 필요 여부 판단 → 어떤 도구를 어떤 파라미터로 호출할지 결정 → 에이전트 런타임이 실제로 도구 실행 → 도구 결과가 LLM에게 다시 전달 → 최종 답변 생성(또는 추가 도구 호출). 핵심은 실제 실행은 LLM이 아니라 런타임이 한다는 거야. LLM은 요청만 하고, 실제 실행은 에이전트 프레임워크가 담당하니까 실행 전에 검증 레이어를 넣을 수 있지.
도구가 많아지면 선택 전략도 필요해. 도구가 3-5개면 전부 시스템 프롬프트에 넣어도 되지만, 10개 이상이면 도구 설명만으로 컨텍스트 윈도를 많이 차지하게 돼. 카테고리별 그룹핑, 동적 도구 로딩 같은 전략이 필요하고, MCP의 발견(Discovery) 메커니즘이 여기서도 유용해 — 서버가 도구 목록을 동적으로 제공하니까 에이전트가 상황에 맞는 도구만 로드할 수 있거든.
정리
4장에서 기억할 거 세 가지:
- 도구는 에이전트의 손발이야. 모델이 아무리 똑똑해도 도구 없으면 행동할 수 없어. 도구 설계가 곧 에이전트 능력의 상한선을 결정하거든
- MCP가 게임 체인저야. 프레임워크마다 제각각이던 도구 연동을 표준화했지. USB-C처럼 한번 만들면 어디서든 쓸 수 있는 세상이 오고 있어
- 도구 설명은 대충 쓰지 마. 에이전트가 도구를 제대로 고르고, 제대로 된 파라미터로 호출하려면 설명이 명확해야 해. 좋은 도구 설명 = 좋은 에이전트 성능이거든