📚 Agentic AI - 기업용 자율 에이전트 개발 4장 · 에이전트 ② 웹검색 Agentic AI란

따라하기 — 사내 상품 조회 + 웹검색 두 도구를 함께 쥐여 주기

한 줄 요약

TOOLS 딕셔너리로 도구를 묶고, config 에 함께 넘깁니다. 그리고 모델이 도구 두 개를 순서대로 쓰는 모습을 실행 결과로 확인합니다.


1. 도구를 묶는 방법

아래는 code/ch04_web_agent.py 의 일부입니다 — 위에서 이미 붙여 넣었으니 다시 넣지 마세요.

TOOLS = {"get_product_info": get_product_info, "search_web": search_web}

이름 → 함수 딕셔너리입니다. 왜 리스트가 아니라 딕셔너리일까요?

모델이 돌려주는 요청서에는 함수 이름이 문자열로 들어 있습니다.

fc.name = "search_web"        ← 문자열이다

이 문자열로 실제 함수를 찾아야 합니다.

아래는 code/ch04_web_agent.py 의 일부입니다 — 위에서 이미 붙여 넣었으니 다시 넣지 마세요.

result = TOOLS[fc.name](**dict(fc.args))
       └─────────┘
       "search_web" 이라는 문자열로 함수를 꺼낸다

딕셔너리면 TOOLS["search_web"] 한 번에 찾습니다. 리스트였다면 이름으로 뒤져야 하죠.

모델에게 넘길 때는 값(함수)만 꺼내 씁니다.

아래는 code/ch04_web_agent.py 의 일부입니다 — 위에서 이미 붙여 넣었으니 다시 넣지 마세요.

config = types.GenerateContentConfig(
    tools=list(TOOLS.values()),        # [get_product_info, search_web]
    ...
)

이 패턴은 앞으로 계속 나옵니다. 도구를 추가하려면 TOOLS 에 한 줄만 넣으면 됩니다. config 도 실행 코드도 안 고칩니다.
python TOOLS = {..., "get_order_status": get_order_status} # 3장 도구 추가


2. 자동 실행은 끕니다

아래는 code/ch04_web_agent.py 의 일부입니다 — 위에서 이미 붙여 넣었으니 다시 넣지 마세요.

config = types.GenerateContentConfig(
    tools=list(TOOLS.values()),
    system_instruction=SYSTEM,
    temperature=0,
    # 자동 실행을 꺼야 우리가 루프를 직접 제어할 수 있다
    automatic_function_calling=types.AutomaticFunctionCallingConfig(disable=True),
)

3장에서는 켜 두고 SDK에 맡겼습니다. 이번엔 끕니다. 이유가 셋입니다.

  1. 단계를 보고 싶다 — 어떤 도구를 어떤 순서로 쓰는지
  2. 횟수를 통제하고 싶다MAX_STEPS 로 상한을 건다
  3. 루프의 내부를 배우려고 — 5장부터는 안 보이게 된다

3. 실행 — [2] 부분

터미널에서 실행 — 프로젝트 폴더 agentic_ai 에서, (agentic) 표시를 확인한 뒤.

python code/ch04_web_agent.py
==============================================================
[2] 두 도구가 모두 필요한 질문 — ReAct 루프가 도는 모습
  질문: 승승 스마트워치 Fit 5는 얼마인가요? 요즘 스마트워치 시장 가격대와
        비교하면 어느 수준인지도 알려주세요.

  [1단계] get_product_info({'product_name': '승승 스마트워치 Fit 5'})
           └ 승승 스마트워치 Fit 5 | 카테고리 전자기기 | 판매가 159,000원 ...
  [2단계] search_web({'query': '스마트워치 시장 가격대'})
           └ - 스마트워치 시장 규모 및 점유율, 성장 예측 2026-2035: ...
  [3단계] 도구 호출 없음 → 최종 답변

  [답변]
  승승 스마트워치 Fit 5는 159,000원입니다. 현재 스마트워치 시장은
  5만 원대 가성비 모델부터 30만 원대 프리미엄 모델까지 형성되어 있으며,
  Fit 5는 이 중 중간 가격대에 해당합니다.

==============================================================
관찰: 사내 데이터(가격)는 CSV에서, 시장 정보는 웹에서 각각 가져와 합쳤습니다.
      어떤 도구를 먼저 쓸지는 우리가 안 정했습니다. 모델이 스스로 정했습니다.

검색 결과는 실행할 때마다 다릅니다. 단계의 구조(1단계 CSV → 2단계 웹 → 3단계 답변)가 같으면 성공입니다.


4. 결과에서 확인할 것 네 가지

① 순서를 모델이 정했다

우리 코드 어디에도 이런 게 없습니다.

아래는 개념 설명용 코드입니다 — 실습 파일에 넣지 않습니다.

# 우리가 짜지 않은 코드
result1 = get_product_info(...)
result2 = search_web(...)

앞 절에서 예측해 보라고 했던 순서입니다. 가격을 먼저 알아본 이유는, 비교하려면 기준이 있어야 하기 때문입니다. 모델이 그 순서를 스스로 잡았습니다.

② 도구를 정확히 나눠 썼다

자사 가격  → get_product_info   (CSV)
시장 정보  → search_web         (인터넷)

독스트링에 적어 둔 사용 범위가 반영된 결과입니다. 앞 절에서 본 그대로죠.

③ 검색어를 문장에서 뽑아냈다

질문 안의 표현:  "요즘 스마트워치 시장 가격대와 비교하면 어느 수준인지도"
실제 검색어:     "스마트워치 시장 가격대"

④ 3단계에서 스스로 멈췄다

[3단계] 도구 호출 없음 → 최종 답변

MAX_STEPS 는 5인데 3단계에서 끝났습니다. 더 부를 도구가 없다고 모델이 판단한 것입니다. 우리가 "이제 그만"이라고 하지 않았습니다.

도구를 2번 썼는데 모델 호출은 3번입니다. 도구를 N번 쓰면 호출은 N+1번 — 마지막 한 번은 "이제 답한다"는 호출입니다. 3장에서 말한 "도구 한 번에 호출 두 번" 은 N=1인 경우였습니다. 2장 7절의 비용 계산도 이 식으로 하면 맞습니다.

호출 1도구 ① 요청₩ 요금도구 ① 실행get_product_info요금 없음(내 컴퓨터)호출 2도구 ② 요청₩ 요금도구 ② 실행search_web요금 없음(내 컴퓨터)호출 3도구 없음 = 답변₩ 요금도구 N번 → 모델 호출 N+1번도구 실행모델 호출1번2번2번3번5번6번마지막 한 번은 도구를 부르지 않고 답만 하는 호출입니다 — 요금은 파란 상자 개수만큼 나갑니다.
도구를 N번 쓰면 요금이 붙는 모델 호출은 N+1번이 된다

5. history 안을 들여다보기

3단계가 끝난 시점의 history 는 이렇게 생겼습니다. 요소 다섯 개짜리 리스트입니다.

history[0]  user   질문 원문
history[1]  model  get_product_info 호출 요청       ← 모델의 결정
history[2]  user   함수 실행 결과 (159,000원 ...)    ← 우리가 넣은 관찰
history[3]  model  search_web 호출 요청             ← 모델의 결정
history[4]  user   함수 실행 결과 (시장 정보 ...)     ← 우리가 넣은 관찰

직접 찍어 보고 싶다면 run_agentreturn 직전에 넣어 보세요.

직접 고쳐 보기agentic_ai/code/ch04_web_agent.py 를 열어 아래처럼 바꾸거나 덧붙인 뒤 다시 실행하세요. (확인이 끝나면 지워도 됩니다.)

        for i, c in enumerate(history):
            kind = "텍스트" if c.parts[0].text else "도구 관련"
            print(f"    history[{i}] role={c.role:5s} {kind}")

여기서 확인할 것rolemodel 인 것과 user 인 것이 번갈아 나옵니다. 그런데 history[2]history[4]사람이 말한 게 아닌데도 user 입니다. 도구 실행 결과를 "바깥에서 들어온 정보"로 취급하기 때문입니다. 처음 보면 어색하지만, 모델 입장에서는 자기가 만든 것(model)과 밖에서 온 것(user) 두 종류뿐이라 이렇게 됩니다.


6. 한 단계에 도구를 여러 개 부를 수도 있습니다

아래는 code/ch04_web_agent.py 의 일부입니다 — 위에서 이미 붙여 넣었으니 다시 넣지 마세요.

for fc in resp.function_calls:          # 복수형인 이유
    result = TOOLS[fc.name](**dict(fc.args))

3장 6절에서 봤듯 모델이 한 번에 여러 도구를 요청할 수 있습니다. 그러면 한 단계에서 여러 줄이 찍힙니다.

  [1단계] get_product_info({'product_name': '스마트워치'})
           └ 승승 스마트워치 Fit 5 | 카테고리 전자기기 | 판매가 159,000원 ...
  [1단계] get_product_info({'product_name': '로봇청소기'})
           └ 장구 로봇청소기 CleanX | 카테고리 가전 | 판매가 449,000원 ...

서로 의존하지 않는 일이면 모델이 한꺼번에 요청합니다. 반대로 앞 결과가 있어야 다음을 정할 수 있으면 단계를 나눕니다. 우리 예제가 후자였죠.


스스로 해 보기

  1. 질문을 "승승 스마트워치 Fit 5 얼마예요?" 로만 바꿔 보세요. 몇 단계에서 끝나나요? 그럴까요?
  2. 질문을 "승승 스마트워치 Fit 5와 장구 로봇청소기 가격을 알려주세요" 로 바꿔 보세요. 도구가 몇 번, 몇 단계에 걸쳐 불리나요?
    ("승승 로봇청소기"라고 쓰면 안 됩니다. 실제 상품명은 장구 로봇청소기 CleanX 라서 부분 일치로 찾을 수 없습니다. products.csv 를 열어 이름을 확인하고 쓰는 것도 연습입니다.)
  3. verbose=False 로 실행해 보세요 (run_agent(q, verbose=False)). 5장부터 보게 될 화면이 이것입니다.
  4. SYSTEM 에서 도구 이름을 언급한 부분을 지우고 실행해 보세요. 도구 선택이 달라지나요?

핵심 정리

  • 도구는 TOOLS 딕셔너리(이름 → 함수) 로 묶습니다. 모델이 이름 문자열로 요청하기 때문입니다.
  • 모델에게 넘길 때는 list(TOOLS.values()). 도구 추가는 딕셔너리에 한 줄이면 됩니다.
  • 자동 실행을 끕니다. 단계를 보고, 횟수를 통제하고, 안을 배우기 위해서입니다.
  • 결과에서 확인할 것 — ① 순서를 모델이 정했다 · ② 도구를 정확히 나눠 썼다 · ③ 검색어를 뽑아냈다 · ④ 스스로 멈췄다.
  • history 에는 질문 · 모델의 결정 · 실행 결과가 번갈아 쌓입니다. 도구 결과의 roleuser 입니다.
  • 서로 의존하지 않는 일은 한 단계에서 여러 도구를 부르기도 합니다.
← 이전 절05. ReAct 생각 행동 관찰의 반복다음 절 →07. 따라하기 for 루프로 에이전트 직접 만들기