모두가 프롬프트에 집착합니다. 인사를 다듬고, 어조를 조정하며, 모델이 충분히 따뜻하게 들릴지 걱정합니다. 하지만 그것은 본질을 벗어난 일입니다. AI 에이전트가 실제 사용자에게 실제 이메일을 보내기 시작할 때, 진짜 위험은 'Cheers' 대신 'Best regards'라고 쓰는 것이 아닙니다. 진짜 위험은 에이전트의 결정과 메시지가 수신함에 도착하기 사이에 정확히 어떤 일이 일어났는지 확신할 수 없다는 점입니다. 저는 경계(boundary)를 먼저 봅니다. 그곳이 바로 프로덕션 시스템이 조용히 무너지는 지점이기 때문입니다.
계약(Contract)이 취약점입니다
AI 데모는 관대합니다. 브라우저 창 안에서 매끄럽게 이어지는 대화는 그 이면에 깔린 수많은 가정을 숨겨줍니다. 하지만 프로덕션 환경에서 진짜 취약점은 세 가지 요소 사이의 계약에 존재합니다. 바로 에이전트의 결정, 액션을 실행하는 도구, 그리고 결과를 검증하는 단계입니다. 이 경계가 모호하면 시스템은 완벽하게 작동하다가도 어느 순간 갑자기 멈춰버립니다. 그러고는 소리 없이 실패하거나, 전체 고객 세그먼트에 중복 메시지를 보내거나, 왜 그런 일이 발생했는지에 대한 명확한 기록도 없이 잘못된 시간에 메시지를 발송해 버립니다. 프롬프트는 시처럼 아름다울지 모르지만, 그 밑의 아키텍처는 여전히 실로 간신히 버티고 있을 수 있습니다.
에이전트가 자유롭게 글을 쓰게 두지 마십시오
가장 흔한 실수는 에이전트에게 백지를 주는 것입니다. 팀들은 에이전트가 이메일 내용을 가공되지 않은 텍스트로 작성하게 내버려 두고, 하류(downstream) 도구가 그 산문에서 의도를 파악하기를 기대합니다. 이는 매우 취약한 방식입니다. LLM은 합리적인 의도를 제안할 수는 있지만, 인프라에는 창의성이 필요하지 않습니다. 인프라에는 계약이 필요합니다. 기계가 모호함 없이 검증할 수 있는 구체적인 필드가 필요합니다.
에이전트가 이메일 요청을 생성할 때, 그 출력값은 시스템 구조(plumbing)가 요구하는 사항을 정확히 담고 있어야 합니다:
- 템플릿 버전(Template version): 사용자가 어떤 버전의 이메일 본문을 보았는지 알 수 있도록 사용된 이메일 본문의 버전을 명시합니다.
- 수신자 범위(Recipient scope): "방금 가입한 사용자"와 같은 자연어가 아니라, 사용자 ID나 세그먼트 규칙에 의해 정의된 수신 대상을 지정합니다.
- 트레이스 ID(Trace ID): 에이전트에서 실행기(executor)를 거쳐 이메일 제공업체, 그리고 로그에 이르기까지 이 요청을 추적할 수 있는 고유 식별자입니다.
- 시간 범위(Time window): 이 발송이 유효한 시간대를 지정하여, 오래된 에이전트의 결정이 몇 시간 뒤 한밤중에 이메일을 발송하는 일을 방지합니다.
- 멱등성(Idempotency): 에이전트가 재시도하거나 네트워크 오류가 발생하더라도 동일한 논리적 발송이 두 번 실행되지 않도록 방지하는 키입니다.
가공되지 않은 텍스트는 끔찍한 API입니다. 긴급성, 대상, 액션에 대한 모호함을 남깁니다. 반면 구체적인 필드는 기계가 읽을 수 있고, 감사(audit)가 가능하며, 테스트할 수 있습니다. 이러한 필드들은 모호한 지시를 검증 가능한 명령으로 바꿔줍니다.
산문이 아닌 액션(Actions)
에이전트에게 개방형 글쓰기 과제를 주는 대신, 허용된 액션 메뉴로 제한하십시오. 고정된 열거형(enum)을 가진 내부 API라고 생각하면 쉽습니다. 에이전트는 제목을 작성하거나 인사를 고민하지 않습니다. 대신 send_review_request나 send_retry_notice와 같은 액션을 선택합니다. 그것이 에이전트가 누릴 수 있는 창의적 자유의 전부입니다.
그러면 결정론적 실행기(deterministic executor)가 해당 액션 키를 가져와 버전 관리 시스템에서 올바른 템플릿을 불러오고, 정제된 데이터로 내용을 채우며, 검증된 소스에서 수신자 목록을 가져와 최종 명령을 구성합니다. 에이전트는 무엇을 해야 할지를 결정합니다. 지루하고 예측 가능한 코드가 그것이 어떻게 실행될지를 결정합니다.
이러한 분리는 시스템 테스트를 용이하게 만듭니다. LLM 추론을 전혀 실행하지 않고도 특정 입력 상태가 send_retry_notice를 안정적으로 트리거하는지 검증할 수 있습니다. 유닛 테스트는 모델의 온도(temperature)가 아니라 매핑 로직을 확인하므로 빠르고 결정론적으로 변합니다. 통합 테스트는 모델의 컨디션이 좋았는지가 아니라, 실행기가 액션을 이메일 서비스에 올바르게 매핑했는지에 집중할 수 있습니다.
5개의 계층으로 구축하십시오
견고한 시스템은 단 하나의 프롬프트에서 탄생하지 않습니다. 시스템은 계층별로 구축되며, 각 계층은 단 하나의 명확한 책임을 갖습니다.
1. 백엔드는 이벤트를 안전한 데이터로 축소합니다.
트리거가 웹훅이든, 데이터베이스 변경이든, 예약된 작업이든, 이 계층은 입력을 정제하고 예상치 못한 필드를 제거하며 에이전트에게 필요한 데이터만 전달합니다. 웹훅 페이로드에 20개의 필드가 있더라도 에이전트에게 2개만 필요하다면, 그 2개만 전달하십시오. 가공되지 않은 사용자 텍스트가 검증 없이 결정 계층에 도달해서는 안 됩니다.
2. 에이전트는 고정된 스키마에서 액션을 선택합니다.
에이전트는 컨텍스트를 파악하고 판단을 내려, 미리 정의된 액션 키 중 하나와 필요한 메타데이터를 출력합니다. 에이전트는 산문을 작성하지 않습니다. 수신자를 추측하지도 않습니다. 다음 계층이 JSON 스키마를 통해 검증할 수 있는 구조화된 페이로드를 반환합니다.
3. The tool validates permissions and required fields.
Does this agent context have the right to trigger send_review_request for this user? Is the recipient scope non-empty and within allowed limits? Is the idempotency key present and unique in your log? Is the trace ID well-formed? Fail here, loudly, before any email service is ever touched.
4. The email service logs the send with a trace ID.
Every message that leaves your system should carry that trace identifier through the provider's API and into your observability stack. If a user complains they received two copies, you should be able to query one ID and see exactly where the duplication originated: a retried agent call, a flaky executor, or a misbehaving callback.
5. The end-to-end test checks the actual inbox for content and effect.
Open the rendered message in a real mailbox. Is the subject line populated correctly? Does the unsubscribe link resolve? Does clicking the primary call-to-action button land on the correct page with the correct user state? A passing unit test means the code ran. Only an inbox test tells you the email actually works for a human.
Evidence Over Guessing
When a test fails in this pipeline, you need four specific pieces of evidence. Accept nothing less.
- The original decision from the agent. What action did it choose, and what was the full input context?
- The normalized command from the tool. What did the deterministic executor build after applying the template, hydration logic, and validation rules?
- The message in the isolated inbox. Not a log of what you think you sent, but the real MIME message, headers and all, captured in a dedicated test mailbox.
- The final effect after clicking the link. The resulting page state, database change, or external event that proves the email achieved its purpose.
If one piece is missing, your team will fill the gap with assumptions. They will guess. Guessing in automation is expensive. It burns hours, erodes trust, and turns every incident into a forensic mystery instead of a
