"구독 정리 좀 해줘." 이 한 줄을 에이전트에게 던지고 잠든 사이, AI가 당신의 OTT를 해지하고, 3년 치 청구서 PDF를 영구 삭제하고, 가족 단톡방에 "내가 다 정리했어"라는 메일까지 보냈다면 — 편리한가, 아니면 등골이 서늘한가?

아마 둘 다일 것이다. 그리고 그 서늘함의 정체는 하나다. 되돌릴 수 없다는 것.

🌱 이 글은 ★★☆ 손에 익히기되돌릴 수 없는 행동 앞에서 루프가 나에게 묻게 하는 승인 게이트를 한 칸 채웁니다. 빈칸으로 ▶를 눌러도 안전(전부 승인 필요가 기본값). 막히면 "정답 보기 ★☆☆" 한 번.

스스로 도는 게 늘 좋은 건 아니다

바로 앞 편(5편 goal-to-agent)에서 우리는 목표 한 줄만 주면 처음부터 끝까지 스스로 도는 에이전트를 손으로 설계했다. 목표를 쪼개고, 도구를 고르고, 결과를 보고 다시 계획하는 — 꽤 근사한 자동화였다. 그런데 그 근사함에는 그림자가 있다. 에이전트가 알아서 잘 도는 만큼, 알아서 잘못 도는 것도 순식간이라는 것.

읽고 조회하는 행동은 아무리 여러 번 해도 겁나지 않는다. 검색 100번, 메일 열어보기 50번 — 잘못해도 원상복구가 필요 없다. 문제는 그다음이다. 메일 발송·파일 삭제·결제. 이 셋은 성격이 다르다. 한 번 누르면 되돌리는 버튼이 없다. 보낸 메일은 회수되지 않고, 지운 파일은 사라지고, 나간 돈은 그냥 나간 돈이다.

여기서 많은 사람이 에이전트 앞에서 얼어붙는다. "알아서 다 해준다"는 말이 반가우면서도, 바로 그 "알아서"가 되돌릴 수 없는 영역까지 침범할까 봐 손을 못 뗀다. 완전히 맡기자니 불안하고, 완전히 내가 하자니 그럴 거면 에이전트를 왜 쓰나 싶고.

이 딜레마의 답은 "전부 맡긴다"도 "전부 내가 한다"도 아니다. 어디까지 맡기고, 어디서 멈춰 물을 것인가 — 그 경계선을 내가 긋는 것이다.

브레이크가 아니라, 핸들

앞선 4편 stop-the-runaway에서 우리는 폭주하는 루프에 멈춤 조건과 토큰 예산을 걸었다. 그건 자동 안전벨트였다 — 정해둔 한계를 넘으면 기계가 알아서 급정거하는 장치. 사람이 지켜볼 필요도 없다.

오늘 다루는 건 결이 다르다. 4편이 브레이크였다면, 6편은 사람이 쥔 핸들이다. 위험한 순간에 자동으로 멈추는 게 아니라, 위험한 순간에 나에게 넘겨주고 내 판단을 기다리는 것. 업계에서는 이걸 HITL(Human-In-The-Loop), 우리말로 "사람이 루프 안에 들어가 있는" 설계라고 부른다.

핵심은 단순하다. 에이전트가 도구를 하나 호출하려 할 때마다, 그 도구에 붙은 위험 태그를 먼저 본다.

태그무슨 행동인가되돌릴 수 있나기본 처리
read검색·조회·목록 스캔✔ 애초에 바꾼 게 없음자동 통과
write쓰기·수정대체로 가능상황에 따라
send메일·메시지 발송❌ 회수 불가승인 요청
delete파일·메일 영구 삭제❌ 복구 불가승인 요청
pay결제·구독 해지❌ 돈은 나간 돈승인 요청

읽기는 흘려보내고, 되돌릴 수 없는 셋(send·delete·pay)은 붙잡아 나에게 묻는다. 이 한 줄의 판단이 오늘의 전부다. 그리고 이 판단을 코드로 옮기면 딱 이런 함수 하나가 된다.

function needsApproval(action) {
  // 이 행동, 사람에게 물어볼까?  true = 멈추고 승인 요청  /  false = 그냥 자동 실행
}

needsApproval — "승인이 필요한가?" 이 함수가 true를 돌려주면 루프는 그 도구를 쓰기 직전에 멈춰 당신에게 팝업을 띄운다. false면 그냥 실행하고 다음으로 넘어간다. 루프의 나머지 — 스스로 계획하고, 도구를 순서대로 부르고, 팝업을 띄우고, 당신이 거부하면 대안을 찾는 부분 — 는 이미 다 돌아가고 있다. 당신이 채울 칸은 이 함수 하나뿐이다.

🔧 지금 해보기 ACT-6-approval-gate ★★☆ · 빈칸으로 눌러도 안전

옆(또는 아래)의 샌드박스에서 "구독 정리 비서"가 이미 돌 준비를 마치고 있다. 당신은 딱 한 칸, needsApproval(action) 안에 "어디서 멈춰 물을지"만 정하면 된다. 겁먹지 말자 — 빈칸으로 둔 채 ▶를 눌러도 안전하다. 비워두면 안전 기본값이 '전부 승인 필요'로 처리해서, 위험한 행동이 당신 확인 없이 실행되는 일은 절대 없다. 정 막히면 "정답 보기 ★☆☆" 버튼 하나로 채워진다.

승인 게이트 샌드박스 · 외부 의존 0 · 부수효과 0(실제로 아무것도 삭제·발송·결제하지 않는 결정적 시뮬레이션). 승인/거부는 실제로 루프를 일시정지·재개합니다.ACT-6-approval-gate

한 줄이 루프를 멈춰 세우는 순간

샌드박스를 열었다면, 두 번의 실험을 꼭 해보자. 이 편의 진짜 손맛은 여기 있다.

첫째, 균형 게이트로 돌려본다. 코드칸에 이 한 줄을 넣거나, "정답 보기 ★☆☆"를 누른다.

return action.risk !== 'read';   // 읽기만 자동, 나머지는 다 물어봄

이제 ▶를 누르면 에이전트가 목표를 향해 스스로 돌기 시작한다. 받은편지함을 검색하고(read), 구독 메일을 열어 확인하고(read) — 이 둘은 팝업 없이 스르륵 지나간다. 그러다 세 번째 단계, OTT 구독을 해지하려는 순간(pay) 루프가 우뚝 멈춘다.

⏸ 에이전트가 잠깐 멈췄습니다
🔧 OTT 구독 해지(₩13,500/월 결제 중단)
⚠ 이 행동은 한 번 하면 되돌릴 수 없습니다.
거부하면 → '검토 목록'에 추가 (되돌릴 수 있는 안전한 대안)

여기서 [거부]를 눌러보자. 에이전트는 억지로 해지를 밀어붙이지 않는다. 대신 "검토 목록에 담아 둠 — 실제 해지는 사람이 나중에 직접"으로 알아서 우회한다. 삭제 앞에서 거부하면 '보관함 이동'으로, 메일 발송 앞에서 거부하면 '초안 저장'으로 돌아간다. 되돌릴 수 없는 길 대신, 되돌릴 수 있는 길을 스스로 찾는 것이다. 이게 바로 "내가 핸들을 쥐었다"는 감각이다 — 루프를 죽인 게 아니라, 위험한 순간에만 방향을 내가 정한 것.

둘째, 일부러 위험하게 돌려본다. 이번엔 코드칸을 이걸로 바꾼다.

return false;   // 아무것도 안 물어봄 = 전자동

false는 "어떤 행동도 승인받지 마"라는 뜻이다. ▶를 누르면 — 팝업이 단 한 번도 뜨지 않는다. 구독 해지도, 파일 삭제도, 메일 발송도 전부 당신 확인 없이 주르륵 실행되고, 화면 위쪽엔 빨간 경고가 뜬다. "⚠ 3개의 되돌릴 수 없는 행동이 당신 확인 없이 실행됨." 이게 완전 자율(전자동)의 민낯이다. 편리함의 끝에서 통제권이 통째로 사라진 순간을, 안전한 시뮬레이션 안에서 딱 한 번 목격해 두는 게 좋다.

화면 가운데의 자율성 다이얼을 보자. 왼쪽 끝은 전자동 🤖(사람 개입 0), 오른쪽 끝은 전수동 🙋(모두 확인), 그 사이가 균형 ⚖️다. return false는 다이얼을 왼쪽 끝까지, 빈칸은 오른쪽 끝까지 밀어붙인다. 그리고 우리가 채운 action.risk !== 'read' 한 줄은 그 다이얼을 딱 좋은 가운데에 세운다. 자율성은 스위치(켜기/끄기)가 아니라 다이얼이다 — 얼마나 맡길지는 당신이 돌리는 만큼 정해진다.

💡 더 깊이 — '멈춤'은 어떻게 진짜 멈춤이 되는가 심화

이 데모의 루프는 async 함수이고, 승인이 필요한 순간 await waitForApproval(action)에서 실제로 실행을 일시정지한 채 당신의 클릭을 기다린다. 승인/거부 버튼이 내부의 약속(Promise)을 완료시켜야 루프가 비로소 다음 줄로 나아간다. "멈춰서 사람을 기다린다"가 UI 흉내가 아니라 코드의 제어 흐름 자체라는 뜻이다.

실제 프로덕션 에이전트 프레임워크도 이 패턴을 interrupt(인터럽트) → resume(재개)라는 이름으로 지원한다. 위험 도구 앞에서 실행 상태를 통째로 얼려 두었다가, 사람의 결정이 들어오면 그 지점부터 이어 돌리는 방식이다.

심화로 파보고 싶다면, needsApproval 안에서 위험 정책을 더 정교하게 짤 수도 있다. 예를 들어 결제는 막되 소액(₩5,000 미만)은 자동으로 통과시키려면, action.label 문자열에서 금액을 뽑아 임계값과 비교하는 식으로 직접 써 넣으면 된다 — 코드칸에 이런 한 덩어리를 넣어보자.

if (action.risk === 'pay') {
  const m = (action.label.match(/₩([\d,]+)/) || [])[1];
  const won = m ? parseInt(m.replace(/,/g, ''), 10) : 999999;
  return won >= 5000;   // ₩5,000 이상 결제만 승인, 소액은 자동
}
return action.risk !== 'read';   // 나머지는 균형 게이트 그대로

"전송은 초안이 대안이니 자동으로 두고 삭제·결제만 승인" 같은 나만의 원칙도 같은 방식으로 얼마든지 확장할 수 있다. 게이트는 한 줄이 아니라 하나의 함수라는 걸 기억하자.

정직하게: 이건 안전한 축소 모형이다

한 가지는 분명히 해두자. 이 데모는 실제로 아무것도 해지하지 않고, 삭제하지 않고, 보내지 않는다. 화면에 흐르는 "구독 해지 완료", "파일 영구 삭제됨", "메일 발송됨"은 전부 브라우저 안에서만 도는 시뮬레이션 로그다. 외부 서버도, API 키도, 진짜 부수효과도 0이다. 그래서 당신은 return false(전자동)의 위험을 아무 대가 없이 목격할 수 있다 — 진짜 계정이었다면 절대 못 할 실험을.

에이전트가 "스스로 계획하는" 것처럼 보이지만, 이 데모의 5단계 계획은 미리 짜둔 고정 시나리오다. 진짜 LLM이 그때그때 계획을 세우게 만드는 법은 5편 goal-to-agent에서 이미 다뤘다. 오늘의 목적은 다른 데 있다 — "위험한 순간에 루프를 멈춰 세우는 승인 게이트의 의미론"을 마찰 0으로 손에 쥐어보는 것. 축소 모형이지만, 멈춤도 인터럽트도 대안 탐색도 진짜와 똑같이 작동한다.

마치며 — 핸들을 쥔 손

에이전트가 무섭게 느껴졌다면, 그건 통제권을 통째로 넘겨야 한다고 착각했기 때문이다. 하지만 오늘 당신은 알았다. 자율과 통제는 둘 중 하나를 고르는 문제가 아니라, 다이얼을 어디에 둘지의 문제다. 읽기는 맡기고, 되돌릴 수 없는 행동 앞에서만 나에게 핸들을 넘기게 하는 것 — 그 경계선을 긋는 데 필요한 건 코드 한 줄이었다.

시나리오를 메일함 정리, 사진 정리로 바꿔가며 같은 게이트가 서로 다른 위험을 어떻게 붙잡는지 확인해 보자. 그리고 당신이 거부해 에이전트가 대안으로 우회한 결과를 #내가멈춘루프 태그로 공유해 보자. "나는 이 행동을 막았다"는 한 줄이, 다음 독자에게는 "나도 핸들을 쥘 수 있구나"가 된다.

그런데 — 이렇게 핸들을 쥐는 법을 배우기까지, 누군가는 이미 데었다. 게이트 없이 루프를 풀어놨다가 하룻밤 새 토큰을, 돈을, 멀쩡한 파일을 태워 본 사람들의 이야기가 다음 편(7편 burned-my-tokens)을 기다린다. 오늘 당신이 끼운 이 한 칸이 왜 필요했는지, 그 사람들의 목소리로 다시 듣게 될 것이다.