본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 05. 25. 14:36

에이전트 시리즈 (4): 도구 호출(Tool Calling) 심층 분석 — 에이전트의 손과 눈

요약

에이전트의 성능을 결정짓는 핵심 요소인 도구 호출(Tool Calling)의 설계 원칙을 심층 분석합니다. 도구의 문서화, 검증, 보안, 병렬 호출 및 오류 처리의 중요성을 실제 코드 비교를 통해 설명합니다.

핵심 포인트

  • 도구의 상세한 문서화는 에이전트의 정확한 호출을 유도함
  • 잘 설계된 에러 메시지는 에이전트의 자가 수정 능력을 높임
  • 도구의 충돌이 반드시 에이전트의 전체 시스템 중단으로 이어지지는 않음
  • 도구 설계 품질이 에이전트의 신뢰성과 보안을 결정함

도구는 에이전트의 손과 눈입니다

이전 세 편의 글에서는 사고 프레임워크(Thinking Frameworks)를 다루었습니다. 즉, ReAct가 행동하면서 어떻게 추론하는지, 그리고 Plan-and-Solve가 실행하기 전에 어떻게 계획을 세우는지에 대해 알아보았습니다. 하지만 아무리 뛰어난 추론 프레임워크라도 에이전트(Agent)가 스스로에게만 말할 수 있다면 거의 무용지물입니다.

**도구(Tools)**는 에이전트가 언어 모델(Language Model)의 경계를 넘어설 수 있게 해주는 요소입니다. 도구를 통해 에이전트는 다음과 같은 일을 할 수 있습니다:

  • 실시간 데이터 조회 (주가, 날씨, 뉴스)
  • 파일 시스템(File Systems) 조작
  • 외부 API 호출
  • 코드 및 계산 실행

하지만 도구 설계의 품질이 에이전트의 신뢰성을 직접적으로 결정합니다. 잘못 설계된 도구는 에이전트가 오류 루프(Error Loops)에 빠지게 만들거나, 더 심각하게는 보안 취약점(Security Vulnerabilities)을 생성하게 합니다.

이 글에서는 도구 호출(Tool Calling)의 전체적인 그림을 해체하여 살펴봅니다: 설계(Design), 검증(Validation), 보안(Security), 병렬 호출(Parallel Calls), 그리고 오류 처리(Error Handling) — 실제 실행 결과에 기반한 다섯 가지 차원입니다.

좋은 도구 vs 나쁜 도구: 동일한 작업, 완전히 다른 에이전트 행동

결론을 구체화하기 위해 비교 실험부터 시작하겠습니다.

동일한 "주가 조회" 기능에 대해 두 가지 구현 방식을 비교해 보겠습니다.

나쁜 도구 (세 가지 전형적인 문제):

@tool
def bad_stock_tool(x: str) -> str:
    """Get stock info."""   # ← 빈약한 문서화: 매개변수 설명 없음, 반환 형식 없음, 예시 없음
...

좋은 도구 (이에 대응하는 세 가지 개선 사항):

@tool
def get_stock_price(symbol: str) -> str:
    """Query the current price and daily change of a stock.
...

동일한 쿼리로 두 가지를 테스트합니다: "Please check the price of AAPL and a nonexistent stock XYZ999" (AAPL의 가격과 존재하지 않는 주식인 XYZ999의 가격을 확인해 주세요)

나쁜 도구 실행 추적(Execution Trace):

[Tool Call]    bad_stock_tool(x='AAPL')
[Tool Return]  189.5

...

좋은 도구 실행 추적(Execution Trace):

[Tool Call]    get_stock_price(symbol='AAPL')
[Tool Return]  Apple Inc. (AAPL): $189.50 USD, today +1.23%

...

주목할 만한 두 가지 관찰 사항은 다음과 같습니다:

관찰 사항 1: 잘못된 도구의 KeyError가 에이전트(Agent)를 중단시키지 않았습니다. LangGraph는 해당 예외(exception)를 포착하여 Error: KeyError('XYZ999') Please fix your mistakes.와 같이 감싸서 전달합니다. 에이전트는 여전히 최종 답변을 생성했습니다. 따라서 "도구의 충돌(tool crash) = 에이전트의 충돌(Agent crash)"은 근거 없는 믿음입니다. 프레임워크에는 결함 허용(fault tolerance) 기능이 내장되어 있습니다.

관찰 사항 2: 하지만 출력 품질의 차이는 명확합니다. 잘 설계된 도구의 에러 메시지는 입력이 왜 유효하지 않은지(형식 문제)를 설명하여, 에이전트가 더 도움이 되는 응답을 제공할 수 있게 합니다. 반면 잘못된 도구의 에러는 단순히 예외 이름만 전달하므로, 에이전트는 그저 "존재하지 않습니다"라고 모호하게 말할 수밖에 없습니다.

결론: 충돌하지 않는다고 해서 자동으로 잘 설계된 도구인 것은 아닙니다. 에러 메시지의 품질이 사용자에게 전달되는 에이전트 답변의 품질을 직접적으로 결정합니다.

도구 설계의 세 가지 기둥

위의 비교는 도구 설계의 세 가지 핵심 차원으로 연결됩니다.

기둥 1: 인터페이스(Interface) — 문서화는 계약이다

LLM은 도크스트링(docstring)을 통해 도구를 이해합니다. 불분명한 문서 → LLM의 추측 → 버그로 이어집니다.

완전한 도구 도크스트링에는 다음 내용이 포함되어야 합니다:

@tool
def get_stock_price(symbol: str) -> str:
    """[기능 설명] 주식의 현재 가격과 일일 변동폭을 조회합니다.
...

핵심 포인트: 예시에는 반드시 성공 사례와 실패 사례가 모두 포함되어야 합니다. LLM이 에러 분기(error branches)를 올바르게 처리하려면 실패가 어떤 모습인지 알아야 하기 때문입니다.

기둥 2: 검증(Validation) — Pydantic은 당신의 문지기이다

단일 파라미터 도구의 경우 함수 내부 검증만으로도 충분합니다. 하지만 복잡한 제약 조건이 있는 다중 파라미터 도구의 경우, Pydantic의 BaseModel이 적절한 선택입니다:

class CurrencyConvertInput(BaseModel):
    amount: float = Field(..., gt=0, le=1_000_000_000)
    from_currency: str = Field(...)
...

Pydantic의 차단(interception)을 보여주는 세 가지 테스트 케이스:

# 정상 요청
[Tool Call]    convert_currency(amount=1000, from_currency='USD', to_currency='CNY')
[Tool Return]  1,000.00 USD = 7,250.00 CNY (rate: 1 USD ≈ 7.2500 CNY)
...

Pydantic의 장점:

  1. 사람이 읽기 쉬운 오류 (Human-readable errors): Input should be greater than 0ValueError: invalid amount보다 훨씬 명확합니다.
  2. 자동 타입 변환 (Automatic type coercion): LLM이 문자열 `
class _RateLimiter:
    def __init__(self, max_calls: int, window_seconds: int = 60):
        self._max = max_calls
...

병렬 도구 호출 (Parallel Tool Calls): 이론 vs. 현실

LangGraph는 병렬 도구 호출 (Parallel tool calls)을 지원합니다. 즉, LLM이 단일 응답 내에서 여러 개의 tool_calls를 반환하면, LangGraph는 이를 동시에 실행하여 지연 시간 (Latency)을 크게 단축합니다.

3개 도시의 날씨와 공기 질을 조회하는 경우, 이상적인 흐름은 다음과 같습니다:

LLM 응답:
  → 병렬 호출 [get_weather("Beijing"), get_weather("Shanghai"), get_weather("Chengdu"),
                   get_air_quality("Beijing"), get_air_quality("Shanghai"), get_air_quality("Chengdu")]
...

하지만 실제로는, GLM-4-Flash는 도시를 "동시에" 조회하라고 명시적으로 지시하더라도 병렬 도구 호출을 지원하지 않습니다:

[도구 호출]  get_weather(city='Beijing')
[도구 호출]  get_weather(city='Shanghai')
[도구 호출]  get_weather(city='Chengdu')
...

6개의 호출이 모두 순차적으로 실행되었으며, 각각 별도의 AIMessage로 처리되었습니다.

이는 중요한 실무적 제약 사항입니다: 병렬 도구 호출은 프레임워크뿐만 아니라 모델 자체에 달려 있습니다. OpenAI GPT-4o는 병렬 호출을 지원하지만, 모든 모델이 그런 것은 아닙니다. OpenAI 이외의 모델을 사용할 때는 당연하게 여기지 말고, 항상 사전에 테스트하십시오.

병렬 호출이 실제로 발생했는지 감지하는 방법:

for msg in result["messages"]:
    if isinstance(msg, AIMessage) and msg.tool_calls:
        if len(msg.tool_calls) > 1:
...

오류 분류: 재시도 가능 vs. 재시도 불가능 (Retryable vs. Non-Retryable)

모든 도구 오류가 동일한 것은 아닙니다. 어떤 오류는 재시도를 무의미하게 만들지만 (잘못된 형식, 권한 거부), 어떤 오류는 잠시 시간이 필요할 뿐입니다 (네트워크 타임아웃, 서비스 재시작).

반환 값의 접두사 (Prefix)를 통해 재시도 의도를 전달하십시오:

@tool
def fetch_report(report_id: str, retry_simulation: bool = False) -> str:
    if not re.match(r"^RPT-\d{4}$", report_id):
...

접두사가 에이전트 (Agent)의 동작에 어떤 영향을 미치는지 보여주는 실제 테스트 결과입니다:

형식 오류 (ERROR 접두사)

[Tool Return] ERROR: 잘못된 보고서 ID ('REPORT-001'), RPT-XXXX 형식이 필요합니다
[Agent Reply] 보고서 ID 형식이 잘못되었습니다. RPT-XXXX 형식을 사용해 주세요.
...

결과는 의도와 일치했습니다: ERROR: → 에이전트가 설명하고 사용자에게 수정을 요청함; RETRY: → 에이전트가 재시도한 후 대기를 제안함.

참고: 이 재시도 동작은 프레임워크 수준의 자동 재시도가 아니라, GLM-4-Flash가 문맥 의미론 (Context Semantics)으로부터 추론한 결과입니다. 프로덕션급의 재시도 신뢰성을 확보하려면, 도구 (Tool) 내부 또는 오케스트레이션 계층 (Orchestration Layer)에서 이를 구현하십시오 (예: tenacity 라이브러리 사용).

도구 설계 체크리스트 (Tool Design Checklist)

에이전트 (Agent)에게 도구를 넘겨주기 전에 다음 체크리스트를 검토하십시오:

인터페이스 (Interface)

  • 독스트링 (Docstring)이 파라미터 (Parameter)의 의미와 형식 제약 조건을 명확하게 설명하는가
  • 예시 (Examples)에 성공 사례와 실패 사례가 모두 포함되어 있는가
  • 반환 형식 (Return format)이 일관적인가 (성공과 실패 모두 문자열 사용, 타입 혼용 금지)

검증 (Validation)

  • 단일 파라미터: 함수 내부에서 정규 표현식 (Regex) 또는 조건문으로 검증하는가
  • 다중 파라미터 / 복잡한 제약 조건: @tool(args_schema=XxxInput) + Pydantic을 사용하는가
  • 엣지 케이스 (Edge case) 커버리지: 빈 문자열, 과도하게 큰 입력, 특수 문자 처리

보안 (Security)

  • 파일 작업: 문자 화이트리스트 (Whitelist) + Path.resolve() 샌드박스 체크
  • 데이터베이스 쿼리 (Database queries): 엄격한 형식 검증 + 매개변수화된 쿼리 (Parameterized queries, 문자열 연결 금지)
  • 고빈도 도구: 토큰 버킷 (Token bucket) 방식의 속도 제한 (Rate limiting)

오류 처리 (Error Handling)

  • 재시도 가능한 오류: RETRY: 접두사 + 사유 포함
  • 재시도 불가능한 오류: ERROR: 접두사 + 올바른 형식 힌트 포함
  • 예외 (Exception)가 에이전트에게 그대로 전달되지 않도록 함 (Catch 하여 문자열로 반환)

요약 (Summary)

도구 호출 (Tool calling)은 기술적인 세부 사항처럼 보이지만, 에이전트 신뢰성의 근간입니다. 핵심 요약은 다음과 같습니다:

  1. 도구 충돌(Tool crash) ≠ 에이전트 충돌(Agent crash): 프레임워크가 예외(Exception)를 포착하지만, 에러 메시지의 품질이 에이전트의 최종 답변 품질을 결정합니다.
  2. 문서화(Documentation)는 계약이다: LLM은 독스트링(Docstrings)을 통해 도구를 이해합니다. 따라서 독스트링을 잘 작성해야 합니다.
  3. 입력을 절대 신뢰하지 마라: 에이전트의 "이성적" 추론 단계가 아니라 도구 계층(Tool layer)에서 검증하십시오. 프롬프트 인젝션(Prompt injection)이 에이전트를 하이재킹할 수 있습니다.
  4. 병렬 호출(Parallel calls)은 모델에 달려 있다: LangGraph는 프레임워크 수준에서 병렬 실행을 지원하지만, 모델 또한 이를 지원해야 합니다. 당연하게 가정하기 전에 반드시 확인하십시오.
  5. 에러를 분류하라: RETRY:ERROR:를 구분함으로써 에이전트가 올바른 다음 결정을 내릴 수 있도록 합니다.

다음 편: 의도 인식 및 라우팅 (Intent Recognition and Routing) — 에이전트가 다양한 유형의 사용자 요청에 직면했을 때, 어떻게 의도를 식별하고 적절한 전문 도구 또는 하위 에이전트(Sub-Agent)로 작업을 배정하는지에 대해 다룹니다.

참고 문헌

저의 홈페이지에서 더 유용한 지식과 흥미로운 제품들을 찾아보세요.

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0