실무 Multi-Agent 오케스트레이션 5장 · Function Calling 원리 5 / 7 ← 이전목차다음 → TechLead Cro

5장. Function Calling 원리

따라하기 — 첫 도구 왕복

출력 안내: 실행 코드는 답변·도구 결과·청크 본문을 글자 수로 자르지 않고 출력합니다. 아래의 기존 실행 예시는 일부 축약된 기록이며, 실제 실행 화면에서 전체 내용을 확인하세요.

목표

같은 질문을 도구 없이 한 번, 도구와 함께 한 번 보냅니다. 도구와 함께 보낼 때는 자동 실행을 끄고 결정 → 실행 → 반환을 한 단계씩 출력해, 모델이 한 일과 우리 코드가 한 일의 경계를 눈으로 확인합니다.


0. 실습 준비

VS Code에서 haru-market 폴더를 열고 터미널에서 환경을 켭니다.

$ conda activate myenv

이번 장은 새로 설치할 것이 없습니다.


1. 파일 만들기

haru-market 폴더 맨 위에 새 파일을 만듭니다.

lesson05_function_calling.py

아래 코드 전체를 복사해 붙여 넣고 저장합니다.

# -*- coding: utf-8 -*-
"""[5장] Function Calling 기초 — 모델은 '결정', 실행은 '코드'

문제 상황: "주문번호 HR20260701023 어디까지 왔나요?"
  → 모델은 하루마켓 DB를 볼 수 없다. 모르면서 그럴듯하게 지어낸다(환각).

해결: 모델에게 '도구 목록'을 알려주면, 모델은 함수를 직접 실행하는 게 아니라
  "이 함수를 이 인자로 불러라"라는 **결정(function call)**을 응답으로 보낸다.
  실행은 우리 코드가 하고, 결과를 다시 모델에게 돌려주면 최종 답변이 나온다.

이 왕복(round-trip)을 눈으로 확인하는 것이 오늘의 목표.

실행:  python lesson05_function_calling.py
"""
from google.genai import types

from config import MODEL, get_client

client = get_client()

# ── 0. 먼저 도구 없이 물어보기 — 환각 확인 ────────────────────────────
question = "주문번호 HR20260701023 지금 어디까지 왔나요?"
print("■ [실험 1] 도구 없이 질문")
r = client.models.generate_content(model=MODEL, contents=question)
print(r.text)
print("→ 데이터가 없으니 일반론을 말하거나 지어낸다.\n")


# ── 1. 도구를 '선언'한다 (아직 가짜 구현) ─────────────────────────────
# 도구 선언 = 함수 이름 + 설명 + 인자 스키마.
# 모델은 이 '설명'만 보고 도구를 고른다. 설명이 곧 라우팅 품질이다.
get_order_status_decl = types.FunctionDeclaration(
    name="get_order_status",
    description="하루마켓 주문번호로 주문의 현재 상태(결제/배송준비/배송중/배송완료 등)와 "
                "택배사·송장번호를 조회한다.",
    parameters_json_schema={
        "type": "object",
        "properties": {
            "order_id": {
                "type": "string",
                "description": "하루마켓 주문번호. 'HR'로 시작하는 문자열",
            }
        },
        "required": ["order_id"],
    },
)
tool = types.Tool(function_declarations=[get_order_status_decl])


def get_order_status(order_id: str) -> dict:
    """실제 실행되는 파이썬 함수 (오늘은 하드코딩, 6장에서 CSV 연동)."""
    return {
        "order_id": order_id,
        "status": "배송중",
        "courier": "CJ대한통운",
        "tracking_no": "6889 1234 5678",
        "eta": "내일 도착 예정",
    }


# ── 2. 도구와 함께 질문 → 모델의 '결정'을 받는다 ──────────────────────
print("■ [실험 2] 도구 선언과 함께 질문")
user_content = types.Content(role="user", parts=[types.Part.from_text(text=question)])

response = client.models.generate_content(
    model=MODEL,
    contents=[user_content],
    config=types.GenerateContentConfig(
        tools=[tool],
        # 오늘은 왕복 과정을 직접 보려고 자동 실행을 끈다
        automatic_function_calling=types.AutomaticFunctionCallingConfig(disable=True),
    ),
)

fc = response.function_calls[0]  # 모델의 결정
print(f"모델의 결정: {fc.name}({dict(fc.args)})")
print("→ 모델은 함수를 실행하지 않았다. '불러 달라'고 요청했을 뿐이다.\n")

# ── 3. 코드가 함수를 실행하고, 결과를 모델에게 돌려준다 ───────────────
print("■ [실험 3] 함수 실행 결과를 돌려주고 최종 답변 받기")
result = get_order_status(**fc.args)                 # ← 실행 주체는 코드
print(f"함수 실행 결과: {result}")

function_response_part = types.Part.from_function_response(
    name=fc.name,
    response={"result": result},
)
final = client.models.generate_content(
    model=MODEL,
    contents=[
        user_content,                          # 원래 질문
        response.candidates[0].content,        # 모델의 function call
        types.Content(role="user", parts=[function_response_part]),  # 실행 결과
    ],
    config=types.GenerateContentConfig(tools=[tool]),
)
print(f"\n최종 답변:\n{final.text}")

print("""
──────────────────────────────────────────────────────────
핵심 정리
  1) 도구 선언(이름·설명·인자 스키마)을 모델에게 준다
  2) 모델: "get_order_status(order_id=...)를 불러라" ← 결정만
  3) 코드: 함수 실행 → 결과를 function_response 로 반환 (role='user')
  4) 모델: 결과를 바탕으로 자연어 최종 답변
다음 장: 가짜 구현을 data/orders.csv 실제 조회로 교체한다.
──────────────────────────────────────────────────────────""")

2. 코드에서 볼 곳

파일은 세 번의 실험으로 이루어져 있습니다. 질문은 세 번 모두 같습니다.

실험 무엇을 하나 모델 호출 볼 것
1 도구 없이 질문 1번 모델이 무엇이라고 답하는가
2 도구 선언과 함께 질문 1번 응답이 문장인가, function call인가
3 함수를 실행해 결과를 돌려줌 1번 답변에 어떤 값이 들어 있는가

실험 2와 3 사이의 이 한 줄이 이번 장의 중심입니다.

result = get_order_status(**fc.args)                 # ← 실행 주체는 코드

이 줄의 위쪽은 모델이 한 일(무엇을 부를지 결정)이고, 이 줄은 우리 코드가 한 일(실행)입니다.

그리고 실험 3의 contents에 세 덩어리가 차례로 들어가는 것을 봅니다.

contents=[
    user_content,                          # 원래 질문
    response.candidates[0].content,        # 모델의 function call
    types.Content(role="user", parts=[function_response_part]),  # 실행 결과
],

3. 실행하기

실행하기 전에 짐작해 보세요.

  • 실험 2에서 모델의 응답은 문장일까요, 다른 무엇일까요?
  • 실험 3의 답변에 "내일 도착 예정" 이라는 말이 나올까요? 나온다면 그 말은 어디서 온 것일까요?
$ python lesson05_function_calling.py

실행 결과 (문장은 실행할 때마다 달라집니다)

■ [실험 1] 도구 없이 질문
죄송합니다만, 저는 AI 어시스턴트로서 고객님의 **개인 주문 정보나 쇼핑몰의 내부 데이터베이스에 접근할 권한이 없습니다.** 

따라서 주문번호 **HR20260701023**의 실시간 배송 및 처리 현황을 직접 조회해 드리지 못하는 점 양해 부탁드립니다.

현재 주문하신 상품의 상태를 확인하시려면 다음 방법 중 하나를 이용해 주세요.

1. **구매하신
→ 데이터가 없으니 일반론을 말하거나 지어낸다.

■ [실험 2] 도구 선언과 함께 질문
모델의 결정: get_order_status({'order_id': 'HR20260701023'})
→ 모델은 함수를 실행하지 않았다. '불러 달라'고 요청했을 뿐이다.

■ [실험 3] 함수 실행 결과를 돌려주고 최종 답변 받기
함수 실행 결과: {'order_id': 'HR20260701023', 'status': '배송중', 'courier': 'CJ대한통운', 'tracking_no': '6889 1234 5678', 'eta': '내일 도착 예정'}

최종 답변:
주문번호 **HR20260701023** 상품은 현재 **배송중**입니다.

* **택배사:** CJ대한통운
* **송장번호:** 6889 1234 5678
* **예상 도착일:** 내일 도착 예정

(이하 생략)

위 예시는 이전 코드의 축약 출력 기록입니다. 현재 코드는 답변 전체를 출력하므로 실행 화면에서 끝까지 확인합니다.


4. 무엇을 관찰했나

실험 1 — 도구가 없으면 조회할 수 없다

모델은 "접근할 권한이 없다" 고 답했습니다. 모델 안에는 이 주문의 정보가 없기 때문입니다. 이 답을 실험 3의 답과 비교하는 것이 이번 장의 요점입니다.

실험 2 — 문장 대신 function call이 왔다

같은 질문에 도구 선언을 함께 보냈더니 응답이 달라졌습니다.

모델의 결정: get_order_status({'order_id': 'HR20260701023'})
  • 모델은 도구가 필요하다고 판단했습니다.
  • 문장 속에서 주문번호를 찾아 order_id라는 이름의 인자에 넣었습니다. 선언에 적은 인자 이름 그대로입니다.
  • 그리고 멈췄습니다. 이 시점에 get_order_status 함수는 아직 한 번도 실행되지 않았습니다.

실험 3 — 답변의 내용은 전부 함수가 준 값이다

최종 답변의 택배사, 송장번호, 도착 예정은 함수 실행 결과에 찍힌 값과 글자까지 같습니다. 모델은 받은 데이터를 문장으로 풀어 썼을 뿐, 새로 보탠 사실이 없습니다.

짐작해 본 질문의 답도 여기 있습니다. "내일 도착 예정"은 모델이 추측한 것이 아니라 가짜 함수의 "eta": "내일 도착 예정"에서 왔습니다.

답의 정확도는 도구가 정한다

실험 1과 3을 비교하면, 모델이 똑똑해진 것은 없습니다. 바뀐 것은 모델의 손이 닿는 곳에 데이터를 놓아 준 것 하나입니다.

답변의 정확도는 모델이 아니라 도구가 돌려준 데이터의 정확도가 정합니다.

이번 장의 도구는 구조를 보려고 만든 가짜여서 어떤 주문번호에도 같은 값을 돌려줍니다. 6장에서 이 자리를 data/orders.csv를 읽는 진짜 조회로 바꿉니다. 말투를 상담원답게 만드는 시스템 프롬프트(3장)도 6장에서 도구와 함께 씁니다.


5. 지금 폴더의 모습

haru-market/
├── config.py
├── lesson02_first_call.py
├── lesson03_prompt.py
├── lesson04_intent_classifier.py
└── lesson05_function_calling.py    ← 이번 장

핵심 정리

  • 도구 없이 물으면 모델은 조회할 수 없다고 답합니다. 모델 안에는 이 주문의 정보가 없습니다.
  • 도구 선언을 함께 보내면 응답으로 문장이 아니라 function call이 옵니다.
  • 모델은 무엇을 부를지 결정했고, 실행한 것은 우리 코드입니다.
  • 최종 답변의 사실은 전부 함수의 반환값에서 왔습니다.
  • 답변의 정확도는 도구가 돌려준 데이터가 정합니다. 그래서 다음 일은 도구를 진짜 데이터에 연결하는 것입니다.
← 이전 절왕복을 코드로 옮기기 — 호출 두 번, 대화 세 덩어리다음 절 →정리와 체크리스트
오명운 · macro@prag-ai.com