LLM Function Calling이란?
유데미 강의에서 칸반 AI 챗봇에게 "카드 옮겨줘"라고 했더니 실제로 카드가 이동했다. 어떻게 동작하는 걸까?

들어가며
AI 챗봇에게 "gather customer signals 카드를 Done으로 옮겨줘"라고 입력했더니, 정말로 칸반 보드의 카드가 이동했다.
단순한 채팅 응답이 아니라 실제 데이터베이스가 변경된 것이다.
이게 가능한 이유가 바로 Function Calling(Tool Use) 이다.
핵심 개념: LLM은 직접 실행하지 않는다
가장 먼저 이해해야 할 것은, Claude(또는 GPT)가 직접 DB를 건드리지 않는다는 점이다.
LLM이 하는 일은 딱 하나다:
"나는 이 함수를, 이 인자로 호출하고 싶다"는 의도를 JSON으로 반환하는 것
실제 실행은 백엔드 서버가 한다.
전체 흐름
사용자: "gather customer signals를 Done으로 옮겨줘"
↓
백엔드: 메시지 + 사용 가능한 tools 목록 → Claude API 전송
↓
Claude: tool_use 블록 반환 (move_card 함수 호출 의도 + 인자)
↓
백엔드: tool_use 블록 파싱 → 실제 DB UPDATE 실행
↓
백엔드: 실행 결과 → Claude API 재전송 (tool_result)
↓
Claude: "카드를 Done으로 이동했습니다!" (자연어 최종 응답)
↓
사용자: 응답 수신
단계별 코드로 보기
1단계: 백엔드가 tools 목록과 함께 메시지 전송
일반 API 호출과의 차이는 tools 파라미터가 추가된다는 것이다.
import anthropic
import json
client = anthropic.Anthropic(api_key="...")
tools = [
{
"name": "move_card",
"description": "칸반 카드를 다른 컬럼으로 이동시킵니다",
"input_schema": {
"type": "object",
"properties": {
"card_title": {
"type": "string",
"description": "이동할 카드 제목"
},
"target_column": {
"type": "string",
"enum": ["backlog", "in_progress", "review", "done"]
}
},
"required": ["card_title", "target_column"]
}
}
]
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1000,
tools=tools, # ← 핵심
system=f"현재 보드 상태: {json.dumps(board_state)}",
messages=[{"role": "user", "content": user_message}]
)
2단계: Claude가 반환하는 응답 구조
"카드 옮겨줘"라는 요청에 Claude는 텍스트가 아닌 아래 구조를 반환한다.
# response.content
[
ContentBlock(
type="tool_use", # 텍스트 응답이 아님!
id="toolu_01abc...",
name="move_card",
input={
"card_title": "gather customer signals",
"target_column": "done"
}
)
]
3단계: 백엔드가 실행하고 결과를 Claude에게 재전달
async def handle_response(response, user_message, board_state):
if response.stop_reason == "tool_use":
tool_block = next(b for b in response.content if b.type == "tool_use")
# 실제 DB 업데이트
if tool_block.name == "move_card":
result = move_card_in_db(
card_title=tool_block.input["card_title"],
target_column=tool_block.input["target_column"]
)
# 결과를 Claude에 다시 전달 → 자연어 응답 생성
final_response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1000,
tools=tools,
messages=[
{"role": "user", "content": user_message},
{"role": "assistant", "content": response.content},
{
"role": "user",
"content": [{
"type": "tool_result",
"tool_use_id": tool_block.id,
"content": json.dumps(result)
}]
}
]
)
return final_response.content[0].text
# → "gather customer signals 카드를 Done으로 이동했습니다!"
드래그앤드롭 방식과 뭐가 다른가
자연스러운 의문이다. 그냥 드래그앤드롭 이벤트마다 REST API 호출하면 되는 거 아닌가?
맞다. 오히려 그게 더 단순하다.
| 방식 | 의도 파악 주체 | 적합한 케이스 |
|---|---|---|
| 드래그앤드롭 | 프론트엔드 (UI가 이미 앎) | 명확한 단일 액션 |
| Function Calling | LLM (자연어 해석) | 자연어에서 의도 추출 |
핵심 차이는 "의도 파악의 주체" 다.
드래그앤드롭은 사용자가 카드를 Done 컬럼에 갖다놓는 순간, 프론트엔드가 이미 {card_id, target_column: "done"}을 알고 있다. LLM이 끼어들 여지가 없다.
반면 "gather customer signals 옮겨줘"라는 자연어에는 어떤 카드인지, 어디로인지가 사용자 문장 안에 들어있다. 이걸 파싱해서 구조화된 액션으로 변환하는 것이 LLM의 역할이다.
여기서 드는 의문: LLM이 엉뚱한 tool을 선택하면?
실제로 일어나는 문제다. 프로덕션에서는 반드시 방어 레이어가 필요하다.
1단계: 프롬프트로 행동 제한
system = """
당신은 칸반 보드 어시스턴트입니다.
카드 이동 요청이 명확하지 않으면 반드시 사용자에게 확인을 요청하세요.
절대 추측으로 tool을 실행하지 마세요.
"""
2단계: 백엔드에서 실행 전 검증
if tool_block.name == "move_card":
card = db.query(Card).filter(Card.title == card_title).first()
if not card:
return "해당 카드를 찾을 수 없습니다. 정확한 카드 이름을 알려주세요."
3단계: 위험도에 따라 tool 분리
# 읽기 전용 → 자동 실행 OK
{"name": "get_board_state", ...}
{"name": "search_cards", ...}
# 상태 변경 → 확인 후 실행
{"name": "move_card", ...}
# 파괴적 작업 → 별도 권한 체크
{"name": "delete_card", ...}
정리
- LLM Function Calling의 핵심은 "LLM이 어떤 함수를 호출할지 판단하는 것" 이고, 실제 실행은 백엔드가 담당한다
- 드래그앤드롭과 Function Calling은 대체 관계가 아니라 진입 경로가 다른 것이다
- 프로덕션에서는 LLM의 판단을 그대로 신뢰하지 말고, 백엔드에서 반드시 검증 레이어를 추가해야 한다
Udemy "AI Coder: Vibe Coder to Agentic Engineer" 강의를 수강하며 정리한 내용입니다.
'[IT] 질문과 답변 > 생성형AI' 카테고리의 다른 글
| [IT Insight] 구글, 카카오가 '소셜 로그인'을 무료로 퍼주는 진짜 이유 (OAuth의 비즈니스 모델) (1) | 2026.03.28 |
|---|