🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

Tool-Augmented Agents 완벽 가이드 - 슬라이드 1/6
A

AI Generated

2025. 12. 26. · 2 Views

Tool-Augmented Agents 완벽 가이드

LLM에 도구를 연결하여 검색, API 호출, 계산 등 실제 작업을 수행하는 에이전트를 만드는 방법을 초급 개발자도 쉽게 이해할 수 있도록 설명합니다. 실무에서 바로 활용할 수 있는 멀티 툴 에이전트와 API 통합 예제를 포함합니다.


목차

  1. 도구_선택_전략
  2. 도구_실행_관리
  3. 도구_결과_해석
  4. 실습_멀티_툴_에이전트
  5. 실습_API_통합_에이전트

1. 도구 선택 전략

김개발 씨는 최근 LLM 기반 챗봇 프로젝트를 시작했습니다. 사용자가 날씨를 물어보면 실시간 날씨 정보를 알려주고, 계산을 요청하면 정확한 값을 계산해야 합니다.

그런데 LLM은 학습 데이터만 알고 있어서 실시간 정보를 모릅니다. 선배 박시니어 씨가 말했습니다.

"도구를 연결해야죠. 에이전트가 스스로 필요한 도구를 선택하게 만들어보세요."

도구 선택 전략은 LLM 에이전트가 여러 도구 중에서 상황에 맞는 적절한 도구를 자동으로 고르는 방법입니다. 마치 요리사가 칼, 프라이팬, 냄비 중에서 지금 요리에 필요한 도구를 선택하는 것처럼, 에이전트도 날씨 API, 계산기, 검색 엔진 중에서 사용자 질문에 맞는 도구를 판단합니다.

올바른 도구 선택 전략을 구현하면 에이전트가 훨씬 똑똑하게 동작합니다.

다음 코드를 살펴봅시다.

# 도구 정의: 각 도구의 이름, 설명, 파라미터를 명시합니다
tools = [
    {
        "name": "get_weather",
        "description": "실시간 날씨 정보를 가져옵니다. 도시 이름을 입력하세요.",
        "parameters": {"city": "string"}
    },
    {
        "name": "calculator",
        "description": "수학 계산을 수행합니다. 수식을 입력하세요.",
        "parameters": {"expression": "string"}
    }
]

# LLM에게 도구 목록과 사용자 질문을 함께 전달
user_question = "서울 날씨 어때?"
response = llm.generate(
    prompt=f"사용 가능한 도구: {tools}\n질문: {user_question}\n어떤 도구를 사용할까요?",
    tools=tools  # 도구 메타데이터 전달
)
# LLM이 자동으로 get_weather 선택

김개발 씨는 처음에는 간단하게 생각했습니다. 사용자가 날씨를 물으면 날씨 API를 호출하고, 계산을 요청하면 계산기를 쓰면 되지 않을까요?

하지만 문제가 있었습니다. 사용자는 항상 명확하게 말하지 않습니다.

"오늘 서울 기온이 20도인데, 화씨로는 몇 도야?"라고 물으면 날씨 API와 계산기를 둘 다 써야 합니다. 박시니어 씨가 설명했습니다.

"에이전트가 스스로 판단하게 만들어야 해요. 도구마다 명확한 설명을 붙이면 LLM이 알아서 선택합니다." Tool-Augmented Agent란 무엇일까요?

쉽게 비유하자면, 도구를 장착한 에이전트는 마치 만능 비서와 같습니다. 비서가 일정 관리, 이메일 작성, 자료 검색 등 다양한 업무를 처리하듯이, LLM 에이전트도 여러 도구를 활용하여 실제 작업을 수행합니다.

중요한 것은 비서가 상황에 맞는 업무 방식을 선택하듯이, 에이전트도 적절한 도구를 골라야 한다는 점입니다. 도구 선택이 없던 시절에는 어땠을까요?

개발자들은 모든 경우의 수를 직접 코딩해야 했습니다. if문으로 "날씨"라는 단어가 들어가면 날씨 API를, "계산"이라는 단어가 있으면 계산기를 호출하는 식이었습니다.

코드가 길어지고, 새로운 도구를 추가할 때마다 조건문을 수정해야 했습니다. 더 큰 문제는 복잡한 질문을 처리할 수 없다는 것이었습니다.

"서울 날씨를 확인하고 화씨로 변환해줘"라는 요청은 두 가지 도구를 순차적으로 사용해야 하는데, 이런 로직을 일일이 작성하기는 너무 어려웠습니다. 바로 이런 문제를 해결하기 위해 도구 선택 전략이 등장했습니다.

도구 선택 전략을 사용하면 LLM이 자연어 이해 능력을 활용하여 자동으로 도구를 고릅니다. 또한 여러 도구를 조합하여 사용하는 것도 가능해집니다.

무엇보다 새로운 도구를 추가할 때 기존 코드를 수정할 필요가 없다는 큰 이점이 있습니다. 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 tools 리스트에 각 도구의 메타데이터를 정의합니다. name은 도구의 고유 이름이고, description은 LLM이 이해할 수 있도록 자연어로 도구의 기능을 설명합니다.

이 설명이 핵심입니다. LLM은 이 설명을 읽고 "아, 이 도구는 날씨 정보를 가져오는구나"라고 판단합니다.

parameters는 도구가 어떤 입력값을 받는지 명시합니다. 다음으로 user_question에 사용자의 실제 질문을 담습니다.

llm.generate 함수를 호출할 때 tools 파라미터로 도구 목록을 함께 전달합니다. 이렇게 하면 LLM은 "서울 날씨 어때?"라는 질문을 분석하고, tools 중에서 get_weather가 가장 적합하다고 판단합니다.

실제 현업에서는 어떻게 활용할까요? 예를 들어 고객 지원 챗봇을 개발한다고 가정해봅시다.

고객이 "내 주문 상태 알려줘"라고 물으면 주문 조회 API를, "환불하고 싶어"라고 하면 환불 처리 API를 호출해야 합니다. 도구 선택 전략을 적용하면 각 API의 설명만 잘 작성해두면 에이전트가 알아서 적절한 API를 선택합니다.

많은 기업에서 이런 패턴을 적극적으로 사용하고 있습니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 도구 설명을 너무 모호하게 작성하는 것입니다. "데이터를 가져옵니다"라고만 쓰면 LLM이 정확히 어떤 데이터인지 알 수 없어서 잘못된 도구를 선택할 수 있습니다.

따라서 "특정 도시의 실시간 날씨 정보를 가져옵니다"처럼 구체적으로 설명해야 합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다. "아, 도구마다 명확한 설명을 달아주면 LLM이 알아서 선택하는구나!" 도구 선택 전략을 제대로 이해하면 더 유연하고 확장 가능한 에이전트를 만들 수 있습니다.

여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - 도구 설명은 최대한 구체적이고 명확하게 작성하세요

  • 비슷한 기능의 도구가 여러 개라면 차이점을 명시하세요
  • LLM이 헷갈릴 만한 도구는 예시를 설명에 포함하세요

2. 도구 실행 관리

김개발 씨는 도구 선택은 성공했지만, 새로운 문제에 부딪혔습니다. LLM이 "get_weather" 도구를 선택했는데, 실제로 날씨 API를 호출하는 것은 어떻게 할까요?

그리고 API 호출이 실패하면 어떻게 처리해야 할까요? 박시니어 씨가 웃으며 말했습니다.

"도구를 선택하는 것과 실행하는 것은 별개예요. 안전하게 실행하는 방법을 배워야죠."

도구 실행 관리는 LLM이 선택한 도구를 실제로 호출하고, 오류를 처리하며, 결과를 받아오는 과정입니다. 마치 요리사가 레시피를 보고 실제로 요리를 만드는 것처럼, 에이전트도 선택한 도구를 안전하게 실행해야 합니다.

타임아웃, 에러 처리, 재시도 로직을 포함하여 안정적인 실행 환경을 구축하는 것이 핵심입니다.

다음 코드를 살펴봅시다.

import requests
from typing import Dict, Any

def execute_tool(tool_name: str, params: Dict[str, Any]) -> Dict[str, Any]:
    """도구를 실행하고 결과를 반환합니다"""
    try:
        if tool_name == "get_weather":
            # 실제 날씨 API 호출
            response = requests.get(
                f"https://api.weather.com/v1/current?city={params['city']}",
                timeout=5  # 5초 타임아웃
            )
            response.raise_for_status()  # HTTP 에러 체크
            return {"status": "success", "data": response.json()}

        elif tool_name == "calculator":
            # 안전한 수식 계산
            result = eval(params['expression'])  # 주의: 실제로는 ast.literal_eval 사용
            return {"status": "success", "data": result}

    except requests.Timeout:
        return {"status": "error", "message": "API 응답 시간 초과"}
    except Exception as e:
        return {"status": "error", "message": str(e)}

# 사용 예시
result = execute_tool("get_weather", {"city": "Seoul"})
if result["status"] == "success":
    print(f"날씨 데이터: {result['data']}")

김개발 씨는 첫 번째 버전을 만들었습니다. LLM이 선택한 도구 이름을 받아서 그냥 API를 호출했습니다.

하지만 테스트 중에 문제가 발생했습니다. 날씨 API 서버가 느려서 응답이 30초나 걸렸고, 그동안 프로그램이 멈춰 있었습니다.

또 다른 경우에는 잘못된 도시 이름을 입력했더니 프로그램이 크래시되었습니다. 박시니어 씨가 코드를 보더니 말했습니다.

"도구 실행은 항상 실패할 수 있다고 가정해야 해요. 타임아웃, 에러 처리, 재시도 로직이 필요합니다." 도구 실행 관리의 핵심은 무엇일까요?

쉽게 비유하자면, 도구 실행 관리는 마치 전기 제품에 달린 안전장치와 같습니다. 과부하가 걸리면 자동으로 전원이 차단되고, 문제가 생기면 경고등이 켜집니다.

이처럼 도구 실행도 타임아웃으로 무한 대기를 막고, try-except로 예외를 잡아서 처리하며, 에러 메시지를 명확하게 반환해야 합니다. 도구 실행 관리가 없던 시절에는 어땠을까요?

개발자들은 단순하게 API를 호출하고 결과를 기다렸습니다. 하지만 실제 서비스 환경에서는 네트워크가 불안정하거나 외부 API 서버가 다운될 수 있습니다.

이런 상황에서 아무런 대비책이 없으면 사용자는 무한정 기다리거나 에러 화면만 보게 됩니다. 더 큰 문제는 에이전트가 왜 실패했는지 알 수 없어서 디버깅이 불가능하다는 것이었습니다.

바로 이런 문제를 해결하기 위해 체계적인 도구 실행 관리가 필요합니다. 도구 실행 관리를 적용하면 타임아웃으로 응답 시간을 제한할 수 있습니다.

또한 예외 처리로 모든 종류의 에러를 안전하게 잡아낼 수 있습니다. 무엇보다 에러 메시지를 구조화하여 LLM이 이해하고 대응할 수 있게 만든다는 큰 이점이 있습니다.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 execute_tool 함수는 tool_name과 params를 받아서 실제 도구를 실행합니다.

try 블록 안에서 도구별로 다른 로직을 수행하는데, 이렇게 분기 처리하면 각 도구의 특성에 맞게 실행할 수 있습니다. get_weather의 경우 requests.get으로 HTTP 요청을 보내는데, timeout=5로 5초 제한을 겁니다.

이렇게 하면 API가 느려도 최대 5초만 기다리고 예외가 발생합니다. response.raise_for_status()는 HTTP 상태 코드가 400번대나 500번대면 예외를 발생시켜서 에러를 감지할 수 있게 합니다.

결과는 항상 딕셔너리 형태로 반환하는데, status 필드로 성공/실패를 명확히 구분합니다. 성공하면 data에 실제 결과를, 실패하면 message에 에러 내용을 담습니다.

이런 일관된 포맷 덕분에 LLM이 결과를 쉽게 해석할 수 있습니다. except 블록에서는 Timeout 에러와 일반 에러를 각각 처리합니다.

에러 종류에 따라 다른 메시지를 반환하면 문제를 빠르게 파악할 수 있습니다. 실제 현업에서는 어떻게 활용할까요?

예를 들어 금융 서비스에서 계좌 조회 에이전트를 만든다고 가정해봅시다. 계좌 조회 API가 응답하지 않으면 사용자에게 "일시적으로 조회가 불가능합니다"라고 안내하고, 재시도 버튼을 제공할 수 있습니다.

또한 에러 로그를 남겨서 나중에 분석하고 시스템을 개선할 수 있습니다. 실제로 많은 서비스에서 이런 패턴을 사용하여 안정성을 높이고 있습니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 모든 예외를 똑같이 처리하는 것입니다.

"에러가 발생했습니다"라고만 하면 사용자도, LLM도 무엇이 잘못되었는지 알 수 없습니다. 따라서 네트워크 에러, 인증 에러, 데이터 포맷 에러 등을 구분하여 처리해야 합니다.

또 하나 중요한 점은 eval 함수를 조심해야 한다는 것입니다. 위 코드에서 calculator는 예시로 eval을 사용했지만, 실제로는 보안 위험이 있습니다.

사용자가 악의적인 코드를 입력하면 시스템이 공격받을 수 있으므로, ast.literal_eval이나 전용 수식 파서를 사용해야 합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

박시니어 씨의 조언을 들은 김개발 씨는 타임아웃과 에러 처리를 추가했습니다. 테스트해보니 API가 느려도 5초 후에 적절한 에러 메시지를 반환했고, 잘못된 입력에도 크래시 없이 안전하게 처리되었습니다.

도구 실행 관리를 제대로 구현하면 안정적이고 신뢰할 수 있는 에이전트를 만들 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - 모든 외부 API 호출에는 반드시 타임아웃을 설정하세요

  • 에러 메시지는 구체적으로 작성하여 디버깅을 쉽게 만드세요
  • 중요한 도구는 재시도 로직을 추가하여 일시적 장애에 대응하세요

3. 도구 결과 해석

김개발 씨는 도구 실행까지 성공했지만, 또 다른 난관에 부딪혔습니다. 날씨 API가 반환한 JSON 데이터를 LLM에게 그대로 전달했더니, LLM이 이상한 답변을 생성했습니다.

"temperature_celsius: 15.3"이라는 데이터를 받았는데 "온도는 temperature_celsius입니다"라고 말하는 겁니다. 박시니어 씨가 설명했습니다.

"도구의 결과를 LLM이 이해할 수 있는 형태로 가공해야 해요."

도구 결과 해석은 도구가 반환한 원시 데이터를 LLM이 이해하고 활용할 수 있는 자연어 형태로 변환하는 과정입니다. 마치 통역사가 외국어를 우리말로 번역하는 것처럼, 에이전트도 JSON, XML 같은 기계 언어를 사람이 읽을 수 있는 문장으로 바꿔야 합니다.

이렇게 하면 LLM이 결과를 정확히 파악하고 사용자에게 자연스러운 답변을 제공할 수 있습니다.

다음 코드를 살펴봅시다.

def interpret_tool_result(tool_name: str, result: Dict[str, Any]) -> str:
    """도구 실행 결과를 LLM이 이해할 수 있는 텍스트로 변환"""
    if result["status"] == "error":
        # 에러는 명확하게 설명
        return f"도구 실행 실패: {result['message']}"

    if tool_name == "get_weather":
        data = result["data"]
        # JSON을 자연어로 변환
        weather_text = f"""
        {data['city']}의 현재 날씨 정보입니다.
        - 기온: {data['temperature']}도
        - 날씨: {data['condition']}
        - 습도: {data['humidity']}%
        """
        return weather_text.strip()

    elif tool_name == "calculator":
        # 계산 결과를 문장으로
        return f"계산 결과는 {result['data']}입니다."

    return str(result["data"])  # 기본값

# 사용 예시
raw_result = {"status": "success", "data": {"city": "Seoul", "temperature": 15, "condition": "맑음", "humidity": 60}}
interpreted = interpret_tool_result("get_weather", raw_result)
# LLM에게 전달: "Seoul의 현재 날씨 정보입니다. 기온: 15도..."

김개발 씨는 처음에는 API 응답을 그대로 LLM에게 던져주면 알아서 처리할 거라고 생각했습니다. LLM은 똑똑하니까 JSON 정도는 읽을 수 있겠지요.

하지만 실제로 테스트해보니 결과가 좋지 않았습니다. LLM은 JSON 구조를 이해하지만, 필드 이름이 축약어이거나 복잡한 중첩 구조면 종종 헷갈려했습니다.

박시니어 씨가 코드를 보더니 말했습니다. "LLM도 사람처럼 명확한 문장을 선호해요.

데이터를 자연어로 풀어서 설명해주면 훨씬 정확하게 이해합니다." 도구 결과 해석은 왜 중요할까요? 쉽게 비유하자면, 도구 결과 해석은 마치 의사가 검사 결과지를 환자에게 설명하는 것과 같습니다.

혈압 수치가 "120/80"이라고만 보여주는 게 아니라 "정상 범위입니다"라고 말해주는 거죠. 이처럼 원시 데이터를 맥락과 함께 설명하면 LLM이 훨씬 쉽게 이해하고, 사용자에게 더 자연스러운 답변을 만들 수 있습니다.

도구 결과 해석이 없던 시절에는 어땠을까요? 개발자들은 API 응답 JSON을 문자열로 변환해서 LLM에게 전달했습니다.

LLM은 그 안에서 필요한 정보를 찾아내야 했는데, 때로는 잘못된 필드를 읽거나 단위를 혼동했습니다. 특히 복잡한 API 응답은 수십 개의 필드가 있어서 LLM이 핵심 정보를 놓치기 쉬웠습니다.

더 큰 문제는 에러 메시지도 기계적으로 전달되어서 사용자가 이해하기 어려웠다는 것입니다. 바로 이런 문제를 해결하기 위해 체계적인 결과 해석이 필요합니다.

도구 결과 해석을 적용하면 LLM이 핵심 정보에 집중할 수 있습니다. 또한 단위와 맥락을 명시하여 혼동을 방지할 수 있습니다.

무엇보다 에러 메시지도 사용자 친화적으로 만들어서 더 나은 사용자 경험을 제공한다는 큰 이점이 있습니다. 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 interpret_tool_result 함수는 tool_name과 result를 받아서 자연어 문자열을 반환합니다. 제일 먼저 status를 체크하여 에러인 경우 명확한 에러 메시지로 변환합니다.

"도구 실행 실패: API 응답 시간 초과"처럼 구체적으로 설명하면 LLM이 사용자에게 적절한 안내를 할 수 있습니다. get_weather의 경우 JSON 데이터에서 필요한 필드를 추출하여 불릿 포인트 형태로 정리합니다.

단순히 "temperature: 15"가 아니라 "기온: 15도"라고 쓰면 LLM이 이것이 온도라는 것을 명확히 알 수 있습니다. 또한 도시 이름, 날씨 상태, 습도를 한 문단으로 묶어서 전체적인 맥락을 제공합니다.

calculator의 경우 숫자만 반환하는 게 아니라 "계산 결과는 ~입니다"라는 완전한 문장으로 만듭니다. 이렇게 하면 LLM이 이 값이 무엇인지 바로 이해하고 사용자 질문에 맞춰 답변할 수 있습니다.

마지막으로 기본값으로 str(result["data"])를 반환하는데, 이는 예상치 못한 도구가 추가되어도 최소한의 정보는 전달하기 위함입니다. 물론 실제로는 모든 도구마다 전용 해석 로직을 만드는 것이 좋습니다.

실제 현업에서는 어떻게 활용할까요? 예를 들어 의료 상담 챗봇을 만든다고 가정해봅시다.

혈액 검사 API가 20개의 수치를 반환하는데, 이를 그대로 보여주면 사용자가 당황합니다. 대신 "혈당 수치가 정상 범위입니다", "콜레스테롤이 약간 높으니 식단 조절이 필요합니다"처럼 해석해주면 LLM이 이를 바탕으로 친절한 상담을 할 수 있습니다.

실제로 많은 헬스케어 서비스에서 이런 패턴을 사용합니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 너무 많은 정보를 한꺼번에 제공하는 것입니다. API가 100개의 필드를 반환한다고 해서 전부 설명하면 LLM이 오히려 혼란스러워합니다.

따라서 사용자 질문과 관련된 핵심 정보만 추려서 전달해야 합니다. 또 하나 조심할 점은 단위를 명시하는 것입니다.

"기온: 15"라고만 하면 섭씨인지 화씨인지 모릅니다. "기온: 15도(섭씨)"처럼 단위를 붙여주면 LLM이 정확하게 이해합니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 조언대로 결과 해석 로직을 추가하자, LLM의 답변이 훨씬 자연스러워졌습니다.

"서울의 현재 기온은 15도이며 날씨가 맑습니다"라고 정확하게 말하더군요. 도구 결과 해석을 제대로 구현하면 LLM이 더 똑똑하고 정확한 답변을 제공할 수 있습니다.

여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - 핵심 정보만 추려서 간결하게 전달하세요

  • 단위와 맥락을 명시하여 혼동을 방지하세요
  • 에러 메시지도 사용자가 이해할 수 있는 자연어로 변환하세요

4. 실습 멀티 툴 에이전트

김개발 씨는 이제 하나의 도구를 쓰는 것은 자신 있게 되었습니다. 하지만 선배가 새로운 도전 과제를 던졌습니다.

"사용자가 '서울 날씨를 확인하고 화씨로 변환해줘'라고 하면 어떻게 처리할래요?" 두 가지 도구를 순차적으로 사용해야 하는 상황입니다. 박시니어 씨가 웃으며 말했습니다.

"이제 진짜 에이전트를 만들 시간이에요. 여러 도구를 조합해서 사용하는 거죠."

멀티 툴 에이전트는 여러 도구를 순차적으로 또는 병렬적으로 사용하여 복잡한 작업을 수행하는 에이전트입니다. 마치 요리사가 재료를 손질하고, 볶고, 양념하는 여러 단계를 거쳐 요리를 완성하듯이, 에이전트도 여러 도구를 조합하여 최종 결과를 만듭니다.

도구 간 데이터 전달과 실행 순서 관리가 핵심입니다.

다음 코드를 살펴봅시다.

class MultiToolAgent:
    def __init__(self, tools):
        self.tools = {tool["name"]: tool for tool in tools}

    def run(self, user_query: str):
        """사용자 질문을 처리하는 메인 루프"""
        conversation_history = [{"role": "user", "content": user_query}]

        while True:
            # LLM이 다음 행동 결정
            response = self.llm_decide_next_action(conversation_history)

            if response["action"] == "answer":
                # 최종 답변 반환
                return response["content"]

            elif response["action"] == "use_tool":
                # 도구 실행
                tool_name = response["tool_name"]
                tool_params = response["tool_params"]

                result = execute_tool(tool_name, tool_params)
                interpreted = interpret_tool_result(tool_name, result)

                # 대화 히스토리에 추가
                conversation_history.append({
                    "role": "tool",
                    "tool_name": tool_name,
                    "content": interpreted
                })

            # 무한 루프 방지 (최대 10회)
            if len(conversation_history) > 20:
                return "작업이 너무 복잡하여 완료할 수 없습니다."

# 사용 예시
agent = MultiToolAgent(tools)
answer = agent.run("서울 날씨를 확인하고 섭씨를 화씨로 변환해줘")
# 1단계: get_weather로 15도 확인
# 2단계: calculator로 (15 * 9/5) + 32 = 59 계산
# 최종 답변: "서울은 현재 15도(섭씨)이며, 화씨로는 59도입니다."

김개발 씨는 새로운 요구사항을 듣고 생각에 잠겼습니다. 하나의 도구만 쓸 때는 간단했는데, 두 개를 조합하려니 복잡해졌습니다.

날씨 API를 먼저 호출하고, 그 결과를 가져와서 계산기에 넘겨야 하는데, 이 과정을 어떻게 자동화할까요? 박시니어 씨가 설명했습니다.

"에이전트가 스스로 판단하게 만들어야 해요. LLM에게 대화 히스토리를 주고 '다음에 뭘 할까?'라고 물어보면 됩니다." 멀티 툴 에이전트의 작동 원리는 무엇일까요?

쉽게 비유하자면, 멀티 툴 에이전트는 마치 체스 게임을 하는 것과 같습니다. 체스 선수는 현재 판의 상태를 보고 다음 수를 결정합니다.

마찬가지로 에이전트도 지금까지의 대화 히스토리를 보고 "다음에는 이 도구를 써야겠다" 또는 "이제 충분한 정보가 모였으니 답변하자"라고 판단합니다. 이런 반복적인 사고 과정을 통해 복잡한 작업을 단계별로 해결합니다.

멀티 툴 에이전트가 없던 시절에는 어땠을까요? 개발자들은 모든 경우의 수를 직접 프로그래밍해야 했습니다.

"날씨 확인 후 변환"이라는 시나리오를 위해 전용 함수를 만들고, "주식 조회 후 계산"을 위해 또 다른 함수를 만들었습니다. 새로운 조합이 필요할 때마다 코드를 추가해야 했고, 유지보수가 악몽이었습니다.

더 큰 문제는 사용자가 예상치 못한 방식으로 질문하면 처리할 수 없다는 것이었습니다. 바로 이런 문제를 해결하기 위해 멀티 툴 에이전트 패턴이 등장했습니다.

멀티 툴 에이전트를 사용하면 에이전트가 자율적으로 도구를 조합할 수 있습니다. 또한 새로운 도구를 추가해도 기존 로직을 수정할 필요가 없습니다.

무엇보다 사용자의 복잡한 질문을 자동으로 여러 단계로 분해하여 처리한다는 큰 이점이 있습니다. 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 MultiToolAgent 클래스는 초기화 시 도구 목록을 받아서 딕셔너리로 저장합니다. 이렇게 하면 도구 이름으로 빠르게 찾을 수 있습니다.

run 메서드가 핵심입니다. 먼저 conversation_history에 사용자 질문을 추가합니다.

그리고 while True 무한 루프를 돌면서 LLM에게 "다음에 뭘 할까?"라고 묻습니다. llm_decide_next_action은 대화 히스토리를 분석하여 "도구를 써야 할지" 또는 "이제 답변할지"를 결정합니다.

만약 LLM이 "도구를 쓰자"고 판단하면, 어떤 도구를 쓸지와 파라미터를 반환합니다. 에이전트는 그 도구를 실행하고, 결과를 해석한 후, conversation_history에 추가합니다.

이렇게 하면 다음 턴에 LLM이 이전 도구의 결과를 참고하여 다음 행동을 결정할 수 있습니다. 만약 LLM이 "이제 답변하자"고 판단하면 최종 답변을 반환하고 루프를 종료합니다.

또한 무한 루프 방지를 위해 대화가 20턴을 넘으면 강제로 종료합니다. 이는 에이전트가 같은 도구를 반복 호출하는 버그를 막기 위함입니다.

실제 사용 예시를 보면, 사용자가 "서울 날씨를 확인하고 섭씨를 화씨로 변환해줘"라고 물으면 에이전트는 다음과 같이 동작합니다. 첫 번째 턴: LLM이 "먼저 날씨를 확인해야겠다"고 판단하여 get_weather 도구를 호출합니다.

결과로 "서울의 현재 기온은 15도입니다"를 받습니다. 두 번째 턴: LLM이 "이제 화씨로 변환해야겠다"고 판단하여 calculator 도구를 호출합니다.

파라미터로 "(15 * 9/5) + 32"를 전달하고, 결과로 59를 받습니다. 세 번째 턴: LLM이 "이제 충분한 정보가 모였으니 답변하자"고 판단하여 최종 답변을 생성합니다.

"서울은 현재 15도(섭씨)이며, 화씨로는 59도입니다." 이 모든 과정이 자동으로 이루어집니다. 개발자는 도구만 정의해두면 됩니다.

실제 현업에서는 어떻게 활용할까요? 예를 들어 여행 예약 서비스를 만든다고 가정해봅시다.

사용자가 "부산까지 기차표를 예매하고 호텔도 함께 검색해줘"라고 하면, 에이전트는 기차 예약 API와 호텔 검색 API를 순차적으로 호출합니다. 또한 "기차 도착 시간 이후에 체크인 가능한 호텔"처럼 도구 간 데이터를 연결하여 똑똑한 추천을 제공할 수 있습니다.

실제로 많은 여행 플랫폼에서 이런 패턴을 사용합니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 무한 루프에 대한 대비가 없는 것입니다. LLM이 잘못 판단하여 같은 도구를 계속 호출하면 시스템이 멈춥니다.

따라서 최대 턴 수를 제한하고, 같은 도구를 연속으로 호출하는지 감지하는 로직을 추가해야 합니다. 또 하나 중요한 점은 대화 히스토리를 관리하는 것입니다.

히스토리가 너무 길어지면 LLM의 컨텍스트 한계를 넘을 수 있으므로, 중요한 정보만 요약하여 유지하는 전략이 필요합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

멀티 툴 에이전트를 구현한 김개발 씨는 신기한 듯이 테스트를 해봤습니다. "제주도 날씨를 확인하고 비가 온다면 실내 관광지를 추천해줘"라고 입력했더니, 에이전트가 날씨 API와 관광지 API를 조합하여 완벽한 답변을 만들어냈습니다.

멀티 툴 에이전트를 제대로 구현하면 복잡한 작업을 자동으로 처리하는 똑똑한 시스템을 만들 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - 무한 루프 방지를 위해 최대 턴 수를 제한하세요

  • 대화 히스토리가 길어지면 요약하여 컨텍스트를 관리하세요
  • LLM의 판단을 로그로 남겨서 디버깅을 쉽게 만드세요

5. 실습 API 통합 에이전트

김개발 씨는 이제 에이전트의 기본기를 다졌습니다. 박시니어 씨가 실전 프로젝트를 던졌습니다.

"우리 회사 CRM API를 에이전트에 연결해보세요. 고객 정보 조회, 주문 내역 검색, 티켓 생성까지 가능하게 만들어야 합니다." 실제 비즈니스 API를 연결하는 것은 예제와는 다른 어려움이 있었습니다.

인증, 에러 처리, 데이터 포맷 변환 등 고려할 것이 많았습니다.

API 통합 에이전트는 실제 비즈니스 API를 에이전트에 연결하여 실무 작업을 자동화하는 시스템입니다. 마치 비서가 회사의 여러 시스템에 접근하여 업무를 처리하는 것처럼, 에이전트도 CRM, ERP, 결제 시스템 등 다양한 API를 활용합니다.

인증, 보안, 에러 처리, 데이터 변환을 체계적으로 관리하는 것이 핵심입니다.

다음 코드를 살펴봅시다.

import os
from typing import Dict, Any

class CRMAgent:
    def __init__(self, api_key: str):
        self.api_key = api_key
        self.base_url = "https://api.crm.example.com/v1"

        # API 도구 정의
        self.tools = [
            {
                "name": "get_customer",
                "description": "고객 ID로 고객 정보를 조회합니다",
                "parameters": {"customer_id": "string"},
                "endpoint": "/customers/{customer_id}",
                "method": "GET"
            },
            {
                "name": "search_orders",
                "description": "고객의 주문 내역을 검색합니다",
                "parameters": {"customer_id": "string", "status": "string"},
                "endpoint": "/orders",
                "method": "GET"
            },
            {
                "name": "create_ticket",
                "description": "고객 지원 티켓을 생성합니다",
                "parameters": {"customer_id": "string", "subject": "string", "description": "string"},
                "endpoint": "/tickets",
                "method": "POST"
            }
        ]

    def execute_api_tool(self, tool_name: str, params: Dict[str, Any]) -> Dict[str, Any]:
        """API 도구를 실행하고 결과 반환"""
        tool = next((t for t in self.tools if t["name"] == tool_name), None)
        if not tool:
            return {"status": "error", "message": "도구를 찾을 수 없습니다"}

        try:
            # URL 구성
            url = self.base_url + tool["endpoint"]
            if tool["method"] == "GET":
                url = url.format(**params)  # URL 파라미터 치환

            # HTTP 요청
            headers = {
                "Authorization": f"Bearer {self.api_key}",
                "Content-Type": "application/json"
            }

            if tool["method"] == "GET":
                response = requests.get(url, headers=headers, timeout=10)
            else:
                response = requests.post(url, json=params, headers=headers, timeout=10)

            # 응답 처리
            if response.status_code == 200:
                return {"status": "success", "data": response.json()}
            elif response.status_code == 401:
                return {"status": "error", "message": "인증 실패: API 키를 확인하세요"}
            elif response.status_code == 404:
                return {"status": "error", "message": "데이터를 찾을 수 없습니다"}
            else:
                return {"status": "error", "message": f"API 오류: {response.status_code}"}

        except requests.Timeout:
            return {"status": "error", "message": "API 응답 시간 초과"}
        except Exception as e:
            return {"status": "error", "message": f"예외 발생: {str(e)}"}

# 사용 예시
agent = CRMAgent(api_key=os.getenv("CRM_API_KEY"))
result = agent.execute_api_tool("get_customer", {"customer_id": "12345"})
# 실제 CRM에서 고객 정보를 가져옴

김개발 씨는 지금까지 예제 API로만 테스트했습니다. 날씨 API는 인증이 필요 없고, 계산기는 로컬에서 돌아가니 간단했습니다.

하지만 회사의 CRM API는 달랐습니다. API 키가 필요하고, 잘못된 요청을 보내면 계정이 잠기며, 응답 데이터가 복잡했습니다.

박시니어 씨가 조언했습니다. "실제 API를 연결할 때는 보안과 안정성이 가장 중요해요.

API 키는 환경 변수로 관리하고, 모든 HTTP 상태 코드를 처리해야 합니다." API 통합 에이전트의 핵심은 무엇일까요? 쉽게 비유하자면, API 통합 에이전트는 마치 대사관 직원과 같습니다.

대사관 직원은 여권을 확인하고, 서류를 검토하고, 규정에 맞게 처리합니다. 이처럼 API 에이전트도 인증을 확인하고, 요청을 검증하고, 에러를 적절히 처리하여 안전하게 비즈니스 시스템과 통신합니다.

API 통합이 제대로 되지 않던 시절에는 어땠을까요? 개발자들은 각 API마다 별도의 스크립트를 작성했습니다.

인증 로직도 중복되고, 에러 처리도 제각각이었습니다. API 키가 코드에 하드코딩되어 보안 사고가 발생하기도 했습니다.

더 큰 문제는 API 스펙이 변경되면 여러 곳을 수정해야 하는 유지보수의 악몽이었습니다. 바로 이런 문제를 해결하기 위해 체계적인 API 통합 패턴이 필요합니다.

API 통합 에이전트를 사용하면 모든 API를 일관된 방식으로 관리할 수 있습니다. 또한 인증과 에러 처리를 중앙화하여 보안을 강화할 수 있습니다.

무엇보다 새로운 API를 추가할 때 도구 정의만 추가하면 되므로 확장이 쉽다는 큰 이점이 있습니다. 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 CRMAgent 클래스는 초기화 시 API 키를 받습니다. 이 키는 외부에서 주입받아서 코드에 하드코딩하지 않습니다.

실제로는 os.getenv()로 환경 변수에서 읽어오는 것이 안전합니다. tools 리스트에는 각 API 엔드포인트를 도구로 정의합니다.

기존 도구 정의에 더해서 endpoint와 method 필드를 추가했습니다. 이렇게 하면 실제 HTTP 요청을 어떻게 보낼지 명시할 수 있습니다.

execute_api_tool 메서드가 핵심입니다. 먼저 도구 이름으로 메타데이터를 찾고, URL을 구성합니다.

GET 요청의 경우 URL 파라미터를 format으로 치환하고, POST 요청의 경우 JSON 바디에 담습니다. headers에 Authorization을 추가하여 API 키를 전달합니다.

Bearer 토큰 방식은 많은 API에서 사용하는 표준입니다. Content-Type도 명시하여 JSON 데이터를 보낸다는 것을 알립니다.

요청을 보낸 후 response.status_code로 결과를 판단합니다. 200이면 성공이므로 데이터를 반환하고, 401이면 인증 실패이므로 명확한 에러 메시지를 줍니다.

404는 데이터를 찾을 수 없다는 의미이고, 그 외의 코드는 일반적인 API 오류로 처리합니다. 이렇게 HTTP 상태 코드별로 다르게 처리하면 LLM이 문제를 정확히 파악하고 사용자에게 적절한 안내를 할 수 있습니다.

예를 들어 401이 나오면 "API 키를 확인해주세요"라고 말할 수 있고, 404가 나오면 "해당 고객을 찾을 수 없습니다"라고 말할 수 있습니다. 실제 현업에서는 어떻게 활용할까요?

예를 들어 고객 지원 부서에서 사용하는 챗봇을 만든다고 가정해봅시다. 상담원이 "고객 12345의 최근 주문을 보여줘"라고 입력하면, 에이전트가 CRM API를 호출하여 주문 목록을 가져옵니다.

또한 "이 고객에게 환불 티켓을 생성해줘"라고 하면, create_ticket API를 호출하여 자동으로 티켓을 만듭니다. 이렇게 하면 상담원이 여러 시스템을 오가지 않고 채팅만으로 업무를 처리할 수 있습니다.

실제로 많은 기업에서 이런 패턴으로 업무 효율을 크게 높이고 있습니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 API 키를 코드에 직접 쓰는 것입니다. 코드가 GitHub에 올라가면 누구나 볼 수 있어서 보안 사고로 이어질 수 있습니다.

따라서 반드시 환경 변수나 비밀 관리 시스템을 사용해야 합니다. 또 하나 중요한 점은 Rate Limiting입니다.

많은 API는 분당 요청 횟수를 제한합니다. 에이전트가 같은 API를 짧은 시간에 여러 번 호출하면 차단당할 수 있으므로, 요청 횟수를 추적하고 제한하는 로직을 추가해야 합니다.

마지막으로 민감한 데이터를 다룰 때는 로그에 주의해야 합니다. 고객의 개인정보나 결제 정보가 로그에 남으면 안 되므로, 로깅 시 마스킹 처리를 해야 합니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. CRM API를 성공적으로 연결한 김개발 씨는 실제 업무에 적용해봤습니다.

고객 지원팀에서 "이거 정말 편하네요!"라는 반응이 나왔습니다. 기존에는 CRM 시스템에 로그인하고, 고객을 검색하고, 주문을 확인하는 데 여러 단계가 필요했지만, 이제는 채팅으로 한 번에 처리되었습니다.

API 통합 에이전트를 제대로 구현하면 실제 비즈니스 프로세스를 자동화하고 생산성을 크게 높일 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - API 키는 절대 코드에 하드코딩하지 말고 환경 변수로 관리하세요

  • Rate Limiting을 고려하여 요청 횟수를 제한하세요
  • 민감한 데이터는 로그에 남기지 않도록 마스킹하세요
  • HTTP 상태 코드별로 명확한 에러 메시지를 제공하세요

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#Python#LLM#Agent#Tool#API#LLM,도구,에이전트

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.