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

23장. Human-in-the-Loop

요청서 — 사람이 바로 판단하게 만든다

한 줄 요약

승인 게이트에서 사람에게 건네는 글을 요청서라고 부르겠습니다. 요청서가 부실하면 상담원이 처음부터 다시 조회해야 하고, 그러면 코드와 AI가 준비한 의미가 없습니다. 좋은 요청서는 판단에 필요한 값을 한 화면에 보여 주고, 그 끝에 코드가 확인한 사실을 붙입니다.


1. 요청서는 interrupt로 내보내는 값이다

코드에서 요청서는 interrupt(...)에 넘기는 딕셔너리입니다.

decision = interrupt({
    "type": "refund_approval",
    "order_id": state["order_id"],
    "action_id": state["action_id"],
    "review": state["review"],
    "ask": "이 환불을 실행할까요? (approve / reject)",
})
항목 내용 누가 만들었나
type 어떤 종류의 승인 요청인지 개발자가 정한 값
order_id 주문번호 상태에 있던 값 (코드)
action_id 등록된 승인 요청의 처리번호 (A로 시작) 코드 (request_refund가 기록하며 매긴 번호)
review 요청서 본문, 끝에 [시스템 확인] 줄 AI가 쓴 글 + 코드가 확인한 사실
ask 상담원에게 묻는 질문과 답의 형식 개발자가 정한 값

type을 넣어 두면, 나중에 환불 말고 다른 승인(예: 쿠폰 발급)이 생겨도 상담원 화면이 종류별로 다르게 보여 줄 수 있습니다.


2. AI가 쓰는 부분 — 형식을 정해 준다

review의 본문은 검토 노드에서 모델이 씁니다. 이때 준 지시가 요청서의 품질을 정합니다.

r = client.models.generate_content(
    model=MODEL,
    contents=f"주문 정보: {order}\n고객 요청: {state['question']}\n\n"
             f"상담원이 보고 바로 승인 여부를 정할 수 있도록 환불 요청서를 "
             f"4줄 이내로 작성하라. 주문번호·상품·금액·배송 완료일을 반드시 "
             f"포함하라. 주문 정보에 없는 내용은 쓰지 마라.",
)

3장에서 배운 것들이 들어 있습니다.

지시 왜 넣었나
"상담원이 보고 바로 승인 여부를 정할 수 있도록" 읽는 사람과 쓰임을 밝힌다. 고객에게 보내는 글이 아니다
"4줄 이내" 길이를 숫자로 정한다. 상담원이 한눈에 읽어야 한다
"주문번호·상품·금액·배송 완료일을 반드시 포함" 상담원이 판단에 쓸 값 네 가지를 이름으로 짚어 빠지지 않게 한다
"주문 정보에 없는 내용은 쓰지 마라" 모델이 규정이나 사정을 지어 덧붙이지 않게 한다

이 지시에는 "환불해도 되는지 판단하라"는 말이 없습니다. 요건을 채웠는지는 이 호출보다 앞에서 코드가 이미 판정했습니다. 모델이 이 자리에서 하는 일은 판정이 아니라 정리입니다.


3. 코드가 붙이는 부분 — [시스템 확인]

모델이 쓴 글 뒤에 코드가 한 줄을 붙입니다.

# 요청서 끝에 코드가 확인한 사실을 그대로 붙인다 — 상담원이 직접 본다
review = (r.text.strip() + f"\n[시스템 확인] 승인 요청 {req['action_id']} 등록 · "
          f"결제 {req['order_amount']}원 · 상태: {req['status']}")
return {"review": review, "eligible": True, "action_id": req["action_id"]}

req는 request_refund가 돌려준 딕셔너리입니다. 처리번호, 결제 금액, 상태를 모델을 거치지 않고 글 끝에 그대로 붙입니다. 완성된 요청서는 이런 모양이 됩니다(따라하기에서 실제로 나온 것입니다).

- 주문번호: HR20260721001 (상품: 쿠션 운동화)
- 결제 금액: 49,800원
- 배송 완료일: 2026-07-25
- 고객 요청 사유: 단순 변심에 따른 환불 요청
[시스템 확인] 승인 요청 A00001 등록 · 결제 49800원 · 상태: 승인대기

위 네 줄은 AI가, 마지막 한 줄은 코드가 썼습니다. 상담원은 AI가 정리한 금액(49,800원)과 코드가 붙인 금액(49800원)이 같은지 그 자리에서 맞춰 볼 수 있습니다.

22장에서 처리번호와 티켓번호를 코드가 [시스템 확인] 줄로 붙였던 것과 같은 생각입니다. 정확해야 하는 값은 코드가 옆에 붙입니다.


4. 좋은 요청서의 두 조건

판단에 필요한 사실이 다 있다

상담원이 이 화면 하나만 보고 결정할 수 있어야 합니다. 주문번호, 상품, 금액, 받은 날짜, 사유. 하나라도 빠져서 다른 화면을 열어 봐야 한다면 요청서가 일을 덜 한 것입니다.

결론만이 아니라 재료가 보인다

요청서 상담원이 할 수 있는 일
"환불 가능" 한 줄 그대로 누르거나, 다른 화면을 열어 처음부터 다시 조회한다
"주문번호 … 금액 49,800원 … 배송 완료일 2026-07-25 … 승인 요청 A00001 등록" 값을 보고 그 자리에서 확인한다

사람의 역할은 결론에 도장을 찍는 것이 아니라 확인하고 결정하는 것입니다. 확인하려면 결론이 아니라 결론이 나온 근거가 보여야 합니다.


5. 요청서의 칸을 누가 채우는가

요청서의 내용은 성격이 셋으로 나뉩니다. 성격에 따라 채우는 쪽을 정합니다.

요청서에 담을 것 누가 채우나 우리 코드에서
주문번호, 상품, 금액, 날짜 같은 값 코드가 데이터에서 가져온다 get_order_status의 결과를 프롬프트에 그대로 넣고, 네 가지를 반드시 쓰게 한다
처리번호, 상태처럼 틀리면 안 되는 사실 코드가 직접 붙인다 [시스템 확인] 승인 요청 A00001 등록 · 결제 49800원 · 상태: 승인대기
고객의 말을 읽고 정리한 설명 AI가 쓴다 "단순 변심에 따른 환불 요청" 같은 사유 줄

모델이 쓰는 글은 실행할 때마다 문장이 달라집니다. 그래도 괜찮은 이유는, 달라지면 안 되는 것들을 코드 쪽에 두었기 때문입니다. 요건을 채웠는지는 코드가 판정했고, 처리번호와 상태는 코드가 붙였고, 승인 게이트로 갈지 말지도 코드의 판정이 정합니다.


핵심 정리

  • 요청서는 interrupt로 내보내는 값이고, 상담원이 보는 화면의 내용입니다.
  • AI에게는 읽는 사람, 길이, 반드시 넣을 값 네 가지를 정해 주고, 판정은 맡기지 않습니다.
  • 처리번호·금액·상태는 코드가 [시스템 확인] 줄로 직접 붙입니다.
  • 좋은 요청서는 한 화면으로 판단할 수 있고, 결론과 함께 재료를 보여 줍니다.
  • 사람의 역할은 도장이 아니라 확인하고 결정하는 것입니다.
← 이전 절interrupt — 그래프를 멈추고 이어 가기다음 절 →따라하기 — 환불 승인 게이트
오명운 · macro@prag-ai.com