에이전트가 메시지를 보내기 전 확인 절차를 추가한 방법
요약
에이전트가 메시지를 자동 발송할 때 발생할 수 있는 위험을 방지하기 위해 초안 작성과 전달 단계를 분리하는 승인 루프(approval loop) 설계 방식을 소개합니다. 인간의 명시적 검토를 거치는 Human-in-the-loop 구조를 통해 에이전트의 실행 안전성을 확보하는 방법을 다룹니다.
핵심 포인트
- 초안 작성(Drafting)과 전달(Delivery) 단계를 분리하여 실행 위험 최소화
- 승인 대기, 승인/거절, 전송/취소 등 4단계 상태 머신 도입
- 최소 권한 원칙을 적용하여 전달 컴포넌트만 메시징 자격 증명 사용
- 메시지 해시를 포함한 내구성 있는 승인 레코드 관리
How We Added Confirmation Before an Agent Sends a Message — Agent Lab Journal
Agent Lab Journal
Guides
...
Case study · Agent safety
에이전트가 메시지를 보내기 전 확인 절차를 추가한 방법
Intermediate
7 min read
Safe approval loop
에이전트가 준비한 메시지가 소유자의 이름으로 자동 발송되어서는 안 됩니다. 우리는 초안 작성 (drafting)과 전달 (delivery)을 분리하고, 그 사이에 명시적이고 검증 가능한 인간의 의사결정 단계를 배치했습니다.
문제는 텍스트 생성이 아니었습니다
우리의 에이전트는 대화 문맥 (context)으로부터 유용한 Telegram 답장을 구성할 수 있었습니다. 위험한 부분은 그 이후에 발생했습니다. 동일한 실행 경로 (execution path)가 전송 작업 (send operation)에도 접근할 수 있었기 때문입니다. 따라서 잘못된 수신자, 오래된 초안, 오해된 지시 사항, 또는 소스 자료 내의 악의적인 파편이 외부 행동으로 즉시 이어질 수 있었습니다.
이는 승인 루프 (approval loop) 문제입니다. 초안 작성은 되돌릴 수 있지만, 전송은 되돌릴 수 없습니다. 메시지가 다른 사람에게 도달하면, 이를 삭제하더라도 스크린샷, 알림, 자동화된 처리 또는 평판 손상을 되돌리지 못할 수 있습니다.
안전 요구 사항은 명확했습니다:
소유자가 정확한 텍스트, 정확한 목적지를 검토하고 해당 특정 버전을 명시적으로 승인하기 전까지는 에이전트가 생성한 메시지를 발송해서는 안 된다.
중요한 단어는 "정확한 (exact)"과 "특정 (specific)"입니다. 이전 대화에서 나온 일반적인 "네"라는 답변이 나중에 작성된 초안을 승인해서는 안 되며, 한 수신자에 대한 승인이 다른 수신자에게 재사용되어서도 안 됩니다.
구체적인 사례
워크플로우는 수신된 Telegram 메시지에 응답이 필요할 때 시작되었습니다. 에이전트는 대화 식별자 (identifier)와 답장을 제안하기에 충분한 최근 문맥 (context)을 전달받았습니다. 이전에는 핸들러 (handler)가 생성 직후 바로 Telegram 전송 메서드 (send method)를 호출할 수 있었습니다.
우리는 흐름을 네 가지 별개의 상태로 변경했습니다:
-
Drafted (초안 작성됨): 에이전트가 텍스트를 생성하지만 이를 전달할 권한은 없습니다.
-
Pending approval (승인 대기 중): 소유자가 수신자, 초안 및 승인 컨트롤을 확인합니다.
-
Approved or rejected (승인 또는 거절됨): 소유자가 명시적인 결정을 내립니다.
-
Sent or cancelled (전송 또는 취소됨): 별도의 전달 컴포넌트(delivery component)가 승인되고 변경되지 않은 요청만을 실행합니다.
이러한 분리는 중대한 행동(consequential action) 주변에 인간 참여형(human-in-the-loop) 경계를 설정합니다. 또한 최소 권한 원칙(principle of least privilege)을 따릅니다. 즉, 초안 작성 컴포넌트는 대기 중인 레코드를 생성할 수 있지만, 메시징 자격 증명(messaging credential)을 사용할 수 있는 것은 오직 전달 컴포넌트뿐입니다.
승인 레코드 (The approval record)
대기 중인 승인은 내구성이 있어야 합니다. 이를 모델 컨텍스트(model context)나 프로세스 메모리에만 유지하면 재시작 시 안전하지 않으며, 실제로 무엇이 승인되었는지 판단하기 어렵습니다.
우리는 다음과 같은 필드와 동등한 레코드를 사용했습니다:
{
"id": "generated-random-id",
"status": "pending",
...
메시지 해시(message hash)는 승인을 콘텐츠와 목적지 모두에 결합합니다. 텍스트만 해싱하지 말고 정형화된 페이로드(canonical payload)를 해싱하세요:
payload = channel + "\n" + recipient_id + "\n" + message_text
message_hash = SHA256(payload)
수신자나 텍스트가 변경되면 해시가 변경되어 이전 승인은 무효화됩니다. 이 경우 레코드는 다시 대기(pending) 상태로 돌아가며 반드시 다시 검토되어야 합니다.
실제 구현 (Practical implementation)
1. 에이전트의 도구 세트에서 전송 기능 제거
가장 강력한 통제는 아키텍처 차원에서 이루어집니다. 단순히 에이전트에게 먼저 물어보라고 프롬프트(prompt)를 주는 것에 그치지 마세요. 프롬프트는 행동 지침일 뿐, 권한 경계(authorization boundary)가 아닙니다.
초안 작성 작업(draft operation)을 노출하세요:
{
"name": "create_message_draft",
"description": "소유자 검토를 위한 대기 중인 메시지를 생성합니다. 전송하지 않습니다.",
...
실제 제공자 토큰(provider token)은 에이전트 런타임(agent runtime) 외부에 유지하세요. 초안 서비스는 데이터베이스 접근 권한은 필요하지만, Telegram 전송 자격 증명은 필요하지 않습니다.
2. 어떤 일이 일어날지 보여주기
승인 화면이나 소유자 알림에는 다음 내용이 표시되어야 합니다:
-
채널 및 명확한 수신자;
-
숨겨진 잘림(truncation)이 없는 전체 메시지;
-
해당되는 경우 첨부 파일, 서식(formatting), 답장 대상 및 링크 미리보기;
-
초안이 생성된 시점과 만료되는 시점;
-
별도의 승인(Approve), 수정(Edit), 거절(Reject) 작업.
소유자가 Enter를 누르거나, 대화 상자를 닫거나, 응답하지 않을 때 승인을 기본값으로 설정하지 마십시오. 응답이 없다는 것은 초안이 만료될 때까지 대기(pending) 상태로 유지됨을 의미합니다.
3. 결정 사항 인증 (Authenticate the decision)
승인 콜백(callback)은 소유자의 검증된 계정과 연결되어야 합니다. 콜백 데이터가 자신의 인터페이스에서 생성되었더라도 신뢰할 수 없는 입력값으로 취급하십시오.
무작위의 일회용 토큰 또는 승인 ID, 의도된 소유자, 만료 시간을 포함하는 서명된 값(signed value)을 사용하십시오. 수신 시 서버는 소유자 신원, 서명, 만료 여부, 현재 레코드 상태 및 메시지 해시(hash)를 검증해야 합니다.
POST /approvals/{approval_id}/approve
Authorization: Bearer <owner-session>
If-Match: "<message-hash>"
요청의 유효성 여부는 클라이언트가 아닌 서버가 결정해야 합니다.
4. 상태 전이를 원자적(atomic)으로 수행
두 번의 승인 클릭이나 동시에 실행되는 워커(worker)가 두 번의 전송을 발생시켜서는 안 됩니다. 원자적 조건 업데이트(atomic conditional update)를 사용하십시오:
UPDATE message_approvals
SET status = 'approved',
approved_by = :owner_id,
...
정확히 하나의 행(row)만 변경된 경우에만 계속 진행하십시오. 이는 승인 경계에서의 멱등성(idempotency) 보호 장치입니다.
5. 별도의 워커를 통해 전달
전달 워커는 승인된 레코드를 선택하고, 해시를 다시 검증하며, Telegram에 연락하기 전에 하나의 레코드를 점유(claim)합니다:
pending -> approved -> sending -> sent
\-> failed
제공자(provider)가 지원하는 경우 승인 ID에서 파생된 고유한 멱등성 키(idempotency key)를 사용하십시오. 지원하지 않는 경우, 제공자의 응답을 즉시 저장하고 보수적으로 재시도 동작을 설계하십시오. 불확실한 타임아웃(timeout) 이후에는 맹목적으로 재전송하지 마십시오. 첫 번째 요청이 성공했을 수도 있습니다.
권장 정책 구성
설정(configuration)에 규칙을 유지하면 경계가 명확해지고 검토가 가능해집니다:
outbound_messages:
default_mode: require_approval
approvers:
...
자동 전송 예외 사항을 처음부터 설정하지 않는 것을 권장합니다. 향후 워크플로(workflow)에서 실제로 예외가 필요한 경우, 모델의 신뢰도(confidence)가 아니라 작업 유형(action type)과 목적지(destination)를 기반으로 좁은 범위의 허용 목록(allowlist)을 정의하십시오. 신뢰도는 권한 부여(authorization)가 아닙니다.
검증 체크리스트 (Verification checklist)
우리는 생성된 문장이 합리적으로 보이는지 판단하는 대신, 상태 전이(state transitions)와 권한(permissions)을 확인하여 제어 기능을 검증했습니다. 반복 가능한 테스트 계획에는 다음 사항이 포함되어야 합니다:
- 새로운 초안(draft)은 제공자 전송 엔드포인트(provider send endpoint)를 호출할 수 없다.
- 인증되지 않은 사용자는 초안을 승인할 수 없다.
- 다른 인증된 사용자는 소유자의 초안을 승인할 수 없다.
- 문자 하나, 첨부 파일, 채널 또는 수신자를 변경하면 승인이 무효화된다.
- 만료된 초안은 승인 상태로 이동할 수 없다.
- 두 개의 동시 승인 요청은 하나의 성공적인 전이(transition)로 이어진다.
- 반복되는 전달 작업(delivery jobs)이 이미 승인된 동일한 레코드를 의도적으로 두 번 전송하지 않는다.
- 거부된 초안은 절대 전송될 수 없다.
- 서비스 재시작 시 대기 중(pending), 승인됨(approved), 전송됨(sent) 상태가 올바르게 유지된다.
- 로그는 자격 증명(credentials)이나 불필요한 개인 콘텐츠를 노출하지 않고 결정 사항을 기록한다.
유용한 수동 테스트 방법은 미리보기(preview)와 승인(approval) 사이에 일시 중지한 후, 일반적인 애플리케이션 경로를 통해 데이터베이스 레코드를 편집하고 저장된 해시(hash)가 더 이상 일치하지 않는지 확인하는 것입니다. 전달 워커(delivery worker)는 요청을 거부하고 새로운 승인을 요구해야 합니다.
우리가 설계한 실패 사례 (Failure cases we designed for)
소유자가 잘못된 채팅을 승인하는 경우
표시 이름(display name)만으로는 모호할 수 있습니다. 채널, 채팅 유형 및 식별 가능한 수신자 라벨과 같이 안정적인 컨텍스트(context)를 보여주십시오. 내부 수신자 식별자(recipient identifier)를 해시에 결합하십시오.
미리보기 이후 초안이 변경되는 경우
어떠한 편집이든 새로운 버전을 생성하며 상태를 대기 중(pending)으로 재설정합니다. 승인된 레코드를 제자리에서(in place) 수정하지 마십시오.
승인 버튼을 두 번 클릭하는 경우
두 번째 요청은 또 다른 전달(delivery)을 예약하지 않고 기존 상태를 반환해야 합니다. 조건부 업데이트(Conditional updates)와 고유한 전달 레코드(unique delivery record)를 통해 중복 작업을 방지합니다.
제공자(provider)의 타임아웃 발생
타임아웃은 알 수 없는 결과(unknown result)이지, 실패의 증거가 아닙니다. 이를 별도로 표시하고, 요청 세부 정보를 유지하며, 가능한 경우 재시도하기 전에 조정(reconcile)하십시오.
소유자가 응답하지 않는 경우
승인이 만료됩니다. 타임아웃 핸들러, 예약된 작업(scheduled task) 또는 폴백 분기(fallback branch)를 통해 승인 상태가 되어서는 안 됩니다.
프롬프트 인젝션(Prompt injection)으로 에이전트에게 검토를 우회하도록 요청하는 경우
에이전트에게는 발송 자격 증명(sending credential)과 승인 전환 능력(approval transition capability)이 없으므로 요청은 실패합니다. 이것이 시스템 프롬프트의 문구보다 권한 경계(permission boundary)가 더 중요한 이유입니다.
한계
승인 루프(approval loop)는 승인되지 않은 발송을 줄여주지만, 소유자가 모든 사실적, 법적, 개인정보 보호 또는 어조(tone) 문제를 인지할 것이라고 보장하지는 않습니다. 미리보기(preview)는 의미 있는 결정을 내릴 수 있도록 충분한 컨텍스트를 제공해야 하며, 고위험 메시지는 추가 검토가 필요할 수 있습니다.
또한 이 패턴은 지연 시간(latency)과 인터페이스 작업을 유발합니다. 지연 자체가 위험한 긴급 자동화(emergency automation)에는 부적합할 수 있습니다. 그러한 시스템에는 엄격하게 범위가 지정된 작업, 사전 정의된 목적지, 모니터링 및 명시적인 위험 결정이 포함된 별도로 설계된 정책이 필요합니다.
마지막으로, 외부 제공자가 요청을 수락했지만 응답을 분실하는 경우, 정확히 한 번 전달(exactly-once delivery)을 항상 보장할 수는 없습니다. 로컬 상태 머신(local state machine)은 일반적인 중복은 방지할 수 있지만, 모호한 네트워크 실패는 여전히 조정(reconciliation) 또는 제공자가 지원하는 멱등성 메커니즘(idempotency mechanism)을 필요로 합니다.
결과적인 안전 경계
최종 설계는 하나의 규칙을 강제할 수 있게 만들었습니다: 에이전트는 제안하고, 소유자는 승인하며, 별도의 워커(worker)가 실행합니다. 승인은 인증되고, 만료되며, 하나의 불변 페이로드(immutable payload)에 적용되고, 재생(replay)될 수 없습니다.
이 패턴은 Telegram 이외의 분야에서도 유용합니다. 동일한 경계(boundary)가 이메일, 캘린더 초대, 고객 지원 답변, 이슈 댓글, 구매, 문서 공유, 그리고 개인의 이름으로 수행되는 기타 모든 작업에도 적용됩니다.
Agent Lab Journal 가이드의 구현 패턴(implementation patterns)을 계속 확인하거나, 용어 사전(glossary)에 링크된 개념들을 검토해 보세요.
© Agent Lab Journal
원문 기사: https://agentlabjournal.online/en/agent-telegram-approval-case.html
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기