자체 편지함을 소유하는 고객 지원 분류(Triage) 에이전트 구축하기
요약
단순한 웹훅 연결을 넘어, 자체 이메일 주소와 정체성을 가진 고객 지원 분류(Triage) 에이전트 구축 방법을 소개합니다. Nylas를 활용하여 에이전트 계정을 생성하고, 기존 인프라를 그대로 활용하며 안정적으로 메일을 처리하는 워크플로우를 다룹니다.
핵심 포인트
- 공유 편지함 대신 독립적인 에이전트 계정(Agent Account) 사용 권장
- Nylas를 통해 OAuth 복잡성 없이 API 호출만으로 계정 생성 가능
- 기존의 관측성 및 웹훅 인프라를 그대로 활용하여 SRE 관점에서 유리
- 커스텀 도메인 사용 시 DNS 설정 및 도메인 워밍업 기간 고려 필요
대부분의 "AI 고객 지원 봇" 데모는 LLM을 공유 Gmail에 연결하거나 헬프데스크(helpdesk)에 웹훅(webhook)을 걸어두는 것으로 끝납니다. 하지만 에이전트가 대화의 실제 참여자가 되기를 원한다면 이야기가 달라집니다. 즉, 실제 주소로 메일을 받고, 어떻게 처리할지 결정하며, 타인의 편지함을 건드리는 스크립트가 아닌 자기 자신으로서 답장을 보내야 하는 상황 말입니다.
그러니 그런 방식은 하지 맙시다. support@yourcompany.com에 일등 시민(first-class)으로서의 정체성을 부여합시다. 즉, 모든 수신 이메일을 받고, 모델로 분류하며, 서버 측 규칙(server-side rules)으로 라우팅 및 필터링하고, 적절한 스레드(thread)에서 단 한 번만(두 번은 안 됩니다) 답장을 보내는 **에이전트 계정(Agent Account)**을 만드는 것입니다. 공유 편지함도, 사람의 계정을 스크래핑(scraping)할 필요도, 헬프데스크라는 중간 매개체도 필요 없습니다.
저는 Nylas CLI를 개발하고 있으므로, 아래의 터미널 명령어들은 제가 이러한 시스템을 설정할 때 실제로 사용하는 것들입니다. 모든 단계는 두 가지 관점, 즉 원시 curl 호출과 동일한 작업을 수행하는 nylas 명령어를 함께 살펴봅니다.
실제로 얻게 되는 것
**에이전트 계정(Agent Account)**은 근본적으로 grant_id를 가진 Nylas **권한(grant)**일 뿐입니다. 이것이 핵심이며, 잠시 생각해 볼 가치가 있습니다. 데이터 평면(data plane)에서 새로 배울 것은 없습니다. 여러분이 이미 알고 있는 모든 권한 범위 엔드포인트(grant-scoped endpoint) — Messages, Drafts, Threads, Folders, Attachments, Contacts, Calendars, Events — 는 OAuth를 통해 얻은 Gmail 또는 Microsoft 권한에 대해 작동하는 방식과 정확히 동일하게 이 권한에 대해서도 작동합니다. 차이점은 제공자(provider)가 google 대신 nylas라는 것뿐이며, 이것이 여러분의 애플리케이션 코드에서 볼 수 있는 유일한 차이점입니다.
구체적으로, 이 계정에는 다음이 포함됩니다:
-
여러분이 제어하는 도메인(또는
*.nylas.email체험용 서브도메인)에 있는 실제 송수신 편지함. -
기본적으로 제공되는 6개의 예약된 시스템 폴더 —
inbox,sent,drafts,trash,junk,archive. -
OAuth 절차(dance)나 관리해야 할 리프레시 토큰(refresh token)이 없습니다. 단 한 번의 API 호출로 생성할 수 있습니다.
SRE(Site Reliability Engineer)로서 제가 좋아하는 부분은, 이것이 일반적인 권한 부여(grant) 방식이기 때문에 이미 인간 계정을 위해 구축해 놓은 관측성(observability), 재시도(retry), 웹훅(webhook) 배관 구조에 그대로 끼워 맞출 수 있다는 점입니다. 별도의 특수 케이스 코드 경로를 유지 관리할 필요가 없습니다.
시작하기 전에
두 가지가 필요합니다:
- API 키. 모든 요청은
Authorization: Bearer <NYLAS_API_KEY>를 통해 인증되며, 이 키는 귀하의 애플리케이션을 식별합니다. 여기의 예제들은https://api.us.nylas.com을 호출합니다. - 인증된 도메인. 에이전트 계정(Agent Accounts)은 도메인 상에 존재합니다. 직접 등록하고 DNS 레코드를 게시한 커스텀 도메인이거나,
yourapp.nylas.email과 같은 Nylas 체험판 서브도메인 중 하나여야 합니다. 새 도메인은 약 4주에 걸쳐 워밍업(warm up) 기간을 거치므로, 프로덕션 환경에 적용할 계획이라면 실제 도메인을 조기에 등록하십시오. 전체 DNS 절차는 프로비저닝 문서에서 확인할 수 있습니다.
이미 nylas init을 실행했다면, CLI가 귀하의 애플리케이션을 가리키고 있으므로 준비가 완료된 상태입니다.
support@ 프로비저닝하기
"provider": "nylas"와 settings.email에 이메일 주소를 사용하여 단 한 번의 POST /v3/connect/custom 호출로 계정을 생성합니다. 선택 사항인 최상위 name은 해당 계정이 보내는 모든 메시지의 기본 From 표시 이름이 됩니다.
curl --request POST \
--url "https://api.us.nylas.com/v3/connect/custom" \
--header "Authorization: Bearer <NYLAS_API_KEY>" \
...
응답으로 data.id가 반환됩니다. 이를 저장해 두세요 — 이것이 이후의 모든 호출에서 사용할 grant_id입니다.
CLI를 사용하면 한 줄로 가능합니다:
nylas agent account create support@yourcompany.com --name "Acme Support"
이 명령은 권한 부여(grant)를 프로비저닝하고 해당 id, 상태(status), 커넥터(connector) 세부 정보를 출력합니다. 만약 애플리케이션에 기반이 되는 nylas 커넥터가 아직 존재하지 않는다면, CLI가 먼저 이를 생성하므로 별도로 신경 쓸 필요가 없습니다. 또한 API는 계정을 위한 기본 워크스페이스(workspace)와 기본 정책(policy)을 자동으로 생성하며, 이는 잠시 후 필터링(filtering) 단계에서 중요하게 작용합니다.
지금 주의해야 할 정직한 함정(gotcha)이 하나 있습니다. agent account create 명령에는 --workspace 플래그가 없습니다. 워크스페이스(Workspaces, 정책과 규칙을 담고 있음)는 별도로 연결됩니다. 그 작업은 아래에서 진행하겠습니다.
나중에 사람이 IMAP을 통해 편지함을 들여다볼 수 있게 하려면, 생성 시점에 --app-password를 전달하세요 (18~40자의 ASCII 문자, 대소문자 및 숫자 혼합). 이 단계를 건너뛰면 프로토콜 액세스(protocol access)가 비활성 상태로 유지되며, 이는 완전히 자동화된 에이전트의 경우 보통 의도하는 바와 일치합니다.
모든 수신 메시지 받기
에이전트 전체는 하나의 웹훅(webhook)을 통해 작동합니다. support@로 들어오는 수신 메일은 표준 message.created 알림을 발생시키며, 이는 다른 권한(grant)에서 받는 것과 동일한 형태입니다.
curl --request POST \
--url "https://api.us.nylas.com/v3/webhooks" \
--header "Authorization: Bearer <NYLAS_API_KEY>" \
...
터미널에서도 동일하게 수행합니다:
nylas webhook create \
--url https://support-agent.yourcompany.com/webhooks/nylas \
--triggers message.created \
...
message.created 페이로드(payload)는 대략 다음과 같은 모습입니다. 전체 본문이 아닌 요약(summary) 필드를 포함하고 있다는 점에 유의하세요:
{
"type": "message.created",
"data": {
...
핸들러(handler)는 즉시 200을 반환하고, X-Nylas-Signature 헤더를 검증한 다음 비동기적으로 작업을 수행해야 합니다. 가장 먼저 해야 할 일은 에이전트 자신이 보낸 메시지를 건너뛰는 것입니다. message.created는 발신 메일에 대해서도 발생하기 때문이며, 자신의 답장에 다시 답장하는 에이전트는 아주 특별한 종류의 고장 난 상태이기 때문입니다:
app.post("/webhooks/nylas", async (req, res) => {
res.status(200).end();
...
웹훅은 스니펫(snippet)만 전달하므로, 모델에 무언가를 전달하기 전에 전체 메시지를 가져와야 합니다:
curl --request GET \
--url "https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/messages/msg-abc123" \
--header "Authorization: Bearer <NYLAS_API_KEY>"
nylas email read msg-abc123
여기에는 명확한 한계가 있습니다. 본문(body)이 약 1MB보다 크면 웹훅(webhook) 유형이 message.created.truncated로 변경되고 본문은 생략됩니다. 따라서 페이로드(payload)가 항상 전체 텍스트를 포함할 것이라고 신뢰해서는 안 됩니다.
LLM으로 분류하기
분류(Triage)는 분류 문제(classification problem)이며, 분류는 모델이 가장 잘하는 작업입니다. 전체 메시지를 가져온 다음, 의도(intent)를 결정하는 실제 세 가지 필드인 제목(subject), 발신자(sender), 본문(body)을 모델에 입력하세요. 그리고 카테고리를 반환하도록 요청합니다.
이 부분은 의도적으로 특정 제공업체에 종속되지 않도록 설계되었습니다. 어떤 chat-completions 스타일의 API든 작동하며, 프롬프트(prompt)가 설계의 핵심입니다:
async function triage(msg) {
const full = await getFullMessage(msg.grant_id, msg.id); // GET .../messages/{id}
...
라벨(label) 세트는 작고 폐쇄적으로 유지하세요. 6개의 카테고리 중에서 선택하도록 요청받은 모델은 신뢰할 수 있지만, "고객의 요구사항을 요약하라"는 요청을 받은 모델은 새벽 2시에 디버깅을 해야 하는 골칫거리가 될 것입니다. 카테고리는 메시지가 어떤 폴더에 담길지, 에이전트가 자동 답장을 보낼지, 사람이 개입할지 등 모든 후속 프로세스를 결정하는 핵심 요소입니다.
정책(Policies), 규칙(Rules), 리스트(Lists)를 통한 라우팅 및 필터링
많은 "AI 편지함" 빌더들이 놓치는 핵심적인 단계가 있습니다. 모든 메시지가 애플리케이션에 도달할 필요는 없다는 점입니다. Nylas Agent Accounts는 웹훅이 실행되기 전에 메일을 필터링하고 정렬할 수 있는 세 가지 서버 측 프리미티브(primitives)를 제공하므로, LLM은 분류할 가치가 있는 항목에만 토큰(token)을 소비하게 됩니다.
- 정책 (Policies): 제한 사항(전송 할당량, 저장 용량, 보관 기간)과 스팸 탐지를 하나로 묶습니다. 하나의 정책으로 여러 계정을 관리할 수 있습니다.
- 규칙 (Rules): 발신자/수신자 필드를 기준으로 수신 또는 발신 메일을 매칭하며,
block,mark_as_spam,assign_to_folder,archive, 또는trash와 같은 작업을 실행합니다. - 리스트 (Lists): 규칙이
in_list연산자를 통해 참조할 수 있는 도메인, TLD 또는 주소의 타입화된 컬렉션입니다. 이를 통해 엔지니어가 아닌 사람도 배포(deploy) 없이 허용 목록(allowlist)이나 차단 목록(blocklist)을 업데이트할 수 있습니다.
이들은 하나의 체인을 형성합니다: 목록(lists)은 값을 보유하고, 규칙(rules)은 목록을 참조하며 조건과 동작(actions)을 기술하며, 정책(policies)은 제한 사항을 묶고, **워크스페이스 (workspace)**는 하나의 policy_id와 rule_ids 배열을 가집니다. 워크스페이스 내의 모든 에이전트 계정(Agent Account)은 이 두 가지를 모두 상속받습니다. 개별 권한(grant)에는 아무것도 부착하지 않습니다.
먼저 SMTP 계층에서 알려진 악성 발신자를 차단하여 메일함에 도달하지 않도록 시작해 보세요:
curl --request POST \
--url "https://api.us.nylas.com/v3/rules" \
--header "Authorization: Bearer <NYLAS_API_KEY>" \
...
CLI는 동일한 규칙을 생성하고 이를 기본 워크스페이스에 한 번에 부착합니다:
nylas agent rule create \
--name "Block spam-domain.com" \
--trigger inbound \
...
허용 목록/차단 목록(allowlist/blocklist) 패턴의 경우, 목록을 생성하고 규칙에서 이를 참조하십시오. API 우선 방식:
curl --request POST \
--url "https://api.us.nylas.com/v3/lists" \
--header "Authorization: Bearer <NYLAS_API_KEY>" \
...
해당 호출은 목록 id를 반환합니다. 두 번째 호출로 항목을 채우십시오 — 목록 항목은 그 자체로 하위 리소스(sub-resource)입니다:
curl --request POST \
--url "https://api.us.nylas.com/v3/lists/<LIST_ID>/items" \
--header "Authorization: Bearer <NYLAS_API_KEY>" \
...
그런 다음 in_list를 사용하여 해당 목록과 일치하는 규칙을 생성합니다:
curl --request POST \
--url "https://api.us.nylas.com/v3/rules" \
--header "Authorization: Bearer <NYLAS_API_KEY>" \
...
CLI는 목록 생성과 항목 채우기를 하나의 명령어로 축소하며, 규칙 참조는 또 다른 명령어로 처리합니다:
nylas agent list create --name "Blocked domains" --type domain --item spam.com
nylas agent rule create \
--name "Block anything on our blocklist" \
...
라우팅(Routing)도 다른 동작을 사용할 뿐 구조는 동일합니다. 에이전트가 분류(classification)에 낭비하지 않도록 자동화된 알림을 별도의 폴더로 푸시하십시오 — assign_to_folder를 mark_as_read와 결합합니다. curl 형태는 다음과 같습니다:
curl --request POST \
--url "https://api.us.nylas.com/v3/rules" \
--header "Authorization: Bearer <NYLAS_API_KEY>" \
...
그리고 규칙을 생성하고 기본 워크스페이스 (default workspace)에 연결하는 CLI (Command Line Interface) 방식은 다음과 같습니다:
nylas agent rule create \
--name "Route notifications to a folder" \
--trigger inbound \
...
만약 커스텀 (custom) 정책을 생성했고 이를 계정에 적용하고 싶다면, 워크스페이스에 연결하십시오. 이것이 앞서 언급한 --workspace 플래그가 없는 세부 사항이 해결되는 지점입니다. 정책을 생성한 다음, 해당 정책을 가리키도록 워크스페이스를 PATCH 하십시오. curl 형태는 다음과 같습니다:
# 정책 생성
curl --request POST \
--url "https://api.us.nylas.com/v3/policies" \
...
CLI는 이 두 단계를 하나로 축소합니다:
nylas agent policy create --name "Support Agent Policy"
nylas workspace update <workspace-id> --policy-id <policy-id>
반드시 숙지해야 할 한 가지는 인바운드 (inbound) 및 아웃바운드 (outbound) 규칙은 격리되어 있다는 점입니다. 인바운드 규칙은 전송 시에 실행되지 않으며, 아웃바운드 규칙은 수신 시에 실행되지 않습니다. 따라서 예를 들어 에이전트가 보내는 모든 답장에 별표(star)를 표시하고 싶다면, 이는 outbound.type이 reply와 일치하는 outbound 규칙이 됩니다. 이는 귀하의 인바운드 분류 (triage) 작업에 전혀 방해를 주지 않습니다.
올바른 스레드 (thread)에서 답장하기
에이전트가 응답하기로 결정했을 때, 스레딩 (threading)은 타협할 수 없는 요소입니다. 답장은 연결되지 않은 새로운 이메일이 아니라, 고객의 기존 대화 _내부_에 도착해야 합니다. Nylas는 reply_to_message_id를 통해 이를 처리합니다. 전송 시 이 ID를 전달하면 Nylas가 In-Reply-To 및 References 헤더를 대신 설정해 주므로, 고객의 메일 클라이언트가 답장을 올바르게 그룹화할 수 있습니다.
curl --request POST \
--url "https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/messages/send" \
--header "Authorization: Bearer <NYLAS_API_KEY>" \
...
CLI의 경우, nylas email reply가 귀하를 대신해 이러한 관리 작업을 수행합니다. 메시지 ID를 지정하면 해당 명령어가 원본 메시지를 가져와 수신자와 제목을 채운 다음, reply_to_message_id를 통해 자동으로 스레딩을 유지합니다.
nylas email reply msg-abc123 --body "Hi Dana — I see the reset went through an hour ago..."
기본적으로 답장은 원본 발신자에게만 전송됩니다. 스레드(thread)에 포함된 나머지 수신자(To/Cc)를 유지해야 한다면 --all 옵션을 추가하세요. 이 명령어는 제가 에이전트를 수동으로 테스트할 때 가장 즐겨 사용하는 방식입니다. "메시지가 도착했다"에서 "스레드가 유지된 답장이 발송되었다"까지 가는 가장 짧은 경로입니다.
중복 답장 방지하기
이 부분이 바로 초보적인 빌드(naive builds)가 운영 환경에서 무너지는 지점입니다. 웹훅(Webhooks)은 최소 한 번(at least once) 전달됩니다. 만약 엔드포인트(endpoint)가 200 응답을 보내는 데 시간이 걸리거나 네트워크 순단(network blip)이 발생하면, 동일한 message.created 이벤트가 다시 전달될 수 있습니다. 동시성 워커(concurrent workers)를 추가할 경우, 두 개의 워커가 동일한 밀리초(millisecond)에 같은 알림을 가로챌 수도 있습니다. 결과는 어찌 되었든 두 번의 답장, 그리고 한 명의 화난 고객입니다.
해결책은 멱등성(idempotency)이며, 이는 Nylas가 아닌 전적으로 귀하의 자체 저장소(store)에서 관리되어야 합니다. 에이전트 계정(Agent Account) 리소스에 대한 커스텀 메타데이터 태깅(Custom metadata tagging)은 아직 지원되지 않으므로, 메시지나 권한(grant)에 중복 제거 상태를 저장할 수 없습니다. 대신 Redis나 Postgres에 저장하세요. 이미 처리한 메시지 ID를 추적하고, 원자적 체크 앤 셋(atomic check-and-set)을 사용하여 동일한 메시지를 두 번 처리하지 않도록 거부해야 합니다.
const messageId = event.data.object.id;
// 원자적 삽입(Atomic insert). 키가 새로 삽입되었을 때만 참(truthy)을 반환합니다:
...
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기