AI 에이전트의 지루한 부분, 즉 작동하는 부분이 핵심이다
요약
AI 에이전트가 사고 이메일을 처리하는 과정에서 핵심은 LLM 자체의 능력보다 시스템 배관(plumbing)에 달려있습니다. 글쓴이는 BSS 플랫폼을 위해 루프를 붕괴시키는 에이전트를 구축하여 평균 해결 시간을 크게 단축시켰으며, 그 비결은 데이터 전처리 및 구조화된 출력 요청에 있습니다.
핵심 포인트
- 사고 처리 시간 지연의 주범은 LLM보다 '대기' 과정이다.
- LLM 활용의 핵심은 이메일에서 노이즈를 제거하는 강력한 전처리(Normalise) 단계다.
- 모델 호출 시, 고정된 열거형과 Pydantic을 사용해 구조화되고 제한적인 출력을 요청해야 한다.
- 분류 시스템에는 'unknown' 옵션을 반드시 포함하여 모델의 과도한 확신을 방지해야 한다.
대부분의 경우 사고가 '처리되는' 시간은 실제 작업 시간이 아닙니다. 그것은 누군가가 조치할 수 있는 사람에게 도달하기를 기다리는 시간입니다. 경고(alert)가 발생하고, 공유 받은 편지함에 이메일이 도착하며, 누군가가 훑어보고 어느 팀이 소유했는지 추측한 뒤 전달합니다. 그리고 그 팀은 그것이 자신들의 일이 아니라고 결정합니다. 좋지 않은 밤에는 아무도 터미널을 열기 전에 이 루프가 당신에게 한 시간을 비용으로 지불하게 합니다.
저는 BSS 플랫폼을 위해 이러한 루프를 붕괴시키는 에이전트를 만들었고, 그 결과 라우팅된 사고의 평균 해결 시간(mean time to resolution)은 약 두 시간에서 10분 정도로 줄었습니다. 저를 놀라게 한 것은 이 중 얼마나 적은 부분이 언어 모델(language model)에서 비롯되었는지입니다. 모델은 중간에 작은 일 하나만 합니다. 시스템을 신뢰할 수 있게 만드는 모든 것이 배관(plumbing)입니다.
이 글은 그 배관에 관한 것입니다.
문제의 형태
들어오는 사고 이메일은 최악의 방식으로 반정형화되어 있습니다: 템플릿이 있고, 세 개의 시스템은 그것을 무시하며, 두 개는 자체 스택 트레이스(stack traces)를 추가하고, 가장 중요한 하나는 인간이 작성한 순수 산문(plain prose)을 보냅니다. 정규 표현식(Regex)으로는 어쩌면 60% 정도만 해결할 수 있고, 업스트림 팀이 경고 본문을 편집할 때마다 고장 납니다.
그 나머지 40%의 부분이 바로 LLM이 자리를 얻는 정확한 곳입니다. 하지만 '이 이메일을 분류하라'가 시스템 그 자체가 아닙니다. 시스템은 다음과 같습니다:
poll inbox → normalise → classify → resolve owner → create ticket → notify → observe
모델은 세 번째 단계입니다. 첫 번째, 두 번째, 네 번째, 다섯 번째, 여섯 번째, 그리고 일곱 번째 단계가 누군가가 그것을 신뢰할지 여부를 결정합니다.
분류하기 전에 정규화(Normalise)하라
제가 한 가장 높은 레버리지의 작업은 모델이 보기 전에 이메일을 공격적으로 제거하는 것이었습니다. 서명, 법적 푸터, 인용된 답장 체인, base64 인라인 이미지, 그리고 40줄에 달하는 Received: 헤더는 순수한 토큰 비용이자 순수한 방해 요소입니다.
import re
from email import policy
from email.parser import BytesParser
...
이 [:4000]이 중요합니다. 자르기(Truncation)는 기능입니다. 분류하기 위해 4000자 이상의 컨텍스트가 필요한 사고 이메일이라면, 애초에 인간이 읽어야 하는 것입니다.
모델이 문장이 아닌 형태를 반환하도록 하라
분류 호출은 의도적으로 작고 제한적입니다. 저는 고정된 열거형(enum)의 팀 목록, 신뢰 점수, 그리고 한 줄 분량의 근거를 가진 구조화된 출력을 요청합니다.
from pydantic import BaseModel, Field
from typing import Literal
...
쉽게 건너뛰기 쉽지만, 건너뛰면 비용이 많이 드는 세 가지 디테일이 있습니다:
unknown도 유효한 답변입니다. 열거형에 탈출구(escape hatch)가 없다면, 모델은 가장 가까운 그럴듯한 팀을 선택하고 그것에 대해 확신하는 것처럼 보일 것입니다. 솔직하게 갈 곳을 제공하는 것이 이용 가능한 가장 저렴한 정확도 향상 방법입니다.
근거는 글자 수 제한이 있습니다. 아무도 평소에 읽지는 않지만, 새벽 3시에 분류(triage)가 잘못될 때 근거야말로 모델이 이메일을 오해했는지 아니면 소유자 지도(owner map)가 오래되었는지를 알려주는 유일한 산출물입니다. 200자 제한은 이것을 에세이가 아닌 신호로 유지하게 합니다.
신뢰도는 모델의 것이며, 기껏해야 방향성만 가집니다. 이를 확률이 아니라 동점 처리기(tiebreaker)로 취급하세요. 저는 임계값(threshold)을 설정하지만, 이 임계값은 숫자가 의미한다고 주장하는 것과 대비되는 것이 아니라 관찰된 결과와 대비하여 조정됩니다.
소유자 지도는 모델의 문제가 아닙니다
프롬프팅으로는 아무리 고쳐도 해결되지 않는 부분이 여기 있습니다. 모델은 티켓이 청구(billing)에 관한 것임을 알려줄 수 있지만, 청구가 어제 온콜(on-call) 로테이션을 했는지, 또는 지난 분기에 청구팀이 두 개의 스쿼드로 나뉘었는지는 알지 못합니다.
따라서 모델은 _사람_이 아닌 _팀_으로 결과를 도출합니다. 별도의 디렉터리 조회 — 제 경우 Microsoft Graph — 를 통해 팀을 현재 실제로 온콜인 사람에게 연결하는 것입니다.
async def resolve_owner(team: str, graph) -> Owner:
group = OWNER_GROUPS[team] # 정적이며 버전 관리되는 매핑
members = await graph.group_members(group)
...
이러한 분할(split) 덕분에 시스템은 조직 변화에도 살아남을 수 있습니다. 팀이 재편성될 때 누군가 YAML 파일을 편집하기만 하면 됩니다. 아무도 모델을 재학습시키거나 프롬프트를 다시 작성할 필요가 없습니다. LLM이 세상을 바라보는 관점은 '이 이메일의 주제는 무엇인가'와 같이 안정적입니다. 휘발성이 강한 지식(volatile knowledge)은 이미 그 정보의 진실 공급원(source of truth)인 시스템에 존재해야 합니다.
이 게시물에서 한 가지를 얻어 가신다면: **절대로 모델이 다른 시스템이 소유하는 사실(facts)을 담도록 두지 마십시오.**
## 신뢰도가 낮은 경로는 사람에게, 명확하게 전달하기 (Route low confidence to a human, loudly)
async def handle(mail: Mail) -> None:
body = extract_body(mail.raw)
triage = await classify(body)
...
폴백(fallback) 경로는 실패 경로가 아니라, 모호한 모든 것에 대한 _기본값(default)_ 경로이며, 눈에 띄는 곳으로 연결됩니다. 초기에 이메일 약 6개 중 1개가 사람의 채널로 갔습니다. 이는 나쁜 수치처럼 느껴졌지만, 그 안에 무엇이 들어있는지 살펴보니: 대부분은 정말로 모호했고, 그렇지 않은 경우들은 제가 상위 시스템(upstream)에서 수정해야 할 정확한 알림 템플릿을 알려주었습니다. 폴백 대기열(fallback queue)이야말로 개선 작업을 위한 최고의 자료원입니다.
## Idempotency (멱등성), 이메일은 대기열이 아니므로
Mail API는 동일한 메시지를 두 번 전달할 수 있습니다. 사용자의 폴러(poller)가 '티켓 생성됨'과 '읽음으로 표시됨' 사이에서 충돌을 일으킬 수도 있습니다. 누군가는 테스트를 위해 배치(batch)를 재실행할 것입니다.
모든 메시지에는 결정론적 키(deterministic key)가 부여되며, 티켓 생성은 이 키에 조건부로 의존합니다:
def dedupe_key(mail: Mail) -> str:
# message-id는 재전송 시에도 안정적이므로 사용하고; subject+sender를 폴백으로 사용
raw = mail.message_id or f"{mail.sender}|{mail.subject}|{mail.sent_at}"
...
그리고 그 키에 대해 백업하는 스토어(store)의 고유 인덱스(unique index)를 만드십시오. 캐시가 아닙니다 — 고유 인덱스여야 합니다. 캐시는 메모리 압박이 걸리면 기꺼이 데이터를 제거해 버리고, 가장 바쁜 시간에 중복 티켓이 들어오는 것을 그때서야 알게 될 것입니다. 저는 캐시 키와 조용한 데이터 손실(silent data loss)에 대해 의견이 있는데, 이는 다음 게시물에서 다루겠습니다.
## 제가 첫날부터 측정할 것들
저는 '정확하게 분류했는지'로 시작했는데, 이것은 가장 유용하지 않은 지표임이 밝혀졌습니다. 왜냐하면 샘플을 가지고 손으로 계산해야만 할 수 있기 때문입니다.
실제로 결정을 이끈 요소들:
- **재경로화율 (Reroute rate)** — 티켓 생성 후 30분 이내에 재할당된 건수. 이것이 바로 실제 정확도 지표이며, 사람들이 업무를 수행하는 과정에서 자연스럽게 생성되기 때문에 비용이 들지 않습니다.
- **폴백률 (Fallback rate)** — 휴먼 채널로 넘어가는 비율. 이 수치가 상승한다는 것은 상위(upstream) 알림 형식이 변경되었다는 것을 의미합니다.
- **최초 인간 조치까지의 시간 (Time to first human action)** — 프로젝트 전체가 움직이도록 존재하는 목표 숫자입니다. 해결까지 걸리는 시간이 아닙니다. 그건 당신이 통제할 수 없습니다.
- **메시지당 비용 (Cost per message)** — 화려하지는 않지만, 시스템의 지속적인 존재 이유를 주장할 때 필요한 지표입니다.
재경로화율(Reroute rate)은 제가 어떤 분류(triage) 시스템을 구축하든 가장 먼저 측정할 지표입니다. 수집하는 데 비용이 들지 않는 '진실 값(ground truth)'입니다.
## 솔직한 요약
이 모델은 비정형 텍스트에 대해 예의 바른 분류기(classifier)입니다. 이것은 정말 유용하며, 정규 표현식(regex)으로는 불가능했을 일입니다. 하지만 '2시간에서 10분'이라는 수치는 다음 요소들로부터 나왔습니다: 인간의 개입 단계를 제거하고, 온콜(on-call)을 자동으로 해결하며, 모호한 사례들이 예의 바른 곳이 아닌 눈에 보이는 어딘가로 떨어지도록 만들었기 때문입니다.
만약 비슷한 것을 구축한다면, 그에 맞춰 시간을 계획해야 합니다. 프롬프트 작성에는 오후 시간이 걸릴 것입니다. 소유자 맵(owner map), 아이뎀포턴시(idempotency), 폴백 경로(fallback path) 및 지표들(metrics)을 만드는 데는 나머지 한 달이 걸릴 것이며, 이것들이 시스템이 6개월 후에도 여전히 작동하는 이유입니다.
_저는 백엔드 엔지니어링, 데이터 파이프라인, 그리고 신뢰성(reliability)에 대해 글을 씁니다. 만약 이 분야에서 무언가를 구축했고 소유자 매핑 문제를 다르게 해결한 경험이 있다면 듣고 싶습니다._
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기