실무 Multi-Agent 오케스트레이션 8장 · 보안 - 안전한 도구 실행 4 / 8 ← 이전목차다음 → TechLead Cro

8장. 보안 - 안전한 도구 실행

읽기 도구와 쓰기 도구 — 흔적을 남기는 도구는 다르게 다룬다

한 줄 요약

지금까지의 도구는 데이터를 읽기만 했습니다. 이번 장에서 처음으로 파일에 기록을 남기는 도구 create_ticket이 생깁니다. 흔적을 남기는 도구는 잘못 불렸을 때 되돌려야 하므로, 되돌릴 수 있는 것만 도구로 만듭니다. 그리고 어떤 도구든 예외로 멈추지 않고 값을 돌려주게 만듭니다.


1. 부작용이 있는 도구

함수가 값을 돌려주는 것 말고 바깥에 남기는 변화를 부작용(side effect) 이라고 합니다. 파일에 쓰기, 메일 보내기, 결제하기가 그렇습니다.

읽기 도구 쓰기 도구
예 get_my_orders, search_products, check_stock create_ticket
실행하면 값을 돌려준다 값을 돌려주고 파일이 바뀐다
잘못 불리면 토큰이 낭비된다 누군가 되돌려야 한다
여러 번 불리면 같은 값이 나온다 기록이 여러 개 쌓인다

읽기 도구는 모델이 백 번을 잘못 불러도 세상은 그대로입니다. 쓰기 도구는 한 번 잘못 불리면 흔적이 남습니다.


2. 무엇을 도구로 만들고 무엇은 만들지 않는가

판단 기준은 잘못 실행됐을 때 되돌릴 수 있는가입니다.

기능 잘못 실행되면 도구로 만드는가
상담 티켓 접수 상담원이 티켓을 닫으면 된다 만든다 (create_ticket)
환불 실행 돈이 이미 나갔다 만들지 않는다
주문 취소 재고와 결제가 함께 움직인다 만들지 않는다

haru_tools.py의 쓰기 도구는 create_ticket 하나뿐입니다. 환불 도구가 없는 것은 빠뜨린 것이 아니라 일부러 만들지 않은 것입니다.

5장에서 본 것을 떠올립니다. 모델은 선언된 도구만 부를 수 있습니다. 환불 도구가 없으면, 고객이 "지금 당장 환불 처리해"라고 아무리 요구해도 모델이 환불을 실행할 방법이 없습니다. 앞 절에서 고객 ID를 "인자에서 뺀" 것과 같은 생각입니다. 없는 것은 쓸 수 없습니다.

되돌릴 수 없는 일은 사람의 승인을 거친 뒤에만 실행하도록 따로 만듭니다(23장).


3. create_ticket이 하는 일

def create_ticket(category: str, summary: str, urgency: str = "보통") -> dict:
    """[쓰기] 상담원 처리가 필요한 문의를 티켓으로 등록한다.
    AI가 해결하지 못한 문의, 상담원 연결 요청, 계정·결제 문제에 사용한다.

    Args:
        category: 티켓 분류. 제품문의/주문배송조회/환불교환/계정결제/
            상담원연결/기타 중 하나
        summary: 상담원이 볼 한 줄 요약 (고객 상황 + 요청 사항)
        urgency: 낮음/보통/높음
    """

티켓(ticket) 은 "사람이 처리해야 할 문의 한 건"을 적어 둔 기록입니다. 이 도구가 접수한 티켓은 memory_store/tickets.json 파일에 쌓입니다.

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

값 정하는 것 이유
category, summary, urgency 모델 대화를 읽고 요약하는 일은 모델이 잘한다
ticket_id (T00001) 코드 번호는 겹치면 안 된다
created_at 코드 시각은 정확해야 한다
customer_id, customer_name 코드 (세션) 누가 접수했는지는 속일 수 없어야 한다
ticket = {
    "ticket_id": f"T{len(tickets) + 1:05d}",
    "created_at": datetime.now().strftime("%Y-%m-%d %H:%M"),
    "customer_id": session.customer_id,
    "customer_name": mask_name(session.name),   # 마스킹된 이름만 기록
    "category": category, "summary": summary,
    "urgency": urgency, "status": "접수",
}

모델에게 "티켓 번호를 지어내라"고 하지 않습니다. 번호, 시각, 고객처럼 틀리면 안 되는 값은 코드가 만들고, 모델은 돌려받은 번호를 고객에게 전하기만 합니다.

독스트링 맨 앞의 [쓰기] 는 이 도구가 흔적을 남긴다는 표시입니다. 모델도 읽고, 코드를 여는 사람도 읽습니다.


4. 말만 하고 부르지 않는 경우

쓰기 도구에는 살펴볼 것이 하나 더 있습니다. 모델이 "접수해 드리겠습니다"라고 말만 하고 도구를 부르지 않을 수 있습니다. 고객은 접수됐다고 믿고 기다리지만 기록은 없고, 아무도 연락하지 않습니다.

그래서 이번 장의 시스템 프롬프트에는 이런 줄이 있습니다.

상담원 처리가 필요한 문의는 "접수하겠다"고 말만 하지 말고
즉시 create_ticket 을 호출해 접수한 뒤 티켓번호를 안내합니다.

이 두 줄은 바라는 행동을 적어 둔 부탁입니다. 그래서 접수됐는지는 따로 확인합니다. 답의 문장이 아니라 호출 이력에 create_ticket이 있는지, 그리고 파일에 기록이 생겼는지를 봅니다. 「따라하기」에서 그렇게 확인합니다.


5. 도구는 예외로 멈추지 않는다

마지막 위험입니다. 도구 안에서 예외가 나면 어떻게 될까요.

search_products는 str.contains로 검색합니다. 이 함수는 검색어를 정규식(글자 패턴을 적는 표기법)으로 읽으므로, 고객이 텀블러(처럼 괄호 하나를 잘못 치면 패턴으로 읽을 수 없어 예외가 납니다. 그래서 haru_tools.py의 도구는 검색 부분을 try로 감쌌습니다.

try:
    mask = (
        PRODUCTS["product_name"].str.contains(keyword, na=False)
        | PRODUCTS["category"].str.contains(keyword, na=False)
        | PRODUCTS["description"].str.contains(keyword, na=False)
    )
    hits = PRODUCTS[mask].head(5)
except Exception as e:                      # 어떤 입력에도 죽지 않는다
    return [{"found": False, "error": str(e)}]

검색어 텀블러(를 넣은 결과입니다.

[{'found': False, 'error': 'missing ), unterminated subpattern at position 3'}]

멈추지 않고 값을 돌려줬습니다. 모델은 이 값을 읽고 "검색어를 다시 확인해 달라"고 응대를 이어 갈 수 있습니다.

상황 돌려주는 값
찾았다 {"found": True, ...}
없다 (예상한 실패) {"found": False, "message": ...}
남의 주문이다 없을 때와 같은 값
예외가 났다 (예상 못 한 실패) {"found": False, "error": ...}

어느 경우든 모델이 읽을 수 있는 값이 돌아갑니다. 6장에서 "없는 주문번호"를 found: False로 돌려준 것과 같은 생각을 예외까지 넓힌 것입니다.


핵심 정리

  • 파일에 쓰기처럼 바깥에 흔적을 남기는 것을 부작용이라 하고, 그런 도구를 쓰기 도구로 구분합니다.
  • 도구로 만드는 기준은 잘못 실행됐을 때 되돌릴 수 있는가입니다. 환불 실행은 도구로 만들지 않았습니다.
  • 티켓의 번호·시각·고객은 코드가, 분류·요약·긴급도는 모델이 정합니다.
  • 쓰기 도구가 실제로 불렸는지는 답의 문장이 아니라 호출 이력과 파일로 확인합니다.
  • 도구는 예외로 멈추지 않고 항상 값을 돌려줘, 모델이 응대를 이어 가게 합니다.
← 이전 절개인정보 마스킹 — 모델에게 준 것은 화면에 나올 수 있다다음 절 →따라하기 1 — 도구 모듈 haru_tools.py 만들기
오명운 · macro@prag-ai.com