~/ai-dev.md
AI_DEV

사용자 확인 단계는 왜 채팅 UX가 아니라 권한 프로토콜일까

10분 읽기·2026.07.31·출처 4·00
오늘의 질문

에이전트가 도구 실행 중 사용자에게 물어봐야 할 정보나 승인을 만나면, 그냥 채팅창에 질문을 던지면 충분할까?

ai-dev.md
Human approval

사용자 확인 단계는 왜 채팅 UX가 아니라 권한 프로토콜일까

1. 왜 지금 봐야 하나

에이전트형 앱을 만들면 곧 이런 상황을 만난다. 모델이 일정 변경 도구를 호출하려는데 참석자 이메일이 빠져 있다. 환불 처리 도구를 호출하려는데 “정말 취소할까요?”라는 승인이 필요하다. 내부 문서 검색 MCP 서버가 추가 정보를 사용자에게 요구한다. 개발자는 보통 채팅 UI에 “이메일을 알려주세요”라고 묻거나, 도구 실행 전에 확인 모달을 하나 붙인다.

하지만 에이전트가 도구를 쓰기 시작하면 이 문제는 단순 UX가 아니다. 누가 요청했는가, 어떤 도구 실행의 일부인가, 어떤 스키마의 값을 받는가, 사용자가 거절하거나 닫으면 상태를 어떻게 복구하는가, 이 입력이 민감정보인지 아닌지를 남겨야 한다. 그렇지 않으면 에이전트는 친절해 보이지만 운영에서는 재현 불가능한 블랙박스가 된다.

MCP 2025-06-18 사양의 Elicitation은 이 지점을 명확히 보여준다. 서버는 클라이언트를 통해 사용자에게 추가 정보를 요청할 수 있고, 응답은 JSON Schema로 검증되는 구조화 데이터다. 동시에 사양은 서버가 elicitation으로 민감정보를 요청해서는 안 되며, 클라이언트는 어떤 서버가 왜 정보를 요구하는지 분명히 보여주고 사용자가 언제든 거절할 수 있게 해야 한다고 말한다. 즉 “질문하기”가 아니라 클라이언트가 통제하는 구조화된 사용자 입력 요청이다.

OpenAI Agents SDK의 human-in-the-loop 문서도 비슷한 원리를 다른 방향에서 설명한다. 도구 호출이 승인을 요구하면 실행이 멈추고 interruptions가 반환되며, 같은 RunState에서 나중에 재개한다. 승인 표면은 현재 top-level agent에만 묶이지 않고 handoff나 nested agent.asTool() 실행 안쪽에서 발생한 도구 승인도 바깥 run의 interruption으로 올라온다.

Claude Code hooks 문서는 더 운영적인 형태를 보여준다. PreToolUse, PermissionRequest, Elicitation, ElicitationResult 같은 생명주기 이벤트에서 훅이 JSON 입력을 받고, 허용·차단·수정 같은 결정을 반환할 수 있다. 이 흐름은 “사람에게 물어보기”가 에이전트 런타임의 권한·감사·상태 머신과 연결되어야 함을 보여준다.

오늘의 원리는 이렇다. 사용자 확인 단계는 채팅 UX가 아니라 에이전트 실행을 잠시 멈추고, 검증 가능한 입력·승인·거절·재개 상태를 남기는 권한 프로토콜이다.

2. 핵심 개념

에이전트가 사용자에게 다시 묻는 상황은 크게 두 종류다.

첫째, 정보 보충이다. 도구를 실행하려면 빠진 값이 필요하다. 예를 들어 “회의를 잡아줘”라는 요청에는 날짜, 참석자, 시간대가 빠질 수 있다. 일반 챗봇이라면 자연어로 되묻고 답을 대화 히스토리에 넣으면 된다. 하지만 도구 기반 에이전트에서는 어떤 필드가 필요한지 스키마가 있고, 응답이 그 스키마를 만족하는지 검증해야 한다. MCP Elicitation이 JSON Schema 기반 구조화 응답을 말하는 이유가 여기에 있다.

둘째, 권한 승인이다. 도구 인자는 이미 충분하지만, 실행 자체가 위험하거나 되돌리기 어렵다. 주문 취소, 파일 삭제, 결제, 이메일 발송, 프로덕션 DB 변경처럼 실제 세계에 영향을 주는 작업이다. 이때 필요한 것은 추가 정보가 아니라 “이 입력으로 이 도구를 실행해도 되는가”라는 승인이다. OpenAI Agents SDK의 interruptions와 Claude Code의 PermissionRequest는 이 경우를 모델 응답 중간에 끼워 넣는 게 아니라 run 상태를 멈추고 재개하는 흐름으로 다룬다.

이 둘을 섞으면 장애가 생긴다. 정보 보충을 승인처럼 처리하면 사용자는 불필요하게 무거운 결정을 해야 한다. 반대로 위험한 실행을 정보 보충처럼 처리하면 “네”라는 애매한 답이 실제 권한으로 해석된다. 그래서 에이전트 설계에서는 사용자 개입을 최소한 아래 네 필드로 구분해 기록해야 한다.

  • 요청 종류: missing_info, approval, policy_review, re-authentication 중 무엇인가
  • 요청 주체: 어떤 agent, 어떤 MCP server, 어떤 tool call이 요청했는가
  • 요청 스키마: 어떤 필드와 타입을 기대하는가
  • 사용자 결과: accept, decline, cancel, timeout 중 무엇인가

특히 cancel과 decline은 다르다. decline은 사용자가 명시적으로 거절한 것이고, cancel은 창을 닫았거나 응답하지 않은 것이다. MCP Elicitation 사양도 accept, decline, cancel 상태를 구분한다. 제품 로직에서는 decline이면 대안을 제안할 수 있고, cancel이면 나중에 다시 묻거나 현재 작업을 보류할 수 있다.

3. 최신 이슈와 연결

최근 에이전트 플랫폼들이 공통으로 강조하는 변화는 “모델이 알아서 잘 묻는다”가 아니라 런타임이 개입 지점을 표준화한다는 점이다.

MCP Elicitation은 서버가 사용자와 직접 대화하는 구조가 아니다. 서버는 클라이언트에 요청하고, 클라이언트가 사용자 경험과 데이터 공유를 통제한다. 이것은 신뢰 경계를 바꾼다. MCP 서버가 외부 벤더나 플러그인일 수 있다면, 그 서버가 사용자에게 마음대로 비밀번호나 토큰을 요구하게 두면 안 된다. 그래서 사양은 민감정보 요청 금지, 사용자 승인 제어, 서버 표시, rate limiting, 스키마 검증을 보안 고려사항으로 둔다.

MCP Authorization은 같은 문제를 transport 레벨에서 다룬다. HTTP 기반 MCP 서버는 OAuth 2.1 흐름을 사용할 수 있고, 클라이언트는 서버의 authorization server discovery와 token audience validation을 지켜야 한다. 중요한 점은 사용자 입력 요청과 인증 토큰 전달을 혼동하지 않는 것이다. 사용자가 어떤 값을 입력한다고 해서 그것이 서버에 줄 자격증명은 아니다. 토큰은 audience가 맞아야 하고, MCP 서버는 자신을 위해 발급되지 않은 토큰을 받아들이거나 중계해서는 안 된다.

OpenAI Agents SDK의 human-in-the-loop 흐름은 nested agent에서 특히 중요하다. 겉으로는 “여행 계획 agent” 하나를 실행했지만 내부에서는 항공권 검색 agent, 결제 agent, 캘린더 agent가 도구로 연결될 수 있다. 이때 안쪽 도구가 승인을 요구하면 승인 UI는 바깥 run에 interruption으로 올라온다. 제품 입장에서는 “어느 하위 agent가 어떤 인자로 어떤 tool을 실행하려 했는지”를 보여줘야 사용자가 의미 있는 결정을 할 수 있다.

Claude Code hooks는 개발 도구 환경에서 같은 원리를 더 세밀하게 드러낸다. PreToolUse는 실행 전 차단 지점이고, PermissionRequest는 권한 결정 지점이며, Elicitation과 ElicitationResult는 MCP 서버의 사용자 입력 요청 전후를 훅으로 관찰할 수 있는 지점이다. 훅은 단순 알림이 아니라 JSON 입력을 검사하고 차단·허용·수정 결정을 반환할 수 있다. 즉 사용자 개입은 프롬프트 문장이 아니라 런타임 이벤트다.

4. 개발자 관점 해석

AI 기능을 만들 때 흔한 실수는 “사용자에게 물어보면 안전하다”고 생각하는 것이다. 실제로는 질문이 많아질수록 사용자는 습관적으로 승인하고, 모델은 승인 문맥을 넓게 해석하며, 로그에는 무엇이 승인되었는지 남지 않는다.

첫 번째 실패 모드는 모호한 승인이다. “진행할까요?”라는 질문에 사용자가 “네”라고 답했다. 그런데 무엇을 진행한다는 뜻인가? 특정 이메일을 보내는 것인가, 초안만 만드는 것인가, 모든 첨부파일을 포함하는 것인가? 승인 요청에는 tool name, 주요 인자, 영향 범위, 되돌릴 수 있는지 여부가 포함되어야 한다.

두 번째 실패 모드는 스키마 없는 입력이다. 사용자가 “다음 주 초쯤”이라고 답했는데 도구는 ISO 날짜를 기대한다. 모델이 중간에서 해석하면 편하지만, 나중에 장애를 재현하기 어렵다. 정보 보충 단계에서는 사용자의 원문과 파싱된 구조화 값을 분리하고, 파싱 실패 정책을 정해야 한다.

세 번째 실패 모드는 민감정보 우회다. 외부 MCP 서버가 elicitation으로 “API 키를 입력하세요”라고 묻는다면, 사용자는 클라이언트 UI를 믿고 값을 줄 수 있다. MCP 사양이 민감정보 요청을 금지하는 이유다. 비밀번호, OAuth 토큰, 결제 인증, 주민번호 같은 값은 elicitation이 아니라 별도 인증·비밀 관리 흐름으로 처리해야 한다.

네 번째 실패 모드는 재개 상태 손실이다. 에이전트 실행이 승인 대기 상태에서 서버 재시작, 브라우저 새로고침, 모바일 앱 백그라운드 전환을 겪을 수 있다. run state, pending interruption id, tool call id, requested_at, expires_at을 저장하지 않으면 사용자가 나중에 승인해도 어떤 실행을 이어야 하는지 알 수 없다.

다섯 번째 실패 모드는 하위 agent 책임 은닉이다. 멀티 에이전트 구조에서는 바깥 agent 이름만 보여주면 부족하다. 사용자가 승인해야 하는 것은 “AI가 뭔가 함”이 아니라 “calendar_agent가 create_event를 이 참석자 목록과 시간으로 실행함”이다. nested approval이 바깥 run에 올라오더라도 UI와 로그는 내부 경로를 표시해야 한다.

5. 내 프로젝트에 적용할 체크포인트

에이전트 앱에 사용자 확인 단계를 넣을 때는 다음을 체크하자.

  1. 질문 종류를 먼저 분류했는가?

    • 빠진 값을 받는 것인지, 위험한 실행을 승인받는 것인지, 정책 검토인지 구분한다.
  2. 스키마가 있는가?

    • missing info는 JSON Schema, Zod, Pydantic 등으로 기대 필드를 명시한다.
    • 자유 텍스트를 받더라도 원문과 정규화 값을 분리한다.
  3. 승인 요청에 영향 범위가 보이는가?

    • tool name, 주요 인자, 대상 리소스, side effect, 되돌리기 가능 여부를 보여준다.
  4. decline, cancel, timeout을 다르게 처리하는가?

    • 거절은 대안 제안, 취소는 보류, timeout은 만료와 재시도 정책으로 나눌 수 있다.
  5. 민감정보 요청 차단 규칙이 있는가?

    • MCP server나 plugin이 elicitation으로 토큰·비밀번호·개인식별정보를 요구하면 차단한다.
  6. run state를 저장하는가?

    • interruption id, tool call id, agent path, requested schema, created_at, expires_at, user decision을 저장한다.
  7. 자동 승인과 수동 승인의 경계를 정했는가?

    • 읽기 전용 검색은 자동, 외부 발송·삭제·결제·권한 변경은 수동처럼 정책을 명확히 둔다.

6. 오늘 10분 액션

오늘은 새 기능을 만들지 말고, 기존 AI 기능 하나의 “사용자 개입 카드”를 작성해보자.

1분: 최근 만든 에이전트 또는 LLM 기능에서 사용자에게 다시 묻는 지점을 하나 고른다.

3분: 그 지점을 아래 표로 분류한다.

항목적을 내용
intervention_typemissing_info / approval / policy_review / re_auth
requesteragent 이름, MCP server 이름, tool 이름
expected_input필요한 필드와 타입
risk외부 발송, 삭제, 결제, 개인정보 접근 등
allowed_outcomesaccept / decline / cancel / timeout
resume_key어떤 run/tool call을 이어갈지 식별자

3분: 사용자에게 보여줄 문장을 바꾼다. “진행할까요?” 대신 “calendar_agent가 create_event를 실행해 7월 31일 15:00에 외부 참석자 2명에게 초대를 보냅니다. 승인할까요?”처럼 쓴다.

3분: 로그에 남길 JSON을 만든다.

zsh — 생존확인.sh
{
  "intervention_type": "approval",
  "agent_path": ["planner_agent", "calendar_agent"],
  "tool_name": "create_event",
  "risk": ["external_invite", "calendar_write"],
  "decision": "accept",
  "resume_key": "run_.../tool_call_..."
}

이 작은 카드 하나만 만들어도 “사용자에게 물어봤다”가 “검증 가능하고 재개 가능한 권한 프로토콜”로 바뀐다.

7. 더 볼 자료

중복 회피 메모

로컬 content/generated와 Supabase 최근 ai_dev 글을 확인했다. 최근 글은 MCP Sampling의 역방향 권한 요청, tool calling 실행 경계, fine-grained tool streaming의 JSON 검증 지점, 멀티 에이전트 실행 트리, 에이전트 평가 harness, structured outputs, LLM 배칭 스케줄러를 다뤘다. 이번 글은 MCP Sampling이나 일반 도구 권한의 반복이 아니라, 에이전트 실행 중 사용자에게 정보를 요청하거나 승인을 받는 elicitation/human-in-the-loop 단계를 구조화 입력, accept·decline·cancel 상태, run 재개 키, 민감정보 차단 규칙을 가진 권한 프로토콜로 설계하는 데 집중한다.

오늘 10분 액션+5%

댓글 0

최신순 ▾
한 줄 남기기

혹시 이 글을 읽는 동료 개발자가 있다면, GitHub으로 로그인하고 한 줄 흔적을 남겨줘요. (스팸 방지용 로그인이에요)