실무 Multi-Agent 오케스트레이션 21장 · 전문 에이전트 구현 9 / 9 ← 이전목차다음 → TechLead Cro

21장. 전문 에이전트 구현

실습문제와 해답

에이전트를 하나씩 더 불러 보며 프롬프트의 규칙과 도구 안의 판정이 보고에 어떻게 나타나는지 확인합니다. 먼저 스스로 해 본 뒤 해답을 펼쳐 보세요. 해답은 새 파일로 만들어 실행합니다.

문제 2와 3의 해답은 첫 줄에서 처리 기록 파일을 지우고 시작합니다. 그래야 처리번호가 A00001부터 나와 이 교재의 결과와 맞춰 볼 수 있습니다.


문제 1 — 설명에 없는 것, 데이터에 없는 것

주문조회 에이전트의 프롬프트에는 데이터의 경계를 적은 규칙이 두 줄 있습니다. "상품 설명에 없는 성능·기능은 '설명에 없음'이라고 보고한다"와 "재고 수량은 상품 전체 기준이다"입니다. 두 규칙이 보고에 어떻게 나타나는지 확인하세요. 새 파일 ex21_tumbler.py를 만들어 아래 두 질문을 보냅니다.

  1. "텀블러 식기세척기 돌려도 되나요?"
  2. "스테인리스 텀블러 민트색 재고 있어요?"
  • 실행하기 전에 짐작해 보세요. 상품 데이터에서 이 텀블러의 설명은 "12시간 보온·보냉. 세척 편한 와이드 입구."가 전부이고, 재고는 150 하나로만 적혀 있습니다. 색상별 재고는 데이터에 없습니다.
해답 보기
from haru_agents import build_agents, run_agent
from haru_tools import CustomerSession

agents = build_agents(CustomerSession("C003"))

for q in ["텀블러 식기세척기 돌려도 되나요?",
          "스테인리스 텀블러 민트색 재고 있어요?"]:
    print(f"\n질문: {q}")
    print("  보고:", run_agent(agents["order"], q))
$ python ex21_tumbler.py

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

질문: 텀블러 식기세척기 돌려도 되나요?
    [도구] search_products({'keyword': '텀블러'})
  보고: 하루마켓에서 판매 중인 '스테인리스 텀블러 500ml(P008)' 상품 설명에는 세척 편한 와이드 입구라고 안내되어 있으나, 식기세척기 사용 가능 여부에 대한 내용은 설명에 없습니다.

질문: 스테인리스 텀블러 민트색 재고 있어요?
    [도구] search_products({'keyword': '스테인리스 텀블러'})
    [도구] check_stock({'product_id': 'P008'})
  보고: '스테인리스 텀블러 500ml(민트색)' 상품 전체 재고는 150개입니다. (옵션별 수량은 데이터에 없음)

해설 — 두 질문 모두 데이터에 있는 만큼만 보고했습니다.

첫 질문은 search_products로 상품을 찾은 뒤 "식기세척기 사용 가능 여부에 대한 내용은 설명에 없습니다"라고 보고했습니다. 된다고도 안 된다고도 하지 않았습니다. 프롬프트의 "상품 설명에 없는 성능·기능은 '설명에 없음'이라고 보고합니다"가 그대로 나타났습니다.

둘째 질문은 도구를 절차대로 불렀습니다. search_products로 상품 ID(P008)를 찾고, 그 ID로 check_stock을 불렀습니다. 보고는 "상품 전체 재고는 150개입니다. (옵션별 수량은 데이터에 없음)"입니다. check_stock이 돌려준 150은 실버·블랙·민트를 모두 합친 숫자이고, 에이전트는 그 뜻 그대로 전했습니다. 이 교재를 준비하며 세 번 실행해 세 번 모두 같은 도구를 같은 순서로 불렀고, "상품 전체 재고 150개 (옵션별 수량은 데이터에 없음)"로 보고했습니다.

두 가지를 가져갑니다.

  • 숫자를 전하는 것과 숫자의 뜻을 전하는 것은 다릅니다. "150개"만 옮기면 받는 쪽은 민트색이 150개라고 읽습니다. "상품 전체"라는 기준까지 보고에 있어야 합니다.
  • 데이터의 생김새는 개발자가 적어 줍니다. 20장의 재고 Worker에게 적어 준 사실을 이 에이전트에게도 적었습니다. 프롬프트는 에이전트마다 따로이므로, 같은 데이터를 다루는 곳마다 같은 경계를 적습니다.

문제 2 — 세 갈래를 한 번씩

요청처리 에이전트에게 성격이 다른 요청 세 건을 차례로 보내, 요청마다 처리 기록이 어떻게 생기는지 확인하는 파일 ex21_judge.py를 만드세요.

  1. "주문 HR20260721001 운동화를 블랙/270 으로 교환해 주세요. 사이즈가 작아요."
  2. "주문 HR20260628003 텀블러를 반품하고 환불받고 싶어요. 사유는 단순 변심입니다."
  3. "주문 HR20260720022 무드등 주문을 취소해 주세요."
  • 요청마다 새로 생긴 처리 기록(처리번호, 종류, 상태)을 출력합니다. haru_actions의 load_actions()가 기록 전체를 목록으로 돌려줍니다. 요청 전의 건수를 기억해 두면 새로 생긴 것만 잘라 낼 수 있습니다.
  • 실행하기 전에 세 요청의 결과를 짐작해 보세요. 운동화는 7월 25일, 텀블러는 7월 1일에 배송 완료됐고, 무드등은 아직 결제완료 상태입니다. 실습 기준일은 7월 27일입니다.
해답 보기
from haru_actions import ACTIONS_PATH, load_actions
from haru_agents import build_agents, run_agent
from haru_tools import CustomerSession

ACTIONS_PATH.unlink(missing_ok=True)       # 처리 기록을 비우고 시작한다 (A00001 부터)
agents = build_agents(CustomerSession("C003"))

REQUESTS = [
    "주문 HR20260721001 운동화를 블랙/270 으로 교환해 주세요. 사이즈가 작아요.",
    "주문 HR20260628003 텀블러를 반품하고 환불받고 싶어요. 사유는 단순 변심입니다.",
    "주문 HR20260720022 무드등 주문을 취소해 주세요.",
]

for r in REQUESTS:
    before = len(load_actions())
    print(f"\n요청: {r}")
    print("  보고:", run_agent(agents["action"], r))
    new = load_actions()[before:]           # 이번 요청으로 새로 생긴 처리 기록
    print("  새 처리 기록:",
          [(a["action_id"], a["type"], a["status"]) for a in new] or "없음")
$ python ex21_judge.py

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

요청: 주문 HR20260721001 운동화를 블랙/270 으로 교환해 주세요. 사이즈가 작아요.
    [도구] request_exchange({'new_option': '블랙/270', 'order_id': 'HR20260721001', 'reason': '사이즈가 작음', 'reason_category': '단순변심'})
  보고: 주문번호 `HR20260721001`의 교환 접수가 완료되었습니다.

- **처리번호:** A00001
- **결과:** 교환 접수 완료 (기존 '화이트/250' 옵션에서 '블랙/270'으로 교환)

회수 기사가 방문하여 기존 상품을 수거한 후 새 상품이 배송될 예정입니다.
  새 처리 기록: [('A00001', '교환접수', '완료')]

요청: 주문 HR20260628003 텀블러를 반품하고 환불받고 싶어요. 사유는 단순 변심입니다.
    [도구] request_refund({'order_id': 'HR20260628003', 'reason': '단순 변심', 'reason_category': '단순변심'})
  보고: 환불(반품) 요청이 처리되지 않았습니다.

**사유:** 배송 완료 후 26일이 지나 반품 가능 기간(7일)을 넘었습니다.
  새 처리 기록: 없음

요청: 주문 HR20260720022 무드등 주문을 취소해 주세요.
    [도구] request_refund({'order_id': 'HR20260720022', 'reason': '주문 취소', 'reason_category': '확인필요'})
  보고: 주문번호 HR20260720022(LED 무드등)의 주문 취소(환불) 승인 요청이 접수되었습니다. (처리번호: A00002)
담당자 승인 후 최종 처리될 예정입니다.
  새 처리 기록: [('A00002', '주문취소요청', '승인대기')]

해설 — 세 요청이 세 가지로 끝났습니다.

요청 부른 도구 코드의 판정 새 처리 기록
운동화 교환 request_exchange 배송 완료 후 2일, 재고 있음 → 통과 A00001 교환접수 · 완료
텀블러 반품 request_refund 배송 완료 후 26일 → 기간(7일) 초과 없음
무드등 취소 request_refund 출고 전 → 주문 취소로 접수 A00002 주문취소요청 · 승인대기

둘째와 셋째는 같은 도구를 불렀습니다. 에이전트는 두 요청을 똑같이 request_refund로 넘겼고, 갈라놓은 것은 도구 안의 코드입니다. 텀블러는 7월 1일에 받아 26일이 지났으므로 기록 없이 이유만 돌려줬고, 무드등은 아직 출고 전이라 주문취소요청으로 등록됐습니다. 기록의 종류(환불요청인지 주문취소요청인지)도 모델이 고른 것이 아니라 코드가 주문 상태를 보고 정한 것입니다.

둘째 요청의 보고를 봅니다. 에이전트는 "처리되지 않았습니다"라고 했고, 26일과 7일이라는 숫자를 도구가 돌려준 그대로 옮겼습니다. 프롬프트의 "도구가 ok=False 를 돌려주면 처리하지 않은 것입니다. 그 reason 을 그대로 보고하고, 처리했다고 말하지 않습니다"가 지켜졌습니다.

첫째는 완료, 셋째는 승인대기입니다. 교환 접수는 에이전트가 끝냈고, 주문 취소는 사람의 승인을 기다립니다.

이 교재를 준비하며 세 번 실행했고, 세 번 모두 부른 도구와 새 처리 기록이 이 표와 같았습니다.


문제 3 — 세 에이전트를 손으로 이어 보기

표준 질문 Q6은 "지난주에 산 운동화가 불량이에요. 검정 270으로 교환 접수해 주세요."입니다. 고객은 주문번호를 말하지 않았습니다. 어느 주문인지는 주문조회 에이전트가, 불량 교환의 배송비 규정은 정책안내 에이전트가 알고, 교환 접수는 요청처리 에이전트가 합니다. 새 파일 ex21_relay.py를 만들어 세 에이전트를 차례로 부르세요.

  1. 주문조회 에이전트에게 지난주에 산 운동화 주문의 주문번호·옵션·배송 상태를 확인시킵니다.
  2. 정책안내 에이전트에게 불량 상품을 교환할 때 배송비를 누가 부담하는지 묻습니다.
  3. 1번의 보고를 요청문에 담아 요청처리 에이전트에게 블랙/270 으로 교환 접수를 맡깁니다.
  • 힌트: 전문 에이전트는 다른 에이전트가 한 일을 모릅니다. 앞에서 알아낸 것은 요청문에 글로 적어 넘겨야 합니다.
해답 보기
from haru_actions import ACTIONS_PATH, load_actions
from haru_agents import build_agents, run_agent
from haru_tools import CustomerSession

ACTIONS_PATH.unlink(missing_ok=True)       # 처리 기록을 비우고 시작한다 (A00001 부터)
agents = build_agents(CustomerSession("C003"))

# 1) 주문조회 에이전트에게: 어느 주문인지 확인한다
print("■ 1) 주문조회 에이전트 — 어느 주문인가")
order_report = run_agent(agents["order"],
                         "지난주에 산 운동화 주문의 주문번호·옵션·배송 상태를 확인해 주세요.")
print("  보고:", order_report)

# 2) 정책안내 에이전트에게: 불량 교환의 배송비 규정을 확인한다
print("\n■ 2) 정책안내 에이전트 — 불량 교환 규정")
policy_report = run_agent(agents["policy"], "불량 상품을 교환할 때 배송비는 누가 부담하나요?")
print("  보고:", policy_report)

# 3) 요청처리 에이전트에게: 앞에서 받은 보고를 요청문에 담아 넘긴다
print("\n■ 3) 요청처리 에이전트 — 교환 접수")
request = (f"[확인된 주문] {order_report}\n"
           "이 주문의 운동화가 불량입니다. 블랙/270 으로 교환을 접수해 주세요.")
print("  보고:", run_agent(agents["action"], request))

print("\n■ 처리 기록")
for a in load_actions():
    print(f"  {a['action_id']} · {a['type']} · {a['status']} · {a['detail']}")
$ python ex21_relay.py

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

■ 1) 주문조회 에이전트 — 어느 주문인가
    [도구] get_my_orders({})
    [도구] get_order_status({'order_id': 'HR20260721001'})
  보고: - 주문번호: HR20260721001
- 상품명: 쿠션 운동화
- 옵션: 화이트/250
- 배송 상태: 배송완료 (배송완료일: 2026-07-25)

■ 2) 정책안내 에이전트 — 불량 교환 규정
    [도구] search_policy({'query': '불량 상품 교환 배송비'})
  보고: 상품 불량으로 인한 교환 시, 배송비는 회사가 전액 부담합니다.

(근거: 반품·교환·환불 정책 제4조)

■ 3) 요청처리 에이전트 — 교환 접수
    [도구] request_exchange({'reason': '불량', 'reason_category': '상품하자', 'order_id': 'HR20260721001', 'new_option': '블랙/270'})
  보고: 교환 접수가 완료되었습니다.

- **처리번호**: A00001
- **주문번호**: HR20260721001
- **상품명**: 쿠션 운동화 (화이트/250 → 블랙/270)
- **사유**: 불량
- **결과**: 교환이 접수되었습니다. 회수 기사가 방문해 기존 상품을 수거한 뒤 새 상품으로 배송해 드릴 예정입니다.

■ 처리 기록
  A00001 · 교환접수 · 완료 · 쿠션 운동화 화이트/250 → 블랙/270 (불량)

해설

주문조회 에이전트는 주문을 찾아냈습니다. "지난주에 산 운동화"라는 말만으로 get_my_orders를 불러 목록에서 쿠션 운동화를 찾고, get_order_status로 그 주문을 다시 확인했습니다. 누구의 주문인지는 세션(C003)이 정합니다.

정책안내 에이전트는 규정을 찾았습니다. 검색어는 불량 상품 교환 배송비입니다. 질문에서 조사와 물음을 뗀 핵심어를 스스로 만들었고, 보고 끝에 근거 조항을 붙였습니다.

요청처리 에이전트는 넘겨받은 주문번호로 처리했습니다. request_exchange의 인자에 HR20260721001이 들어 있습니다. 이 주문번호는 고객이 말한 것이 아니라 우리가 요청문에 담아 준 [확인된 주문]에서 왔습니다. 사유에 "불량"이 들어 있으므로 코드는 불량에 해당하는 기간을 적용해 판정했고, 처리 기록 A00001이 남았습니다.

누가 무엇을 했는지 나눠 봅니다.

한 일 누가
주문을 찾을 도구를 고르고 부른 것 주문조회 에이전트(모델)
검색어를 만들고 근거를 붙인 것 정책안내 에이전트(모델)
교환 도구를 고르고 인자를 채운 것 요청처리 에이전트(모델)
교환할 수 있는지 판정하고 기록을 남긴 것 도구 안의 코드
누구에게 먼저 물을지 정하고, 앞의 보고를 뒤의 요청문에 담은 것 우리가 쓴 이 파일

마지막 줄이 이 문제의 요점입니다. 이번에는 순서와 전달을 우리가 코드로 적었습니다. 문의마다 이렇게 적을 수는 없습니다. 누구에게 무엇을 물을지 정하고, 알아낸 것을 다음 요청에 담아 넘기는 일을 맡을 자리가 필요합니다(22장).

이 교재를 준비하며 세 번 실행했고, 세 번 모두 세 에이전트가 같은 도구를 불렀으며 처리 기록은 A00001 · 교환접수 · 완료 한 건이었습니다.

문제 2와 3을 마치면 기록 파일에 연습한 처리가 남아 있습니다. 처음 상태로 돌리려면 기록 지우기 한 줄을 실행합니다.

$ python -c "from haru_actions import ACTIONS_PATH; ACTIONS_PATH.unlink(missing_ok=True)"

핵심 정리

  • 주문조회 에이전트는 데이터에 있는 만큼만 보고합니다. 설명에 없으면 없다고, 재고는 상품 전체 기준이라고 전합니다.
  • 요청처리 에이전트는 반품과 주문 취소에 같은 도구를 불렀고, 결과를 가른 것은 도구 안의 코드였습니다.
  • 처리 결과는 세 가지로 끝납니다. 완료, 승인대기, 처리하지 않음(이유 보고).
  • 전문 에이전트는 다른 에이전트가 한 일을 모릅니다. 앞에서 알아낸 사실은 요청문에 담아 넘겨야 합니다.
  • 순서를 정하고 보고를 이어 주는 일을 이번에는 우리가 코드로 했습니다. 이 일을 맡을 자리가 필요합니다.
← 이전 절정리와 체크리스트22장 →왜 오케스트레이션이 필요한가 — 전문가는 셋인데 지휘할 자리가 비어 있다
오명운 · macro@prag-ai.com