28장. Function Calling / Tool Use
이 장의 목표 “모델이 우리 API를 직접 호출” 하는 게 어떻게 가능한지 이해합니다.
그리고 로컬 모델로 그걸 구현하는 첫 코드를 짭니다.
28.1 한 줄로
모델이 텍스트 대신 “이 함수를 이 인자로 부르고 싶어“라는 JSON을 만들고, 우리 코드가 그 함수를 실제로 실행하는 것.
이것 자체가 Agent의 기반입니다.
28.2 흐름
[사용자] "서울 날씨 알려줘"
│
▼
[LLM에 사용 가능한 도구 목록 + 질문 전달]
│
▼
[LLM] "get_weather(city='Seoul') 호출하고 싶음"
│
▼
[우리 코드] 실제 get_weather('Seoul') 실행 → "맑음 22도"
│
▼
[LLM에 결과 전달] "도구가 '맑음 22도'를 반환했음"
│
▼
[LLM] "서울은 현재 맑고 22도입니다."
LLM은 실행자가 아니라 결정자. 실제 실행은 항상 우리 코드가 합니다.
28.3 도구 정의 — JSON Schema
OpenAI 형식.
{
"tools": [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "도시의 현재 날씨를 조회합니다.",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "도시 이름 (예: 'Seoul')"}
},
"required": ["city"]
}
}
}
]
}
핵심:
name: 함수 이름description: LLM이 이걸 보고 호출 여부 결정 → 명확하게!parameters: JSON Schema 형식
28.4 첫 코드 (Python + Ollama)
from openai import OpenAI
import json
client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")
# 1. 실제 함수
def get_weather(city: str) -> str:
fake_db = {"Seoul": "맑음 22도", "Tokyo": "흐림 18도"}
return fake_db.get(city, "정보 없음")
# 2. 도구 정의
tools = [{
"type": "function",
"function": {
"name": "get_weather",
"description": "도시의 현재 날씨를 조회합니다.",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string"}
},
"required": ["city"]
}
}
}]
# 3. 첫 호출 — 도구 사용 의도 결정
messages = [{"role": "user", "content": "서울 날씨 알려줘"}]
resp = client.chat.completions.create(
model="qwen3:32b",
messages=messages,
tools=tools,
)
msg = resp.choices[0].message
messages.append(msg)
# 4. 도구 호출 실행
if msg.tool_calls:
for call in msg.tool_calls:
args = json.loads(call.function.arguments)
result = get_weather(**args)
messages.append({
"role": "tool",
"tool_call_id": call.id,
"content": result,
})
# 5. 결과를 모델에게 다시 — 최종 답변
resp = client.chat.completions.create(
model="qwen3:32b",
messages=messages,
)
print(resp.choices[0].message.content)
28.5 어떤 모델이 도구 호출을 잘하나?
지원 여부와 품질이 모델마다 다릅니다.
잘하는 모델 (2026 기준)
- Qwen 3 시리즈 (특히 32B+)
- Llama 3.3 70B Instruct
- DeepSeek R1 (Thinking 후 호출)
- Mistral Large·Mixtral
약한 모델
- 작은 7~8B 일반 모델 (가끔 형식 깨짐)
- Base 모델
- 일부 한국어 특화 작은 모델
추천: Function Calling은 14B 이상 부터. 가능하면 32B 이상.
28.6 로컬 모델의 흔한 함정
함정 ① 형식이 가끔 깨짐
JSON 따옴표 빠짐, 인자 누락 등.
대처:
- 시스템 프롬프트에 강조
response_format: json_schema지원 모델 우선- 재시도 로직
함정 ② 도구를 너무 자주 / 너무 안 부름
- description을 더 명확히
- system 프롬프트에: “정보가 필요하면 적극적으로 도구를 사용해”
- 또는 반대로: “단순 질문은 도구 없이 답해”
함정 ③ 한국어 인자가 깨짐
- 도시 이름이 “서울” → 모델이 “Seoul“로 바꿔 부름
- description에 명시: “도시 이름은 영어로 (예: ‘Seoul’)” 또는 한국어 그대로 받는 함수로 만들기
28.7 자주 만드는 도구 예시
검색
{
"name": "search_wiki",
"description": "사내 위키에서 키워드로 문서 검색",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string"},
"top_k": {"type": "integer", "default": 5}
},
"required": ["query"]
}
}
파일 읽기
{
"name": "read_file",
"description": "프로젝트 안의 파일을 읽음",
"parameters": {
"type": "object",
"properties": {
"path": {"type": "string"}
},
"required": ["path"]
}
}
코드 실행
{
"name": "run_python",
"description": "샌드박스에서 Python 코드 실행",
"parameters": {
"type": "object",
"properties": {
"code": {"type": "string"}
},
"required": ["code"]
}
}
⚠ run_python, run_shell은 위험. 29장에서.
28.8 다중 호출 — Tool Loop
LLM이 한 번에 끝나지 않을 수 있습니다.
1차: search_wiki("휴가 규정") → 3개 문서 결과
2차: read_file("docs/leave.md") → 문서 내용
3차: 최종 답변
루프 패턴:
while True:
resp = client.chat.completions.create(
model="qwen3:32b",
messages=messages,
tools=tools,
)
msg = resp.choices[0].message
messages.append(msg)
if not msg.tool_calls:
break # 텍스트 답변이 나오면 종료
for call in msg.tool_calls:
result = dispatch(call.function.name, json.loads(call.function.arguments))
messages.append({
"role": "tool",
"tool_call_id": call.id,
"content": str(result),
})
이 루프가 Agent의 출발점입니다 (29장).
28.9 평행 호출 (Parallel Tool Calls)
한 번에 도구 여러 개를 부를 수도 있습니다.
msg.tool_calls # [call1, call2, ...]
call1 과 call2 가 서로 독립이면 병렬 실행 가능.
성능 향상 큼. 단, 모델·도구 지원 여부 확인.
28.10 도구 결과 길이 관리
도구가 큰 결과를 반환하면 LLM 컨텍스트가 폭주합니다.
대처:
- 도구 안에서 결과를 요약·잘라서 반환
- 또는 LLM에 첫 200자만 보여주고 “더 필요하면 다시 호출” 패턴
28.11 Structured Output — 도구 없는 JSON 강제
도구 호출이 아니라 그냥 JSON 답변이 필요할 때도 비슷한 기법을 씁니다.
resp = client.chat.completions.create(
model="qwen3:32b",
messages=[...],
response_format={"type": "json_object"},
)
지원 여부 도구마다 다름. 시스템 프롬프트에 스키마 명시하는 게 가장 호환성 좋음.
이 장에서 기억할 한 가지
LLM은 도구를 부르지 않습니다. “부르고 싶다“는 JSON만 만듭니다.
실제 실행은 항상 우리 코드. 결과를 다시 LLM에 전달 → 최종 답변.
손으로 해볼 것
1. 28.4 코드 그대로 실행
도구 호출이 정상 동작하는지 확인.
qwen3:8b 로도 시도 후, qwen3:32b 와 품질 비교.
2. 도구 한 개 추가
get_time(timezone) 같은 함수를 추가하고
“서울 시간과 도쿄 시간 알려줘” 같은 질문에
두 도구가 호출되는지 확인.
다음 장에서는 Agent의 구조 — 도구 호출 루프의 본격적 형태, 그리고 로컬 모델에 권한을 주는 위험 을 다룹니다.