Skip to main content
Summary

도구를 연결한다는 뜻

지금까지의 체인은 주로 "입력을 받아 답변을 생성하는 흐름"이었다. RAG에서는 외부 문서를 검색했지만, 그 검색 단계도 우리가 체인 안에 고정해 둔 일이었다. 도구 호출은 한 단계 더 나아간다. 모델이 사용자의 요청을 보고 어떤 기능을 써야 하는지 판단하고, 필요한 인자를 만들어 도구 실행을 요청한다.

여기서 중요한 점은 모델이 직접 파일을 읽거나 API를 호출하는 것이 아니라는 사실이다. 모델은 "이 도구를 이런 인자로 호출하라"는 요청을 만들고, 실제 실행은 애플리케이션 코드가 한다. 그래서 도구 호출은 다음 세 부분으로 나뉜다.

  1. 모델에게 사용할 수 있는 도구 목록을 알려 준다.

  2. 모델이 적절한 도구와 인자를 선택한다.

  3. 애플리케이션이 도구를 실행하고 결과를 다시 모델에 전달한다.

이 반복 구조를 자동으로 관리하는 것이 에이전트다. 에이전트는 질문을 보고, 도구를 쓰고, 결과를 읽고, 필요하면 다른 도구를 다시 호출한 뒤 최종 답변을 만든다. LangChain은 모델과 도구 추상화를 제공하고, 더 오래 실행되는 상태 유지형 에이전트 흐름은 LangGraph의 실행 모델과 자연스럽게 연결된다2.

도구 호출과 에이전트 구성하기AI 생성 이미지

도구 선택은 이름, 설명, 스키마가 좌우한다

모델은 Python 함수의 내부 코드를 읽지 않는다. 모델이 보는 것은 보통 도구의 이름, 설명, 입력 스키마다. LangChain의 @tool 데코레이터를 사용하면 함수 이름, docstring, 타입 힌트를 바탕으로 도구 정의를 만들 수 있다1.

나쁜 도구 정의는 다음과 같다.

from langchain_core.tools import tool

@tool
def process(x: str) -> str:
    '''입력을 처리합니다.'''
    return x

사람도 이 도구를 언제 써야 할지 알기 어렵다. 모델도 마찬가지다. 반대로 좋은 도구는 용도가 좁고 분명하다.

from datetime import date, timedelta
from langchain_core.tools import tool

@tool
def calculate_due_date(days_after: int) -> str:
    '''오늘로부터 days_after일 뒤 날짜를 YYYY-MM-DD 형식으로 계산합니다.
    마감일, 납기일, 후속 연락일 계산이 필요할 때 사용합니다.
    days_after는 0 이상 365 이하의 정수여야 합니다.
    '''
    if days_after < 0 or days_after > 365:
        return '오류: days_after는 0 이상 365 이하로 입력해야 합니다.'

    return (date.today() + timedelta(days=days_after)).isoformat()

차이는 작아 보이지만 실제 결과에는 큰 영향을 준다. 도구 이름이 calculate_due_date이면 날짜 계산 도구라는 신호가 된다. 설명에는 "마감일, 납기일, 후속 연락일"처럼 사용 상황이 들어 있다. 입력값의 범위도 적혀 있어 모델이 잘못된 인자를 만들 가능성을 줄인다.

입력이 여러 개이거나 검증 규칙이 필요하면 Pydantic 스키마를 쓰는 편이 좋다.

from pydantic import BaseModel, Field

class FaqSearchInput(BaseModel):
    query: str = Field(..., min_length=2, description='검색할 키워드나 질문')
    max_results: int = Field(3, ge=1, le=5, description='가져올 최대 결과 수')

FAQ = [
    {'title': '휴가 신청', 'text': '휴가는 인사 시스템에서 최소 하루 전에 신청합니다.'},
    {'title': '경비 처리', 'text': '경비는 영수증 첨부 후 매주 금요일까지 제출합니다.'},
    {'title': '계정 잠김', 'text': '계정이 잠기면 보안 포털에서 비밀번호를 재설정합니다.'},
]

@tool('search_company_faq', args_schema=FaqSearchInput)
def search_company_faq(query: str, max_results: int = 3) -> str:
    '''사내 FAQ 샘플에서 관련 항목을 검색합니다.
    휴가, 경비, 계정, 보안 정책 질문에 사용합니다.
    '''
    hits = []
    keywords = query.lower().split()

    for item in FAQ:
        text = item['title'] + ' ' + item['text']
        normalized_text = text.lower()
        if any(keyword in normalized_text for keyword in keywords):
            hits.append('- {}: {}'.format(item['title'], item['text']))

    if not hits:
        return '검색 결과가 없습니다. 다른 키워드로 다시 검색하세요.'

    return 'n'.join(hits[:max_results])

스키마는 단순한 타입 설명이 아니다. 모델에게 "어떤 값을 넣어야 하는지" 알려 주는 안내문이면서, 애플리케이션 입장에서는 잘못된 입력을 막는 검증 장치다. 위 예제처럼 max_results의 범위를 정해 두면 모델이 한 번에 너무 많은 결과를 요구하는 상황도 줄일 수 있다.

파일 조회와 API 호출 도구 만들기

도구가 강력해질수록 위험도 커진다. 특히 파일과 API는 반드시 범위를 좁혀야 한다. 다음 예제는 data/project_notes 폴더 안의 .txt, .md 파일만 읽도록 제한한다.

from pathlib import Path

BASE_DIR = Path('data/project_notes').resolve()

@tool
def read_project_note(filename: str) -> str:
    '''프로젝트 메모 파일을 읽습니다.
    data/project_notes 폴더 안의 .txt 또는 .md 파일 이름만 입력하세요.
    예: sprint.md
    '''
    try:
        target = (BASE_DIR / filename).resolve()

        if BASE_DIR not in target.parents:
            return '오류: 허용된 폴더 밖의 파일은 읽을 수 없습니다.'

        if target.suffix not in {'.txt', '.md'}:
            return '오류: .txt 또는 .md 파일만 읽을 수 있습니다.'

        if not target.is_file():
            return '오류: 파일을 찾을 수 없습니다.'

        text = target.read_text(encoding='utf-8')
        return text[:4000]
    except Exception:
        return '오류: 파일을 읽는 중 문제가 발생했습니다.'

여기서 핵심은 사용자가 입력한 파일 이름을 그대로 믿지 않는 것이다. ../secrets.txt처럼 상위 폴더로 빠져나가려는 경로는 resolve() 후 허용된 폴더 안에 있는지 검사해 막는다. 또 전체 파일을 그대로 넘기지 않고 앞부분만 잘라 반환해 비용과 맥락 오염을 줄인다.

외부 API 도구도 같은 원칙을 따른다. 아래 예제는 실제 네트워크 대신 샘플 주문 데이터를 조회한다. 실무에서는 이 부분을 httpx.get(..., timeout=5.0) 같은 HTTP 호출로 바꾸면 된다.

from pydantic import BaseModel, Field

class OrderStatusInput(BaseModel):
    order_id: str = Field(..., description='주문 번호. 예: ORD-1001')

ORDERS = {
    'ORD-1001': '배송 준비 중',
    'ORD-1002': '배송 중, 내일 도착 예정',
    'ORD-1003': '배송 완료',
}

@tool('lookup_order_status', args_schema=OrderStatusInput)
def lookup_order_status(order_id: str) -> str:
    '''주문 번호로 배송 상태를 조회합니다.
    사용자가 주문, 배송, 도착 예정일을 물어볼 때 사용합니다.
    '''
    try:
        if not order_id.startswith('ORD-'):
            return '오류: 주문 번호는 ORD-로 시작해야 합니다.'

        status = ORDERS.get(order_id)
        if not status:
            return '해당 주문 번호를 찾을 수 없습니다.'

        return f'{order_id} 상태: {status}'
    except Exception:
        return '오류: 주문 상태 조회에 실패했습니다.'

도구 안에서는 예외를 그대로 터뜨리기보다, 모델과 사용자가 이해할 수 있는 오류 문자열을 반환하는 편이 안전하다. 그래야 에이전트가 "다른 인자로 다시 시도할지", "사용자에게 확인할지" 판단할 수 있다. 다만 실무에서는 내부 예외 메시지나 스택 트레이스를 사용자에게 그대로 보여 주지 말고, 자세한 내용은 서버 로그에 남기는 편이 좋다.

모델에 도구를 붙여 호출 의도 확인하기

에이전트를 만들기 전에 모델이 도구를 어떻게 선택하는지 직접 확인해 보자.

from dotenv import load_dotenv
from langchain_openai import ChatOpenAI

load_dotenv()

model = ChatOpenAI(model='gpt-4.1-mini', temperature=0)
tools = [calculate_due_date, search_company_faq, read_project_note, lookup_order_status]

model_with_tools = model.bind_tools(tools)

response = model_with_tools.invoke('ORD-1002 배송 상태를 확인하고, 7일 뒤 후속 연락 날짜도 알려줘.')
print(response.tool_calls)

출력에는 모델이 호출하려는 도구 이름과 인자가 들어 있다. 예를 들면 다음과 같은 형태다. 실제 응답 객체에는 도구 호출 ID나 타입 같은 필드가 더 붙을 수 있지만, 우선은 name과 args를 보면 된다.

[
    {'name': 'lookup_order_status', 'args': {'order_id': 'ORD-1002'}},
    {'name': 'calculate_due_date', 'args': {'days_after': 7}}
]

이 단계에서는 아직 도구가 실행된 것이 아니다. 모델이 "이 도구를 호출하면 되겠다"고 제안한 상태다. 실제 실행은 코드가 한다.

tool_map = {tool.name: tool for tool in tools}

for call in response.tool_calls:
    selected_tool = tool_map[call['name']]
    result = selected_tool.invoke(call['args'])
    print(call['name'], '=>', result)

이 구조를 이해해야 에이전트가 덜 신비롭게 느껴진다. 에이전트는 이 과정을 반복할 뿐이다. 모델이 도구 호출을 요청하고, 실행 결과를 보고, 다음 행동을 결정한다.

에이전트로 도구 사용 흐름 자동화하기

이제 같은 도구를 에이전트에 연결해 보자. 에이전트에는 도구 목록뿐 아니라 기본 행동 규칙도 함께 주는 것이 좋다.

from langchain.agents import create_agent

agent = create_agent(
    model=model,
    tools=tools,
    system_prompt='''
당신은 업무 지원 AI입니다.
필요할 때만 도구를 사용하세요.
도구 결과에 없는 사실을 지어내지 마세요.
도구 호출이 실패하면 같은 시도를 반복하지 말고 사용자에게 필요한 정보를 물어보세요.
파일 삭제, 결제, 외부 발송 같은 파괴적 행동은 수행하지 마세요.
'''
)

result = agent.invoke(
    {
        'messages': [
            {'role': 'user', 'content': 'ORD-1002 배송 상태를 확인하고 7일 뒤 후속 연락 날짜를 알려줘.'}
        ]
    },
    config={'recursion_limit': 8}
)

print(result['messages'][-1].content)

예상되는 최종 답변은 다음과 비슷하다. 날짜 계산 도구가 date.today()를 사용하므로, 실제 날짜는 실행한 날에 따라 달라진다. 예를 들어 실행일이 2026-07-20이라면 다음처럼 나올 수 있다.

ORD-1002의 현재 상태는 "배송 중, 내일 도착 예정"입니다.
오늘로부터 7일 뒤 후속 연락 날짜는 2026-07-27입니다.

에이전트 내부에서는 대략 이런 일이 일어난다.

  1. 사용자 요청을 읽는다.

  2. lookup_order_status가 필요하다고 판단한다.

  3. 주문 번호 ORD-1002로 도구를 호출한다.

  4. 배송 상태 결과를 받는다.

  5. calculate_due_date가 필요하다고 판단한다.

  6. days_after=7로 도구를 호출한다.

  7. 두 결과를 합쳐 최종 답변을 만든다.

이 흐름은 이전 장의 대화 기록과도 연결된다. 같은 세션 안에서 "그 주문은 언제 다시 연락하면 돼?"라고 물으면, 애플리케이션은 이전 대화의 주문 번호를 프롬프트에 넣거나 상태로 관리해야 한다. 모델이 저절로 기억하는 것이 아니라, 애플리케이션이 필요한 상태를 제공하는 것이다.

실패와 반복을 막는 설계 기준

도구 호출 에이전트는 단순 챗봇보다 더 엄격하게 제한해야 한다. 잘못된 답변은 수정하면 되지만, 잘못된 도구 실행은 실제 데이터 변경이나 비용 발생으로 이어질 수 있다.

첫째, 도구 권한을 좁혀라. 파일 도구는 특정 폴더만 읽게 하고, API 도구는 필요한 엔드포인트만 호출하게 한다. 사용자가 입력한 URL이나 파일 경로를 그대로 실행하지 않는다.

둘째, 입력 스키마를 구체적으로 작성하라. str 하나로 모든 것을 받는 도구는 만들기 쉽지만, 모델이 잘못된 값을 넣어도 걸러내기 어렵다. 주문 번호, 날짜, 통화 코드, 최대 검색 개수처럼 제한할 수 있는 값은 스키마로 제한한다.

셋째, 도구 결과의 크기를 제한하라. 검색 결과나 파일 내용 전체를 모델에 넘기면 비용이 커지고 맥락이 흐려진다. 먼저 3~5개 결과만 반환하고, 긴 텍스트는 앞부분만 자르거나 요약한 뒤 전달한다.

넷째, 반복 한도를 둬라. 같은 도구를 실패한 인자로 계속 호출하는 에이전트는 비용을 낭비한다. recursion_limit 같은 실행 한도를 두고, 시스템 프롬프트에도 "같은 실패를 반복하지 말라"고 명시한다. 한도를 높이는 것은 복잡한 그래프를 실행하기 위한 조정일 수 있지만, 무한 반복을 해결하는 근본 대책은 아니다. 실패 조건과 종료 조건을 함께 설계해야 한다.

다섯째, 쓰기 작업에는 확인 단계를 넣어라. 이메일 발송, 결제, 데이터 삭제, 권한 변경은 모델 판단만으로 실행하지 않는다. "다음 내용으로 발송할까요?"처럼 사용자 승인 후 실행하는 별도 단계를 둔다.

여섯째, 로그를 남겨라. 어떤 질문에서 어떤 도구가 어떤 인자로 호출되었는지 기록해야 문제를 재현할 수 있다. 다음 장과 이후 운영 장에서 다룰 LangSmith 추적은 이런 실행 과정을 확인하는 데 유용하다.

정리

도구 호출은 LLM 애플리케이션의 역할을 바꾼다. 모델은 더 이상 답변만 생성하지 않는다. 날짜를 계산하고, 파일을 조회하고, API를 호출하고, 검색 결과를 바탕으로 다음 행동을 결정할 수 있다.

하지만 실제 실행자는 여전히 애플리케이션 코드다. 그래서 좋은 도구 이름, 명확한 설명, 엄격한 입력 스키마, 예외 처리, 반복 제한, 승인 절차가 필요하다. 에이전트의 품질은 모델 성능만으로 결정되지 않는다. 모델이 안전하게 선택할 수 있는 도구를 얼마나 잘 설계했는지가 핵심이다.

다음 장에서는 이 에이전트 루프를 더 명시적인 상태와 분기 흐름으로 다루는 LangGraph를 살펴본다. 단순한 도구 호출을 넘어, 여러 단계의 업무 절차를 그래프로 설계하는 방법을 배운다.

참고

1Tools - Docs by LangChain

2LangGraph overview - Docs by LangChain

책 목록

랭체인 활용법: LLM 애플리케이션 개발부터 RAG·에이전트·운영까지

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기