그래프RAG 발신자 추출에 LLM부터 쓰지 않았습니다 — 헤더와 규칙으로 78%를 커버한 이야기
이메일 57%가 공유 메일함에서 발신되고 있었습니다
지식그래프에 이메일을 임포트하면서 발신자(Person 노드)를 추출하는 작업이 필요했습니다. (Person)-[:SENT]->(Email) 관계가 연결되어야 "이 사람이 어떤 시스템을 자주 언급한다" 같은 그래프 분석이 가능해지거든요.
문제는 이메일의 57%가 공유 메일함(예: ops@example.com 같은 팀 공동 주소)에서 발신되고 있었다는 점이었습니다. 헤더의 from 필드만 보면 전부 같은 주소로 찍혀 있어서 실제로 누가 보냈는지 알 수 없었어요. 운영팀 누군가가 그 메일함을 통해 보낸 것은 분명한데, 그 누군가가 누군지를 헤더는 알려주지 않았습니다.
첫 발상은 "그냥 LLM에 다 맡기자"였습니다
처음에는 단순하게 생각했어요. "본문에서 발신자 이름을 LLM이 뽑아주면 되는 거 아닌가?" LLM이 텍스트 이해는 잘하니까, 모든 이메일 본문을 다 넣어서 발신자를 추출하면 끝날 일처럼 보였습니다.
이 접근의 문제는 두 가지였습니다.
비용: 임포트 대상 이메일이 2,385건이었고, 새 이메일이 매일 추가됩니다. 모든 이메일을 LLM에 넣으면 한 번 임포트할 때 수천 번 LLM 호출이 발생합니다. 데이터 구축 단계의 LLM 호출이 채팅 비용보다 더 클 수 있는 구조였어요.
오탐: LLM이 본문에서 이름을 뽑을 때 문맥을 잘못 잡으면, 메일에 인용된 다른 사람을 발신자로 추출할 수 있습니다. "어제 김OO 과장님께서 말씀하신 대로..."라는 본문이 있으면 발신자를 김OO으로 추출하는 식이에요. 본문에 등장하는 모든 이름이 발신자 후보가 되는 셈이라 오탐 처리만으로 시간을 많이 쓰게 됩니다.
그래서 다른 접근이 필요했습니다.
비싼 도구를 마지막에 두는 cheap-first 3단계
발상 전환의 핵심은 "모든 이메일에 LLM을 쓸 필요가 없다" 였습니다. 발신자 정보는 데이터에 따라 위치가 다릅니다.
외부 발신자가 보낸 이메일: 헤더 from에 이미 이름과 이메일이 다 적혀 있음
공유 메일함에서 발신된 이메일: 본문 서명/인사말에 이름이 있음
시스템 자동메일: 사람 이름 자체가 없음
각 케이스에 맞는 가장 싼 도구를 먼저 쓰고, 그게 못 잡은 것만 다음 단계로 넘기는 캐스케이드 구조로 짰습니다.
단계 | 방법 | 처리 건수 | 비용 |
|---|---|---|---|
1. 헤더 기반 |
| 975건 | 0원 |
2. 규칙 기반 | 본문 인사말/서명 패턴 매칭 | +959건 | 0원 |
3. LLM 폴백 | 미식별분만 LLM 호출 | +23건 | ~$0.01 |
합계 | 1,875/2,385 (78%) |
미식별 510건은 시스템 자동메일이었어요. 발신자 정보가 본문에도 없는 메일이라 LLM도 못 뽑습니다. 이건 데이터 자체의 한계여서 미식별로 남기는 게 맞았습니다.

규칙 단계에서 가장 많은 걸 잡았습니다
흥미로운 점은 가운데 단계인 규칙 기반에서 거의 절반이 잡혔다는 거였어요. 959건이면 헤더로 잡은 975건과 거의 동등한 양입니다. 정규식 몇 개로 LLM 호출을 1,000번 가까이 절약한 셈이에요.
규칙은 세 가지 패턴이었습니다.
패턴 1 — 인사말: "안녕하세요. 운영팀 김OO 입니다."
→ regex: 안녕하세요 이후 200자 내 ([가-힣]{2,4})\s*입니다
패턴 2 — 서명 블록: "김OO (Kim OOO)"
→ regex: ([가-힣]{2,4})\s*\(대문자영문 2단어 이상\)
패턴 3 — 개인 이메일: "Email kim.oo@example.com"
→ regex: Email\s+(이메일주소)이게 잘 동작한 이유는 회사 내부 메일 서명 형식이 일관됐기 때문이에요. 거의 모든 사람이 같은 패턴으로 인사말과 서명을 쓰고 있었습니다. 회사 내부 표준이라는 특수성 덕분에 정규식 몇 개로 대부분이 커버됐어요.
오탐을 막는 작은 장치들이 필요했습니다
규칙 기반이 효율적이긴 한데, 정규식이 항상 정확하지는 않습니다. 오탐을 막기 위한 장치들을 같이 깔아야 했어요.
일반 명사 차단 리스트: "예정", "결과", "확인" 같은 단어가 인사말 패턴에 잡히는 경우가 있었습니다. "확인 입니다"가 들어 있으면 "확인"이 사람 이름으로 추출되는 식이에요. 이런 일반 명사를 별도 차단 리스트(_GREETING_FALSE_POSITIVES)로 두고 매칭 시 거르도록 했습니다.
서명 블록의 영문 검증: 한글 이름 옆에 괄호로 영문이 붙은 패턴(김OO (Kim OOO))을 잡을 때, 괄호 안에 어떤 글자든 들어가게 두면 개발(masking) 같은 비-인명 표현이 잡힙니다. 그래서 괄호 안이 대문자로 시작하는 영문 2단어 이상이어야 매칭하도록 조건을 넣었어요.
LLM 프롬프트 가드: 마지막 단계의 LLM에는 "일반 명사, 영문 소프트웨어명, 조직/팀명은 null로 반환하라"고 명시했습니다. LLM이 헷갈릴 만한 것들을 미리 거르는 가드입니다.
작은 장치들이지만 이런 게 빠지면 노이즈가 많이 들어와서 후속 그래프 분석이 흔들립니다.
"헤더 먼저, 본문은 보완"이 일반 원칙입니다
여기서 한 가지 짚고 넘어가야 할 게 있어요. 이번 케이스에서 본문 기반 추출이 효과적이었지만, 이건 우리 환경의 특수성 덕분이었습니다.
공유 메일함이라 헤더에 실제 발신자 정보가 없는 비율이 매우 높음
회사 내부 표준 덕분에 서명 형식이 일관됨
일반적인 환경에서는 헤더 기반이 항상 우선이에요.
헤더 기반 | 본문 기반 | |
|---|---|---|
정확도 | 100% (구조화된 데이터) | 패턴 의존 (오탐 가능) |
비용 | 0 | 규칙 작성 + LLM 폴백 |
커버리지 | from/to/cc에 있는 것만 | 서명/인사말까지 |
처음부터 본문 기반으로 전부 하려고 하면 오탐 처리와 프롬프트 튜닝에 시간을 훨씬 더 쓰게 됩니다. 헤더 먼저 → 부족한 부분만 본문 규칙 → 그래도 남으면 LLM, 이 순서가 일반 원칙이에요.
cheap-first 패턴의 진짜 의미는 "본문이 LLM보다 싸다"가 아니라 "각 데이터의 가장 싼 위치를 먼저 본다"입니다. 헤더에 있는 것은 헤더에서, 본문 서명에 있는 것은 본문에서, 그래도 못 찾는 것만 LLM으로. 데이터 구조에 맞는 도구를 단계별로 적용하는 게 핵심입니다.

발신자만 뽑고 끝이 아니라 Person 노드로 연결됩니다
발신자 이름을 추출하는 것 자체로는 그래프 탐색이 안 됩니다. 이름이 문자열로만 남아 있으면 검색에는 잡혀도 관계 탐색이 안 되거든요. (Person)-[:SENT]->(Email) 경로가 연결되려면 Person 노드가 그래프에 존재해야 합니다.
그래서 추출 단계에서 Person 노드 생성과 관계 추가까지 같이 처리합니다.
규칙 기반에서 개인 이메일까지 추출된 경우 → Person 노드 생성 + SENT/BELONGS_TO 관계 추가
LLM 폴백에서 추출된 경우 → 동일하게 Person 노드와 관계 보완
발신자 이름 + 이메일 주소가 같이 추출되어야 Person 노드의 식별자(이메일)가 확정됩니다. 이름만 있으면 동명이인 문제가 생기거든요. 그래서 패턴 3(Email kim.oo@example.com)이 중요한 역할을 합니다.
이렇게 만들어진 MANAGES 관계나 SENT 관계를 활용한 후속 분석은 다른 글에서 다뤘었어요.
마무리
이메일 발신자 추출을 헤더(975건) → 규칙(959건) → LLM(23건)의 cheap-first 3단계로 처리해서 78% 커버리지를 달성했습니다. LLM 비용은 전체의 1% 수준에 그쳤고, 미식별 510건은 시스템 자동메일이라 사람 이름 자체가 없는 데이터의 한계였습니다.
이번 케이스에서 본문 기반 추출이 잘 된 것은 회사 내부 메일 서명 형식이 일관됐다는 특수성 덕분입니다. 일반적인 환경에서는 헤더 기반이 항상 우선이에요.
LLM을 써야 하는 작업이 늘어나면서 비용이 부담스러워질 때가 있는데, 그럴 때는 "정말 모든 케이스에 LLM이 필요한가"를 한 번 따져보는 걸 추천합니다. 헤더에 있는 것은 헤더에서, 패턴에 있는 것은 정규식으로, LLM은 마지막 폴백으로. 이 순서로 짜는 것만으로도 비용이 1% 수준으로 떨어지는 경우가 많습니다.