에이전트가 이메일을 보내기 전 사람의 승인을 받도록 설정하기
요약
AI 이메일 에이전트의 환각이나 오류로 인한 사고를 방지하기 위해, 에이전트가 직접 메일을 보내는 대신 초안을 작성하고 사람이 승인하는 '승인 대기열(approval queue)' 구축 방법을 소개합니다. Nylas Drafts API를 활용하여 안정적이고 원자적인 승인 프로세스를 구현하는 기술적 이점을 다룹니다.
핵심 포인트
- 에이전트의 직접 발송 대신 '초안 작성 후 승인' 방식 권장
- Nylas Drafts API를 통한 안정적인 승인 대기열 구축
- 프로세스 중단 시에도 데이터가 유지되는 영속성 확보
- 검토자가 익숙한 메일 클라이언트를 그대로 활용 가능
- 승인된 객체가 변형 없이 그대로 발송되는 원자성 보장
대부분의 "AI 이메일 에이전트 (AI email agent)" 데모는 승리감에 도취된 send로 끝납니다. 모델이 답장을 작성하고, 코드가 이를 POST로 전송하면, 실제 메시지가 낯선 사람의 편지함에 도착합니다. 그것은 훌륭한 데모이지만, 끔찍한 프로덕션 (production) 기본 설정입니다. 아무도 지켜보지 않는 상황에서 에이전트가 메일을 보낼 수 있게 되는 순간, 당신은 LLM (Large Language Model)에게 기업 이메일 주소와 이를 사용할 수 있는 상시 권한을 넘겨준 것입니다. 환각 (hallucination)으로 인한 잘못된 가격 안내, 확신에 찬 잘못된 환불 약속, 혹은 잘못된 고객에게 보내는 사과 한 번이면, 당신은 법무팀에 왜 봇이 회사 이름으로 이메일에 서명했는지 설명해야 할 것입니다.
AI가 등장하기 수십 년 전부터 존재했던 지루하지만 내구성이 뛰어난 해결책이 있습니다. 바로 _보내지 말고 — 초안을 작성하는 것(don't send — draft)_입니다. 메시지를 준비하고, 그 앞에 사람을 배치하며, 이름과 맥박이 있는 누군가가 승인했을 때만 전송하십시오. 이메일 시스템에는 정확히 이 이유 때문에 아주 오래전부터 "임시 보관함 (Drafts)" 폴더가 존재해 왔습니다. Nylas Drafts API는 그 폴더를 더 나은 것, 즉 에이전트가 작성하고 검토자가 비워내는 **승인 대기열 (approval queue)**로 바꿔줍니다.
이 포스트는 해당 대기열을 구축하는 방법을 다룹니다. 에이전트는 초안을 생성하고, 사람은 대기 중인 초안을 검토하며, 승인된 초안은 바이트 단위까지 변경 없이 그대로 전송됩니다. 다시 렌더링하거나,
초안은 메일함에 안정적인 id를 가진 _실제 저장된 이메일 객체(real, persisted email object)_입니다. 이는 자체 제작한 버퍼(buffer)가 제공하지 못하는 세 가지 이점을 제공합니다:
- 프로세스 중단 시에도 유지됩니다. 워커(worker)를 재시작하거나, 재배포하거나, 장애 조치(failover)가 발생하더라도 대기 중인 메시지는 메모리 큐에서 사라지지 않고 메일함에 그대로 남아 있습니다.
- 사람이 이미 확인하는 곳에서 확인할 수 있습니다. 초안은 에이전트 메일함의 '임시 보관함(Drafts)' 폴더에 저장됩니다. 검토자는 관리자 패널에서 JSON 미리보기를 보며 눈을 가늘게 뜨는 대신, 일반 메일 클라이언트(Agent Accounts는 IMAP/SMTP를 지원함)에서 이를 열어볼 수 있습니다.
- 승인이 단일한 원자적 작업(atomic action)이 됩니다. 승인된 초안을 보내는 것은 초안의
id를 대상으로 하는 단 한 번의 API 호출입니다. 버퍼에서 수신자, 제목, 본문, 첨부 파일을 다시 조합하며 제대로 되었기를 바랄 필요가 없습니다. 당신이 승인한 바로 그 객체가 전송되는 객체가 됩니다.
마지막 포인트가 핵심입니다. 당신이 원하는 계약 조건은 "사람이 본 것이 그대로 발송되는 것"입니다. 초안은 전송 동작이 콘텐츠를 다시 제공하는 대신 저장된 초안을 참조하기 때문에, 이 조건을 별도의 노력 없이 무료로 제공합니다.
Grant는 당신에게 필요한 유일한 추상화입니다
아래의 모든 작업은 하나의 메일함을 나타내는 grant_id인 grant를 대상으로 실행됩니다. 연결된 Gmail 또는 Microsoft 계정과 함께 Nylas를 사용해 보았다면, Agent Account도 동일한 것, 즉 grant입니다. 동일한 /v3/grants/{grant_id}/* 엔드포인트, 동일한 인증 헤더(auth header), 동일한 페이로드(payload)를 사용합니다. 데이터 평면(data plane)에서 새로 배울 것은 없습니다. 차이점은 프로비저닝(provisioning)에 있습니다. Agent Account는 OAuth 인증 과정이나
설계 방식에 영향을 미치는 중요한 주의사항을 먼저 말씀드리자면, Agent Account는 커스텀 metadata를 지원하지 않습니다. 초안(draft)에 {"approval_status": "pending"}과 같은 태그를 달고 서버 측에서 이를 필터링할 수 없습니다. 따라서 승인 결정 자체(approval decision) — 즉, 누가 승인했는지, 언제 승인했는지, 상태 머신(state machine) 정보 등 — 는 여러분의 데이터베이스에 저장되어야 합니다. Nylas는 메시지를 저장하고, 여러분의 앱은 그 판결(verdict)을 저장합니다. 그 경계가 어디에서 나뉘는지 보여드리겠습니다.
시작하기 전에
다음 사항이 필요합니다:
- 대시보드에서 발급받아
NYLAS_API_KEY로 내보낸 Nylas API 키. - 등록된 발신 도메인 또는 무료
*.nylas.email체험용 서브도메인. 새 도메인은 약 4주에 걸쳐 워밍업(warm up)이 필요하므로 미리 프로비저닝(provisioning)하세요. - 터미널 경로를 사용하려면 Nylas CLI가 필요합니다. API 키를 저장하려면
nylas init을 한 번 실행하세요.
예제에서는 미국 데이터 리전 호스트인 https://api.us.nylas.com을 사용합니다. 앱이 EU에 있다면 api.eu.nylas.com으로 교체하세요.
에이전트의 메일함 프로비저닝하기
에이전트가 초안을 작성하는 메일함은 Agent Account입니다. 내장된 nylas 프로바이더(provider)와 여러분의 도메인에 있는 주소를 사용하여 POST /v3/connect/custom으로 생성하세요:
curl --request POST \
--url 'https://api.us.nylas.com/v3/connect/custom' \
--header "Authorization: Bearer $NYLAS_API_KEY" \
...
응답에는 이후 모든 작업에 사용할 grant_id가 포함됩니다. API는 또한 해당 계정에 대한 기본 워크스페이스(workspace)와 정책(policy)을 자동으로 생성하므로, 메일을 보내기 위한 추가 설정은 필요하지 않습니다.
CLI를 사용하면 이 모든 과정을 하나의 명령어로 압축할 수 있습니다:
nylas agent account create support@yourapp.nylas.email --name "Support Bot"
생성 시 --workspace 플래그는 존재하지 않으며, 관리해야 할 리프레시 토큰(refresh token)도 없습니다. 이것이 OAuth가 필요 없는 부분입니다. 나중에 더 엄격한 커스텀 정책을 적용하고 싶다면(예: 수신 크기 제한 또는 수신자 차단), nylas workspace update <workspace-id> --policy-id <policy-id>를 통해 자동 생성된 워크스페이스에 적용하면 됩니다. 출력 결과에서 grant_id를 가져와 내보내기(export) 하세요:
export NYLAS_GRANT_ID="<grant-id-from-output>"
Step 1 — 에이전트가 전송하는 대신 초안을 작성 (the agent drafts instead of sends)
여기에 핵심적인 반전(inversion)이 있습니다. 에이전트의 "send" 도구는 메시지를 전송하는 엔드포인트(endpoint)를 호출하지 않습니다. 대신 **초안 생성(create draft)**을 호출합니다. 메시지는 작성되고, 영속화되며, 보류됩니다 — 하지만 아무것도 사서함(mailbox) 밖으로 나가지 않습니다.
POST /v3/grants/{grant_id}/drafts를 사용하여 초안을 만드세요. 본문(body)은 메시지를 전송할 때와 동일한 형태입니다: 수신자(recipients), 제목(subject), 본문(body):
curl --request POST \
--url "https://api.us.nylas.com/v3/grants/$NYLAS_GRANT_ID/drafts" \
--header "Authorization: Bearer $NYLAS_API_KEY" \
...
응답(response)은 id를 가진 초안 객체입니다. 이 id가 나머지 워크플로우가 의존하는 핸들(handle)이 됩니다 — 어딘가에 기록해 두세요. _여기가 에이전트가 자율적으로 행동할 수 있는 유일한 지점_이며, 에이전트가 생성한 것은 전송되지 않은 초안뿐입니다.
CLI도 이를 반영하며, grant id를 위치 인자(positional argument)로 사용합니다:
nylas email drafts create "$NYLAS_GRANT_ID" \
--to dana@customer.example \
--subject "Re: Refund for order #4821" \
...
--json (또는 전역 --quiet 플래그)를 추가하여 테이블 대신 깨끗하고 기계가 읽을 수 있는 핸들(handle)을 받으면 유용합니다 — 이는 오케스트레이션 코드(orchestration code)가 CLI로 호출되어 초안 ID를 캡처해야 할 때 유용합니다.
실질적인 참고 사항: 에이전트가 초안을 생성할 때, 사용자의 데이터베이스에 일치하는 행(row)을 작성하세요 — 초안 ID, 제안된 수신자, 어떤 검토자에게 할당되었는지, 그리고 pending 상태를 기록합니다. 이 행이 승인 기록입니다. 사서함의 초안은 페이로드(payload)이고; 이 행은 대기 중인 판결(verdict-in-waiting)입니다. Agent Accounts는 사용자 지정 메타데이터(custom metadata)를 포함하지 않으므로, 이 DB 행은 필수적입니다 —
curl --request GET \
--url "https://api.us.nylas.com/v3/grants/$NYLAS_GRANT_ID/drafts?limit=20" \
--header "Authorization: Bearer $NYLAS_API_KEY" \
...
각 항목은 id, to, subject, body를 포함하며, 이는 검토 화면 (review screen)을 렌더링하기에 충분합니다. 터미널에서 다음을 실행하세요:
nylas email drafts list "$NYLAS_GRANT_ID" --limit 20
nylas email drafts list는 기본적으로 10개의 초안 (drafts)을 가져오지만, --limit 옵션으로 늘릴 수 있습니다. 결정을 내리기 전 하나의 초안을 자세히 읽기 위해 불러오려면 show를 사용하세요:
nylas email drafts show <draft-id> "$NYLAS_GRANT_ID"
또는 API를 통해 GET /v3/grants/{grant_id}/drafts/{draft_id}로 단일 초안을 가져올 수 있습니다:
curl --request GET \
--url "https://api.us.nylas.com/v3/grants/$NYLAS_GRANT_ID/drafts/<DRAFT_ID>" \
--header "Authorization: Bearer $NYLAS_API_KEY" \
...
명확히 짚고 넘어가야 할 두 가지 사항이 있습니다. 첫째, 이 엔드포인트 (endpoint)는 "승인 대기 중인 초안"이 아니라 사서함에 있는 모든 전송되지 않은 초안을 반환합니다. API 자체에는 사용자의 승인 상태 (approval state)에 대한 개념이 없습니다. "누가 무엇을 승인해야 하는지"에 대한 매핑은 1단계에서 만든 DB 행 (DB rows)을 통해 이루어집니다. 초안 목록을 pending 행과 조인 (join)하면 검토자의 작업 목록 (worklist)을 얻을 수 있습니다. 둘째, 검토자가 메시지를 거절하면, 초안을 삭제하고 (DELETE /v3/grants/{grant_id}/drafts/{draft_id} 또는 nylas email drafts delete), 해당 행을 rejected로 표시하세요. 거절된 메시지는 실수로 전송되지 않도록 초안을 남겨두지 않아야 합니다.
Step 3 — 필요 시 사람이 편집함
검토가 항상 찬성 또는 반대만 있는 것은 아닙니다. 때로는 에이전트가 90%까지 완성했을 때, 사람이 숫자 하나를 수정하거나 문장을 부드럽게 다듬고 싶을 수도 있습니다. 초안은 실제 수정 가능한 객체 (mutable object)이므로, 검토자는 승인하기 전에 그 자리에서 바로 (in place) 편집할 수 있으며, 이 편집 내용은 최종적으로 전송되는 내용의 일부가 됩니다.
PUT /v3/grants/{grant_id}/drafts/{draft_id}를 사용하여 초안을 업데이트하세요:
curl --request PUT \
--url "https://api.us.nylas.com/v3/grants/$NYLAS_GRANT_ID/drafts/<DRAFT_ID>" \
--header "Authorization: Bearer $NYLAS_API_KEY" \
...
CLI에는 초안 업데이트 (draft-update) 명령어가 없습니다. nylas email drafts는 생성 (create), 목록 조회 (list), 상세 보기 (show), 전송 (send), 삭제 (delete)를 지원하지만, 수정 (edit)은 지원하지 않습니다. 따라서 제자리 수정 (in-place edits)은 앞서 설명한 API PUT을 통해 이루어집니다. 터미널에서 이와 동일한 작업을 수행하려면, 오래된 초안을 삭제하고 수정된 내용으로 다시 생성해야 합니다:
nylas email drafts delete <draft-id> "$NYLAS_GRANT_ID"
nylas email drafts create "$NYLAS_GRANT_ID" \
--to dana@customer.example \
...
이렇게 하면 새로운 초안 ID (draft id)가 생성되므로, 데이터베이스 (DB) 행을 해당 ID를 가리키도록 업데이트해야 합니다. API PUT 방식은 동일한 ID를 유지하기 때문에 더 깔끔하며, 이것이 실제 검토 UI (review UI)가 셸 명령어를 실행하는 대신 API를 호출하는 이유입니다.
이것은 초안 (draft)을 기본 단위 (primitive)로 사용할 때 얻을 수 있는, 과소평가된 이점입니다. Redis 기반의 버퍼 (buffer-in-Redis) 설계는 "승인하거나 다시 생성하거나 (approve or regenerate)"를 강제합니다. 반면 초안 방식은 사람이 에이전트의 초안을 가져와 수정하고, 수정된 버전을 그대로 발송할 수 있게 해줍니다. 모델을 거치는 왕복 과정 (round-trip)이 필요 없으며, 모델이 사람이 마음에 들어 했던 부분을 다시 써버릴 위험도 없습니다. 또한 CLI의 create 명령어는 초안이 답장인 경우 --reply-to <message-id> 플래그를 허용하여, 원래 대화의 스레드 (threaded)를 유지할 수 있습니다.
4단계 — 승인된 초안을 변경 없이 전송하기
검토자가 승인합니다. 이제 — 오직 이때만 — 메시지가 발송됩니다. 전송 동작은 기존 초안의 id를 대상으로 하는 POST 요청입니다. 별도의 "초안 전송" 경로가 없으며 내용을 다시 제공할 필요도 없습니다. POST /v3/grants/{grant_id}/drafts/{draft_id}는 저장된 초안을 있는 그대로 전송합니다.
curl --request POST \
--url "https://api.us.nylas.com/v3/grants/$NYLAS_GRANT_ID/drafts/<DRAFT_ID>" \
--header "Authorization: Bearer $NYLAS_API_KEY" \
...
본문이 비어 있는 이 POST 요청이 승인 동작의 전부입니다. 그 순간 초안에 담긴 내용이 무엇이든 — 원본이든 사람이 수정한 것이든 — 그대로 전송됩니다. SRE (Site Reliability Engineer)로서 제가 좋아하는 부분은 이것이 유용한 방식으로 위조 불가능 (unforgeable) 하다는 점입니다. 전송 호출 자체에 조작할 수 있는 콘텐츠가 없으므로, "승인된 것"과 "전송된 것" 사이에 차이가 발생할 수 없습니다.
자동 승인 워커 (automated approval workers)를 위해 확인 프롬프트를 억제할 수 있는 CLI 버전은 다음과 같습니다:
nylas email drafts send <draft-id> "$NYLAS_GRANT_ID" --force
전송이 성공한 후에는 데이터베이스 (DB) 행을 sent로 변경하고, 누가 언제 승인했는지 기록하십시오. 해당 행과 방금 전송된 메시지는 귀하의 감사 추적 (audit trail)이 됩니다: 에이전트가 제안하고, 사람이 승인했으며, 메시지가 발송되었고, 이 모든 과정은 추적이 가능합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기