실무 Multi-Agent 오케스트레이션 23장 · Human-in-the-Loop 2 / 7 ← 이전목차다음 → TechLead Cro

23장. Human-in-the-Loop

AI의 일과 사람의 일 — 준비는 코드와 AI가, 결정은 사람이

한 줄 요약

HITL 설계에서 먼저 정할 것은 기술이 아니라 역할 분담입니다. 코드가 요건을 판정해 승인 요청을 등록하고 결과를 기록하고, AI가 사유를 분류하고 상담원이 볼 요청서를 쓰고, 사람은 승인이나 반려라는 되돌릴 수 없는 결정만 내립니다. 모든 건을 사람에게 보내지 않도록 승인 게이트로 보내기 전에 대상을 선별합니다.


1. 환불 한 건을 나눠 보면

환불 요청 하나를 처리하는 데 필요한 일을 늘어놓고, 누가 할지 정합니다.

일 누가 우리 코드에서 이유
주문 데이터 조회 코드 get_order_status (8장) 정확해야 하는 값은 코드가 가져온다
기간 판정 (받은 지 며칠 지났나) 코드 request_refund 안 (21장) 날짜 뺄셈은 규칙이 정해져 있고 답이 하나다
승인 요청 등록 (승인대기) 코드 request_refund (21장) 처리번호와 상태는 틀리면 안 되는 값이다
사유 분류 AI classify_reason 고객 설명의 의미와 부정 표현을 해석한다
상담원이 볼 요청서 쓰기 AI review_node의 LLM 호출 읽기 좋게 정리하는 일은 모델이 잘한다
승인 또는 반려 사람 approval_gate 되돌릴 수 없다
결과 기록 (승인완료 / 반려) 코드 decide (21장) 사람의 결정이 났을 때만, 정해진 대로

코드와 AI가 맡은 줄은 준비와 기록입니다. 준비한 결과는 요청서에 적혀 사람이 읽습니다. 사람이 맡은 한 줄은 그 뒤에 되돌릴 방법이 없는 일입니다.

코드의 일 AI의 일 사람의 일
성격 값, 계산, 기록 읽고 정리하기 되돌릴 수 없는 결정
결과 실행마다 같다 문장이 실행마다 다르다 요청서가 잘 준비되어 있으면 몇 초

이 배치의 목표는 사람의 일을 줄이되 없애지 않는 것입니다. 조회하고 대조하는 반복 작업은 코드와 AI가 가져가고, 사람은 마지막 문 하나만 지킵니다.


2. 모든 건을 사람에게 보내지 않는다

환불 요청이 하루에 백 건 온다고 합시다. 그중 서른 건은 기간이 한참 지난 요청입니다. 이것까지 전부 상담원 화면에 올리면 두 가지가 나빠집니다.

  • 상담원의 시간이 명백히 안 되는 건을 확인하는 데 쓰입니다.
  • 정작 봐야 할 건이 그 사이에 묻힙니다.

그래서 게이트 앞에 거르는 단계를 둡니다.

판정 결과 가는 곳 사람이 보나
요건을 채웠다 승인 게이트 본다
요건을 채우지 못했다 이유를 안내하고 종료 보지 않는다

AI나 자동 처리가 감당하지 못하는 건을 사람에게 올려 보내는 일을 에스컬레이션(escalation) 이라고 부릅니다. "어떤 건을 올려 보낼 것인가"를 정하는 것이 곧 상담원의 시간이라는 한정된 자원을 어디에 쓸지 정하는 일입니다.

코드에서는 19장에서 배운 조건 분기 한 줄입니다.

def route_after_review(state: RefundState) -> str:
    # 요건 미달이면 사람까지 갈 필요 없이 안내한다 (무엇을 사람에게 올릴지의 설계)
    return "approval_gate" if state["eligible"] else "reject"

3. 거르는 기준은 코드가 판정한다

위 구조에서 두 갈래는 그 뒤가 다릅니다.

판정 결과 그 뒤
요건 충족 사람이 요청서를 한 번 더 본다
요건 미달 사람 없이 바로 안내하고 끝난다

미달 쪽은 사람의 확인 없이 끝나므로, 거르는 기준은 실행마다 같은 답이 나오는 것이어야 합니다. 단순 변심 반품의 요건은 "배송 완료 후 7일 이내"이고, 이것은 날짜 뺄셈입니다. 그 판정은 21장에서 만든 처리 도구 request_refund에 이미 들어 있습니다.

elif status == "배송완료":
    days = _days_since_delivery(order)
    limit = _return_limit(reason_category)
    if limit is None:
        return dict(NEEDS_REASON)
    if days is None or days > limit:
        return {"ok": False, "order_id": order_id, "days_since_delivery": days,
                "reason": f"배송 완료 후 {days}일이 지나 반품 가능 기간({limit}일)을 넘었습니다."}

그래서 이번 장의 검토 노드는 날짜를 직접 세지 않고, 그 도구를 부른 결과의 ok만 봅니다.

classification = classify_reason(state["question"])
req = actions["request_refund"](
    state["order_id"], state["question"], classification.reason_category)
if not req["ok"]:                       # 요건 미달 — 승인 요청을 만들지 않았다
    return {"review": req["reason"], "eligible": False, "action_id": ""}
줄 뜻
classify_reason(...) 모델이 고객 설명을 해석해 사유 분류와 근거를 반환한다
actions["request_refund"](...) 기간을 판정하고, 통과하면 승인 요청을 승인대기로 등록한다. 모델이 아니라 우리 코드가 직접 부른다
req["ok"] 통과했으면 True, 아니면 False. 사유 확인 여부와 주문 상태·기간을 검증한 결과다
"eligible": False 갈림길 함수가 이 값을 보고 승인 게이트 대신 안내로 보낸다

모델은 오늘이 며칠인지 모릅니다. 날짜 판정을 모델에게 맡기면 고객의 말("어제 받았어요")을 그대로 믿게 됩니다. 사유 분류에는 모델의 판단을 사용하지만, 날짜 계산과 기간 비교는 코드가 수행합니다. 사유를 확인해야 하는 경우에도 승인 요청을 만들지 않고 고객에게 확인할 내용을 안내합니다.


4. 기록하는 코드는 어디에 두는가

사람이 결정하면 그 결과를 처리 기록에 남깁니다. 21장에서 만들어 둔 decide가 그 일을 합니다.

def execute_node(state: RefundState) -> dict:
    # 승인 결과를 처리 기록에 남긴다. 승인일 때만 '승인완료'가 된다.
    # 실제 서비스라면 승인 직후 여기서 결제 대행사의 환불 API 를 호출한다.
    done = decide(state["action_id"], approve=state["approved"], by="상담원")
    if state["approved"]:
        return {"result": f"주문 {state['order_id']} 환불이 승인되어 처리되었습니다. "
                          f"(처리번호 {done['action_id']} · 상태: {done['status']})"}
    return {"result": f"상담원 검토 결과 환불이 반려되었습니다. "
                      f"(처리번호 {done['action_id']} · 상태: {done['status']})"}

결제 대행사는 카드 결제와 취소를 대신 처리해 주는 회사입니다(흔히 PG사라고 부릅니다). 수업에서는 실제 돈을 움직이지 않고, 처리 기록의 상태를 승인완료로 바꾸는 것으로 실행을 대신합니다.

이 코드가 안전한 이유는 내용이 아니라 자리에 있습니다.

  • 기록이 승인완료가 되는 것은 approved가 참일 때뿐입니다.
  • approved를 참으로 바꾸는 곳은 승인 게이트 한 곳뿐입니다.
  • 승인 게이트는 사람이 "approve"라고 답해야만 참을 돌려줍니다.

그리고 한 가지 더. decide는 모델이 부를 수 있는 도구가 아닙니다. 요청처리 에이전트의 도구 목록에는 request_refund(승인 요청 등록)는 있어도 decide는 없습니다. Supervisor의 도구 목록에도 없습니다. decide를 부르는 길은 사람의 결정을 지나는 길뿐입니다.

함수 하는 일 모델이 부를 수 있나
request_refund 승인 요청을 승인대기로 등록 있다 (요청처리 에이전트의 도구)
decide 승인완료 또는 반려로 기록 없다 (승인 게이트 뒤, 사람의 결정으로만)

위험한 실행은 "하지 마라"고 쓰는 것이 아니라, 사람을 지나야만 닿는 자리에 둡니다.


핵심 정리

  • 코드가 요건을 판정해 승인 요청을 등록하고 결과를 기록합니다. AI는 사유를 분류하고 요청서를 쓰고, 사람은 승인·반려를 결정합니다.
  • 목표는 사람의 일을 줄이되 없애지 않는 것입니다.
  • 요건 미달은 게이트 앞에서 걸러 사람에게 가지 않게 합니다. 이것이 에스컬레이션 조건의 설계입니다.
  • 거르는 기준인 기간은 코드(request_refund)가 판정합니다. 미달 건은 사람이 다시 보지 않기 때문입니다.
  • 결과를 기록하는 decide는 모델의 도구가 아니고, 사람의 승인을 지나야만 불립니다.
← 이전 절왜 사람이 끼어들어야 하는가 — 되돌릴 수 없는 일 앞에서다음 절 →interrupt — 그래프를 멈추고 이어 가기
오명운 · macro@prag-ai.com