도구 호출 (Tool Calling)을 사용하는 또 다른 방법: 의도 분류 (Intent Classification)
요약
LLM의 도구 호출(Tool Calling) 기능을 단순한 함수 실행이 아닌, 구조화된 데이터를 반환하는 의도 분류(Intent Classification) 용도로 활용하는 방법을 제안합니다. 일반적인 텍스트 분류 방식의 불확실한 출력 문제를 해결하기 위해, 각 의도를 도구 스키마로 정의하여 프로덕션 환경에서 신뢰할 수 있는 분류 시스템을 구축하는 전략을 다룹니다.
핵심 포인트
- 일반적인 텍스트 기반 의도 분류는 출력 형식이 불규칙하여 파싱 오류가 발생하기 쉽습니다.
- 각 의도를 하나의 도구(Tool)로 정의하면 LLM이 반드시 정해진 JSON 스키마를 따르도록 강제할 수 있습니다.
- 도구의 함수 본문은 비워두고 시그니처와 독스트링만을 활용하여 LLM의 가이드로 사용합니다.
- 이 방식은 LLM이 직접 코드를 실행하는 것이 아니라, 구조화된 데이터를 반환하면 개발자의 코드가 라우팅과 실행을 담당하는 구조입니다.
도구 호출 (Tool calling)은 겉보기에는 단순해 보이는 LangChain 기능 중 하나입니다. LLM에 함수를 제공하면, LLM이 언제 호출할지 결정하고 결과를 반환하는 방식이죠. 대부분의 튜토리얼은 여기서 멈춥니다. 하지만 이를 사용하는 더 강력한 방법이 있습니다. LLM이 어떤 함수도 직접 호출하지 않는 방법 말입니다. 제가 Instagram 판매자를 위해 만든 AI SaaS인 IhuSale에서는 도구 호출 (Tool calling)을 데이터를 가져오는 용도가 아니라, 의도 분류 (Intent classification)를 위해 사용합니다. LLM은 스키마 (Schema)를 읽고, 도구를 선택하며, 구조화된 데이터 (Structured data)를 반환합니다. 라우팅 (Routing)과 실행은 제 코드가 담당합니다. 그 결과, 프로덕션 환경에서 실행할 수 있을 만큼 신뢰할 수 있는 분류 시스템이 만들어졌습니다. 작동 방식은 다음과 같습니다.
일반 텍스트 분류의 문제점
의도 분류 (Intent classification)에 대한 순진한 접근 방식은 LLM에게 질문을 던지고 답변을 읽는 것입니다:
response = llm.invoke("What is the intent of this message: 'I want to buy rice'")
반환값: "order", "ORDER", "place_order", "purchase intent", ...
LLM은 메시지를 이해할 만큼 충분히 똑똑합니다. 문제는 출력값입니다. 예측할 수 없는 문자열들 — 서로 다른 대소문자, 서로 다른 표현 방식, 예상치 못한 예외 상황들이 발생합니다. 이제 당신은 해석기(Interpreter)를 해석하기 위해 깨지기 쉬운 문자열 매칭 로직을 작성해야 합니다.
도구 호출 (Tool calling)은 이 문제를 깔끔하게 해결합니다.
핵심 통찰: 의도를 도구로 정의하기
LLM에게 "의도가 무엇인가요?"라고 묻는 대신, 각 의도를 스키마 (Schema)를 가진 하나의 도구 (Tool)로 정의합니다. LLM은 반드시 하나를 선택하고 해당 필드를 채워야만 합니다. 잘못된 형식을 반환할 수 없습니다. 구조 자체가 곧 출력값이기 때문입니다.
from langchain_core.tools import tool
@tool
def handle_order ( product_name : str , quantity : int ) -> dict :
""" 고객이 제품 주문을 하고 싶어 합니다. """
pass
@tool
def handle_inquiry ( question : str ) -> dict :
""" 고객이 제품에 대해 일반적인 질문을 하고 있습니다. """
pass
@tool
def handle_complaint ( issue : str ) -> dict :
""" 고객이 주문 관련 문제를 보고하고 있습니다. """
pass
@tool
def handle_greeting ( message : str ) -> dict :
""" 고객이 인사를 하거나 대화를 시작하고 있습니다. """
""" pass
함수의 본문(function bodies)은 비어 있습니다. LLM은 이를 실행하지 않습니다. LLM은 오직 함수 시그니처(signatures)와 독스트링(docstrings)만을 읽으며, LangChain은 이를 JSON 스키마 (JSON schema)로 직렬화하여 API 요청과 함께 전송합니다. 이 도구(tool)는 두 가지 목적을 수행합니다: LLM을 위한 스키마 가이드 역할, 그리고 응답에서 추출할 tool_name 및 tool_args의 소스 역할입니다.
파이프라인: 모델 (model) → 도구 (tools) → 분류기 (classifier) → 핸들러 (handler)
이렇게 생각해보세요. 당신은 관리자입니다. 당신은 똑똑한 비서인 LLM을 고용하고, 네 가지 의도(intents)와 설명이 적힌 종이 한 장을 건네줍니다. 고객 메시지가 들어오면, 비서는 이를 읽고 적절한 바구니를 선택한 뒤 관련 세부 정보를 채웁니다. 그리고 그 종이를 당신에게 다시 돌려줍니다. 그러면 당신이 코드를 실행하는 것입니다.
실제로 이는 네 가지 구성 요소로 이루어집니다:
모델 (Models) — 의도별 데이터의 형태를 정의합니다. 두 가지 종류가 있습니다: 어떤 필드가 어떤 의도에 속하는지 설명하는 의도 모델 (intent models), 그리고 API 레이어를 위한 요청/응답 모델 (request/response models)입니다.
도구 (Tools) — LLM에게 어떤 의도들이 존재하는지 가르칩니다. llm.bind_tools(ALL_TOOLS)를 호출하면, LangChain은 해당 함수 시그니처와 독스트링을 JSON 스키마로 직렬화하여 모든 API 요청 시 모델로 전송합니다.
분류기 (Classifier) — 프롬프트 (prompt)와 LLM을 체인 (chain)으로 연결하고, 고객 메시지와 함께 호출하여 응답을 읽습니다. 응답은 tool_calls 필드와 함께 돌아옵니다. 이는 LLM이 "나는 handle_order를 선택했으며, 인자(args)는 다음과 같습니다: {product_name: 'rice', quantity: 2} 입니다."라고 말하는 것과 같습니다. 분류기는 tool_name과 tool_args를 추출합니다. 분류기는 아무것도 실행하지 않습니다.
ALL_TOOLS = [ handle_order, handle_inquiry, handle_complaint, handle_greeting ]
llm = ChatAnthropic ( model = "claude-3-5-sonnet-20241022" )
classifier_llm = llm.bind_tools(ALL_TOOLS, tool_choice = "any")
prompt = ChatPromptTemplate.from_messages([
("system", "고객 메시지를 분류하고 관련 데이터를 추출하세요."),
("human", "{message}")
])
chain = prompt | classifier_llm
response = chain.invoke({"message": "I want to buy 2 bags of rice"})
tool_name = response.
tool_calls [ 0 ][ " name " ] tool_args = response . tool_calls [ 0 ][ " args " ] # tool_name = "handle_order" # tool_args = {"product_name": "rice", "quantity": 2} tool_choice="any"는 이 방식이 작동하게 만드는 제약 조건입니다. 이는 LLM이 항상 도구 (tool)를 선택하도록 강제하며, 일반 텍스트 응답을 반환할 수 없게 만듭니다. 이 설정이 없다면, 모델은 의도를 분류하는 대신 가끔 질문에 그냥 답변만 할 수도 있습니다.
핸들러 (Handler) — 의도(intent)당 하나의 함수.
분류기 (classifier)는 tool_args를 일치하는 핸들러로 전달하고, 핸들러는 의도가 요구하는 바에 따라 최종 응답(제안된 답변, 에스컬레이션 플래그 등)을 생성합니다.
handlers = {
" handle_order " : handle_order_handler ,
" handle_inquiry " : handle_inquiry_handler ,
" handle_complaint " : handle_complaint_handler ,
" handle_greeting " : handle_greeting_handler ,
}
handler = handlers [ tool_name ]
result = handler ( tool_args )
5단계로 이루어진 전체 흐름:
- 고객 메시지 수신
- LLM에게 질문: 이 메시지는 어떤 범주 (bucket)에 속하는가?
- LLM의 답변: "범주 3 — handle_order — 추출된 데이터는 다음과 같습니다"
- 범주 3에 대한 핸들러 실행
- 응답 반환
LLM은 결코 어떤 Python 함수도 직접 실행하지 않습니다. LLM은 스키마 (schema)를 읽고, 결정을 내리며, 구조화된 JSON 객체를 반환할 뿐입니다. 라우팅 (routing)과 실행은 여러분의 코드가 담당합니다.
이 패턴이 실제 서비스 (production)에서 사용되는 이유는 도구 호출 (tool calling)이 화려해서가 아니라, LLM을 예측 가능하게 (predictable) 만들기 때문입니다. 그리고 예측 가능성은 핸들러로 라우팅하거나, LangGraph에서 그래프를 구축하거나, Instagram DM을 대규모로 처리할 때 반드시 필요한 요소입니다.
다음: LangGraph — AI가 여러 메시지에 걸쳐 중단된 지점을 기억해야 할 때 어떤 일이 일어나는가.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기