실무 Multi-Agent 오케스트레이션 6장 · 데이터 조회 도구 구현 7 / 7 ← 이전목차다음 → TechLead Cro

6장. 데이터 조회 도구 구현

실습문제와 해답

도구에 다른 질문을 해 보고, 기능을 넓히고, 내부 동작을 살펴봅니다. 먼저 스스로 해 본 뒤 해답을 펼쳐 보세요. 해답은 새 파일로 만들어 실행합니다.


문제 1 — 질문을 바꿔 어떤 도구가 불리는지 확인하기

lesson06_order_tools.py의 ask()를 가져와, 다음 세 질문에서 어떤 도구가 불리는지 확인하는 파일 ex06_which_tool.py를 만드세요.

  1. "주문번호 HR20260620069 송장번호 알려주세요."
  2. "주문번호 HR20260620069 언제 주문한 거였죠?"
  3. "주문번호 HR20260620069 상품이 뭐였는지, 그리고 지금 어디쯤인지 알려주세요."

실행하기 전에 짐작해 보세요. 3번에서는 도구가 몇 번 불릴까요?

  • 힌트: from lesson06_order_tools import ask로 가져오면 if __name__ == "__main__": 아래의 시연은 실행되지 않습니다.
해답 보기
from lesson06_order_tools import ask

questions = [
    "주문번호 HR20260620069 송장번호 알려주세요.",
    "주문번호 HR20260620069 언제 주문한 거였죠?",
    "주문번호 HR20260620069 상품이 뭐였는지, 그리고 지금 어디쯤인지 알려주세요.",
]
for q in questions:
    ask(q)
$ python ex06_which_tool.py

실행 결과 (문장은 실행마다 다릅니다)

고객: 주문번호 HR20260620069 송장번호 알려주세요.
  [도구 호출] track_shipping({'order_id': 'HR20260620069'})
하루: 요청하신 주문번호 HR20260620069의 택배사는 우체국택배이며, 송장번호는 83455521031입니다. 현재 2026년 6월 21일에 출고되어 배송 중에 있습니다. 더 궁금하신 점이 있으시면 언제든지 말씀해 주세요.

고객: 주문번호 HR20260620069 언제 주문한 거였죠?
  [도구 호출] get_order_status({'order_id': 'HR20260620069'})
하루: 고객님의 주문번호 `HR20260620069`는 2026년 6월 20일 오전 11시 12분에 접수되었습니다. 해당 주문 건은 현재 '콜드브루 원액 500ml 2병' 상품으로 정상 확인되며, 현재 배송 중인 상태입니다. 배송 위치나 택배사 정보가 궁금하시면 추가로 안내해 드리겠습니다.

고객: 주문번호 HR20260620069 상품이 뭐였는지, 그리고 지금 어디쯤인지 알려주세요.
  [도구 호출] get_order_status({'order_id': 'HR20260620069'})
  [도구 호출] track_shipping({'order_id': 'HR20260620069'})
하루: 주문하신 상품은 '콜드브루 원액 500ml 2병'이며, 현재 우체국택배(송장번호: 83455521031)를 통해 배송 중입니다. 6월 21일에 출고되어 이동하고 있으나, 상세한 실시간 위치나 정확한 도착 예정 시간은 확인이 어려운 점 양해 부탁드립니다. 추가로 궁금하신 점이 있으시면 언제든지 말씀해 주세요.

해설 — 같은 주문번호인데 질문에 따라 불린 도구가 달랐습니다. 이 교재를 준비하며 세 번 실행했을 때 도구 호출은 세 번 모두 같았습니다.

  • 1번은 송장번호를 물었고 track_shipping이 불렸습니다.
  • 2번은 주문한 때를 물었고 get_order_status가 불렸습니다. 답의 "6월 20일 오전 11시 12분"은 ordered_at의 2026-06-20 11:12입니다.
  • 3번은 둘 다 물었고 도구가 두 번 불렸습니다. 자동 방식은 필요한 만큼 왕복을 이어 가고, 그 횟수의 상한이 maximum_remote_calls=4입니다.

고르는 근거는 두 함수의 독스트링뿐입니다. "주문 상태를 조회한다"와 "택배사·송장번호·출고일·배송완료일을 조회한다. 배송 위치 문의에 사용한다"의 차이가 이 결과를 만들었습니다.

3번의 답에서 "정확한 도착 예정 시간은 확인이 어려운 점"도 봅니다. orders.csv에는 도착 예정 시간이 없고, SYSTEM에 "조회 결과에 없는 내용(도착 예정 시간 등)은 추측하지 않습니다"라고 적어 두었습니다. 모델은 데이터에 있는 것만 전하고 없는 것은 없다고 말했습니다.


문제 2 — 조회하기 전에 코드가 먼저 검사하기

고객이 "주문번호 12345 조회해 주세요."라고 하면, 지금의 도구는 CSV를 뒤진 뒤 "찾을 수 없습니다"를 돌려줍니다. 하루마켓 주문번호는 반드시 HR로 시작하므로, 형식이 틀린 번호는 조회하기 전에 걸러 더 정확한 안내를 줄 수 있습니다.

get_order_status를 새로 정의한 ex06_validate.py를 만드세요.

  • order_id가 HR로 시작하지 않으면 {"found": False, "message": "주문번호 형식이 올바르지 않습니다. HR로 시작하는 번호를 확인해 주세요."}를 돌려줍니다.
  • 함수가 불릴 때마다 어떤 인자로 불렸는지 출력합니다.
  • "주문번호 12345 조회해 주세요."와 "주문번호 HR20260620069 조회해 주세요." 두 질문으로 시험합니다.
해답 보기
from google.genai import types

from config import MODEL, get_client
from lesson06_order_tools import ORDERS, SYSTEM

client = get_client()


def get_order_status(order_id: str) -> dict:
    """하루마켓 주문번호로 주문 상태를 조회한다.

    Args:
        order_id: 하루마켓 주문번호. 'HR'로 시작한다. 예: HR20260701023
    """
    print(f"  [도구 실행] get_order_status(order_id={order_id!r})")
    if not order_id.startswith("HR"):                 # 조회하기 전에 코드가 형식을 검사
        return {"found": False,
                "message": "주문번호 형식이 올바르지 않습니다. HR로 시작하는 번호를 확인해 주세요."}
    row = ORDERS[ORDERS["order_id"] == order_id]
    if row.empty:
        return {"found": False, "message": f"주문번호 {order_id} 를 찾을 수 없습니다."}
    r = row.iloc[0]
    return {"found": True, "order_id": r["order_id"], "product_name": r["product_name"],
            "status": r["order_status"]}


for q in ["주문번호 12345 조회해 주세요.", "주문번호 HR20260620069 조회해 주세요."]:
    print(f"\n고객: {q}")
    response = client.models.generate_content(
        model=MODEL,
        contents=q,
        config=types.GenerateContentConfig(
            system_instruction=SYSTEM,
            temperature=1.0,
            tools=[get_order_status],
            automatic_function_calling=types.AutomaticFunctionCallingConfig(maximum_remote_calls=4),
        ),
    )
    print(f"하루: {response.text}")
$ python ex06_validate.py

실행 결과 (문장은 실행마다 다릅니다)

고객: 주문번호 12345 조회해 주세요.
  [도구 실행] get_order_status(order_id='12345')
하루: 문의하신 주문번호 12345는 'HR'로 시작하는 올바른 하루마켓 주문번호 형식이 아닌 것으로 확인됩니다. 하루마켓 주문번호는 영문 'HR'로 시작하는 조합으로 이루어져 있습니다. 정확한 주문번호를 다시 한번 확인하신 후 말씀해 주시면 신속하게 조회해 드리겠습니다.

고객: 주문번호 HR20260620069 조회해 주세요.
  [도구 실행] get_order_status(order_id='HR20260620069')
하루: 고객님의 주문번호 HR20260620069 상품은 '콜드브루 원액 500ml 2병'이며, 현재 '배송중' 상태입니다. 정확한 도착 예정 시간은 확인이 어려운 점 양해 부탁드립니다. 추가로 궁금하신 점이 있으시면 언제든지 말씀해 주세요.

해설 — [도구 실행] 줄을 봅니다. 독스트링에 "'HR'로 시작한다"고 적어 두었는데도 모델은 order_id='12345'로 도구를 불렀습니다. 독스트링은 모델에게 하는 안내이지 강제가 아닙니다. 고객이 준 번호가 그것뿐이니 모델은 그대로 넘겼습니다.

그래서 확실하게 막아야 하는 것은 코드에 둡니다. 형식 검사는 startswith("HR") 한 줄이면 되고, 틀릴 일이 없습니다. 그리고 검사 결과를 found: False와 안내 문장으로 돌려줬더니, 모델의 답이 "찾을 수 없습니다"가 아니라 "HR로 시작하는 형식이 아닙니다" 로 바뀌었습니다. 고객이 무엇을 고쳐야 하는지 알 수 있는 답입니다.

자동 방식에서는 실행 직전에 끼어들 자리가 우리 코드에 없으므로, 검사는 이렇게 도구 함수 안에 넣습니다.

이 파일의 반환값이 원래의 get_order_status보다 작다는 점도 봅니다. 상품명과 상태만 돌려주도록 줄였더니 답변에서 금액과 주문 일시가 빠졌습니다. 도구가 주지 않은 것은 답변에 나오지 않습니다.


문제 3 — SDK가 만든 도구 선언 들여다보기

자동 방식에서 SDK는 함수를 보고 도구 선언을 만듭니다. 그 선언을 직접 출력하는 파일 ex06_declaration.py를 만들고, 5장에서 손으로 쓴 선언과 무엇이 다른지 찾아보세요.

  • 힌트: types.FunctionDeclaration.from_callable_with_api_option(callable=함수, api_option="GEMINI_API")가 함수로부터 선언을 만들어 돌려줍니다.
  • 이 파일은 LLM을 호출하지 않습니다.
해답 보기
import json

from google.genai import types

from lesson06_order_tools import get_order_status, track_shipping

for func in [get_order_status, track_shipping]:
    decl = types.FunctionDeclaration.from_callable_with_api_option(
        callable=func, api_option="GEMINI_API"
    )
    print(json.dumps(decl.model_dump(exclude_none=True), ensure_ascii=False, indent=2, default=str))
    print()
$ python ex06_declaration.py

실행 결과

{
  "description": "하루마켓 주문번호로 주문 상태를 조회한다.\n\nArgs:\n    order_id: 하루마켓 주문번호. 'HR'로 시작한다. 예: HR20260701023",
  "name": "get_order_status",
  "parameters": {
    "properties": {
      "order_id": {
        "type": "STRING"
      }
    },
    "required": [
      "order_id"
    ],
    "type": "OBJECT"
  }
}

(이하 생략)

해설 — 이 파일은 모델을 부르지 않으므로 누가 실행해도 같은 결과가 나옵니다.

항목 5장에서 손으로 쓴 선언 SDK가 만든 선언
name 직접 적었다 함수 이름에서 왔다
description 한 문장을 직접 적었다 독스트링 전체가 들어갔다 (Args: 부분까지)
인자의 타입 "type": "string" 타입힌트 str에서 왔다
인자의 설명 인자 칸 안에 "description"으로 따로 적었다 인자 칸에는 타입만 있다. 설명은 위의 description 글 안에 있다

여기서 알 수 있는 것은 세 가지입니다.

  • 독스트링에 적은 글자는 전부 모델에게 갑니다. 개발자끼리 보려고 적은 메모(예: "나중에 고칠 것")도 독스트링에 있으면 모델이 읽습니다.
  • 인자 설명을 인자 칸에 정확히 붙여 전달하고 싶거나, 허용 값을 열거형으로 묶고 싶을 때는 5장처럼 선언을 직접 쓰는 편이 낫습니다.
  • 타입힌트를 빼면 SDK는 인자의 타입을 알 수 없습니다. 자동 방식에서는 타입힌트도 기능입니다.

"SDK가 알아서 해 준다"는 말은 편리하지만, 무엇을 어떻게 만들어 보내는지 한 번은 출력해서 봐야 도구가 엉뚱하게 불릴 때 어디를 고칠지 알 수 있습니다.

← 이전 절정리와 체크리스트7장 →왜 라우팅을 따져야 하는가 — 도구가 늘면 고르는 일이 생긴다
오명운 · macro@prag-ai.com