day5_practice/hitl_flow.py

HITL 3갈래 흐름 플레이어

LLM이 고른 도구가 위험 도구면 실행 직전에 멈추고, 사람이 승인 / 수정 / 거절 중 하나를 고르면 그대로 이어서 실행하는 에이전트입니다. 아래 플레이어에서 ▶를 누르면 그래프, 소스 코드, State가 한 단계씩 같이 움직입니다. 메시지와 출력은 모두 실제 실행 결과입니다.

준비 1차 invoke ⏸ 사람 대기 2차 invoke (resume) 끝
route → "gate" route → "tools" route → "end" → agent → agent 🙋 사람 approve·modify·reject 💾 체크포인트 checkpoints.sqlite 저장! START agent LLM이 도구 결정 gate 위험 도구 승인 관문 tools 안전 도구 바로 실행 END

🎒 State["messages"]

add_messages로 누적

🖥️ 터미널 출력

hitl_flow.py

한눈에: interrupt_basic.py와 뭐가 다른가

🤖 멈출지는 LLM의 선택에 달림

interrupt_basic.py는 항상 멈췄습니다. 여기서는 LLM이 어떤 도구를 부르려 하느냐에 따라 route()가 길을 나눕니다. 위험 도구(RISKY_TOOLS)면 gate, 조회 도구면 tools로 갑니다.

🚦 실행 직전에 멈춤

LLM의 tool_calls는 "실행하고 싶다"는 요청일 뿐입니다. 실제 실행은 gate 안의 TOOL_MAP[...].invoke()에서 일어나므로, 그 앞에 interrupt()를 두면 사람이 보기 전에는 아무것도 실행되지 않습니다.

🔀 대답이 3가지

resume 값이 단순 yes/no가 아니라 dict입니다. {"decision": "approve" | "modify" | "reject", ...}. 수정은 인자를 바꿔 실행하고, 거절은 실행하지 않습니다.

🔁 결과는 다시 LLM에게

세 경우 모두 gate가 ToolMessage를 돌려주고 agent로 돌아갑니다. LLM이 그 결과를 읽고 사용자에게 최종 답을 씁니다.

gate 노드의 3갈래 비교 (실제 실행 결과)

갈래resume 값도구 실행gate가 돌려준 ToolMessageLLM 최종 답 (요약)
승인 {"decision": "approve"} 원래 인자 그대로
budget=800000
출장 신청서 제출 완료: 부산 2일, 예산 800,000원 신청 완료, 예산 80만 원
수정 {"decision": "modify", "args": {"budget": 500000}} 인자 병합 후 실행
budget=500000
제출 완료: 예산 500,000원 + "(사용자가 인자를 수정했습니다. 최종 실행값은 … 이 값으로 안내하세요.)" 신청 완료, 예산 500,000원
거절 {"decision": "reject", "reason": "분기 예산이 이미 소진되었음"} 실행 안 함 사용자가 실행을 거절했습니다. 사유: 분기 예산이 이미 소진되었음. 실행하지 말고 대안을 안내하세요. 거절 안내 + 대안 3가지 (다음 분기, 예산 재검토, 규정 확인)

꼭 이해할 포인트 3개

① 거절해도 ToolMessage를 돌려주는 이유

LLM이 tool_calls를 냈으면, 다음 LLM 호출 전에 그 호출 id마다 짝이 되는 ToolMessage(tool_call_id=call["id"])가 있어야 합니다. 짝이 없으면 Bedrock(Anthropic) API가 "tool_use에 대한 tool_result가 없다"며 요청을 거부합니다. 그래서 실행하지 않아도 "거절됐다"는 결과를 도구 결과 자리에 넣습니다.

② 수정 시 note를 붙이는 이유

LLM은 자기가 처음 낸 인자(80만 원)를 기억합니다. 결과에 "예산 500,000원"만 있으면 모델이 원래 요청값을 다시 말할 수 있습니다. 그래서 "최종 실행값은 이것이니 이 값으로 안내하라"는 문장을 결과에 붙입니다(코드 87~90줄 주석). 실제 실행에서도 최종 답이 500,000원으로 나왔습니다.

③ 재개 = gate 노드를 처음부터 다시

2차 invoke에서 gate는 63줄부터 다시 실행됩니다. 이번에는 interrupt()가 멈추지 않고 resume 값을 리턴합니다. 그래서 interrupt() 앞에는 부작용 있는 코드를 두면 안 되고, 실제 도구 실행(92줄)이 interrupt() 뒤에 있습니다.

보너스: tools 노드는 언제 쓰나

세 시나리오 모두 위험 도구라 tools 노드를 지나지 않습니다. 플레이어의 "보너스: 규정 조회" 탭은 같은 그래프에 조회 질문을 넣어 본 실제 결과입니다. 승인 없이 tools에서 바로 실행되고, invoke 한 번으로 끝납니다.

알아둘 점 (코드의 한계)