
콜드 이메일 작성이 지겨워져서, 대신 해줄 AI 에이전트를 만들었습니다.
요약
LinkedIn 프로필을 분석하여 초개인화된 콜드 이메일을 자동으로 작성하는 AI 에이전트 'SalesAgent'를 소개합니다. LangGraph, Tavily, Groq 등을 활용하여 실시간 웹 검색과 리드 점수 산출을 수행합니다.
핵심 포인트
- LinkedIn URL 입력만으로 45초 이내에 개인화된 이메일 생성
- LangGraph 기반의 에이전트 워크플로우 설계
- Tavily 웹 검색을 통한 실시간 뉴스 및 리드 정보 조사
- scikit-learn을 활용한 리드 점수(0-100) 산출 기능
- Groq의 LLaMA 모델을 이용한 고속 이메일 생성
콜드 이메일(Cold Emails) 작성이 지겨워져서, 대신 해줄 AI 에이전트를 만들었습니다.
B2B 영업 담당자들은 단 하나의 리드(Lead)를 조사하기 위해 LinkedIn 프로필을 읽고, 회사를 구글링하고, 최근 뉴스를 확인한 다음, 템플릿처럼 보이지 않는 개인화된 이메일을 작성하는 데 수 시간을 소비합니다. 그 작업의 대부분은 반복적인 패턴 매칭(Pattern-matching)입니다. 저는 AI 에이전트가 복사-붙여넣기 없이 더 잘, 더 빠르게 이 일을 수행할 수 있는지 확인하고 싶었습니다.
그 결과물은 SalesAgent입니다. LinkedIn URL을 붙여넣기만 하면, 조사된 리드 프로필, ML(머신러닝) 기반 점수(0–100), 그리고 초개인화된 콜드 이메일을 얻을 수 있습니다. 처음부터 끝까지 45초 미만이 소요됩니다. 템플릿도 없고, 수동 조사도 필요 없습니다. 그저 붙여넣고 실행하면 됩니다.
라이브 데모: salesagent-theta.vercel.app
GitHub: github.com/ayush-s-tomar/salesagent
작동 방식
- React 프론트엔드에 LinkedIn 프로필 URL을 붙여넣습니다.
- FastAPI 백엔드가 LangGraph 에이전트를 트리거합니다.
- 에이전트가 실시간 Tavily 웹 검색을 실행하여 리드와 해당 회사를 조사합니다.
- scikit-learn 모델이 6가지 신호를 기반으로 리드에게 0–100점의 점수를 부여합니다.
- Groq의 LLaMA가 실제 회사 이벤트를 참조하여 개인화된 콜드 이메일을 생성합니다.
- 리드 요약, 세부 내역이 포함된 점수, 그리고 바로 보낼 수 있는 이메일을 받게 됩니다.
실제로 작동하는 모습은 다음과 같습니다. 저는 Satya Nadella의 LinkedIn 프로필로 실행해 보았습니다. 에이전트는 Microsoft의 최근 AI 키노트 발표 내용을 찾아냈고, 이를 이메일에 직접 참조했습니다.
제목: Microsoft의 퀀텀 리프: AI 혁명에 발맞출 수 있습니까?
Satya 귀하, 7개의 새로운 MAI 모델 출시와 귀사의 양자 컴퓨터인 Majorana 2 도입을 포함하여, Microsoft의 AI 발전 사항에 대한 귀하의 최근 키노트를 매우 흥미롭게 시청했습니다...
이것은 템플릿이 아닙니다. 에이전트가 실시간으로 해당 뉴스를 찾아내어 그 내용을 바탕으로 작성한 것입니다.
아키텍처 (Architecture)
세 개의 노드로 구성되어 있으며, 각 노드는 다음 노드의 컨텍스트를 풍부하게 만듭니다. 스코어링(scoring) 노드는 LLM을 호출하지 않고, 이와 같은 분류 작업에는 더 빠르고 결정론적인 (deterministic) 훈련된 ML 모델을 실행합니다.
스택: LangGraph · FastAPI · React · scikit-learn · Groq · Tavily · Render · Vercel
각 부분이 작동하는 방식
1. 리서치 노드 (Research Node) — Tavily + LangGraph
에이전트는 잠재 고객(lead) 한 명당 Tavily의 검색 API를 두 번 호출합니다:
- 검색 1:
"{name} {company} LinkedIn"— 프로필 신호(직함, 요약, 기술)를 가져옵니다. - 검색 2:
"{company} news funding jobs 2024"— 최근 회사 활동을 확인합니다.
Tavily는 제목, URL, 콘텐츠 스니펫이 포함된 구조화된 결과를 반환합니다. LangGraph 리서치 노드는 이를 여섯 개의 이진/수치 신호로 처리하여 스코어러에 전달합니다:
signals = {
"has_company": bool, # 회사 이름이 알려져 있는가?
"has_title": bool, # 직함이 알려져 있는가?
...
has_news와 has_jobs는 가장 가치 있는 신호입니다. 이 두 가지는 회사가 현재 활동적이고 성장하고 있는지 여부를 알려줍니다. 이는 LinkedIn 요약 정보가 존재하는지 여부보다 더 중요합니다.
2. 스코어링 노드 (Scoring Node) — scikit-learn
스코어러는 numpy로 생성된 500개의 합성 샘플에 대해 훈련된 **경사 부스팅 분류기(Gradient Boosting Classifier)**를 사용합니다. 레이블은 가중치 공식(weighted formula)을 사용하여 할당되었습니다:
score = (
has_news * 0.30 + # 회사 관련 뉴스가 있음 = 관심도가 높은 잠재 고객
has_jobs * 0.25 + # 채용 중 = 성장하고 있으며, 예산이 존재함
...
왜 단순히 LLM (Large Language Model)으로 리드를 점수화하는 대신 ML (Machine Learning)을 사용할까요? 두 가지 이유가 있습니다: 속도와 결정론적 특성 (determinism)입니다. LLM 호출은 2~3초를 추가하며 실행할 때마다 다른 점수를 제공합니다. 반면 학습된 분류기 (classifier)는 밀리초 단위로 실행되며 동일한 입력에 대해 매번 동일한 점수를 제공합니다. 이는 사람들이 실제로 사용하는 무언가를 만들 때 매우 중요한 요소입니다.
실제 운영 환경(production)에서는 펀딩 단계, 회사 규모, 산업군, 이메일 응답률과 같은 더 풍부한 피처 (features)를 포함하여 실제 CRM 데이터(성공한 계약 vs 실패한 계약)로 재학습을 진행할 것입니다. 하지만 CRM 접근 권한이 없는 포트폴리오 프로젝트의 경우, 도메인 지식이 반영된 가중치 (weights)를 사용한 합성 학습 (synthetic training)을 통해 작동 가능하고 설명 가능한 점수 산출기 (scorer)를 얻을 수 있습니다.
3. 이메일 생성 노드 — Groq + LLaMA
이메일 노드는 이름, 직함, 회사, 최근 뉴스, 채용 공고와 같은 전체 리드 컨텍스트 (lead context)를 가져와 구조화된 프롬프트 (prompt)에 주입합니다:
System: 당신은 전문적인 B2B 영업 카피라이터입니다. 구체적이고 짧으며 실제 컨텍스트를 참조하는 이메일을 작성하세요. 일반적인 인사말을 절대 사용하지 마세요.
...
핵심 제약 조건은 **"일반적인 인사말을 절대 사용하지 마세요"**입니다. 이 조건이 없으면 LLaMA는 기본적으로 "I hope this email finds you well(이 이메일이 귀하에게 잘 전달되기를 바랍니다)"와 같은 문구를 사용합니다. 이 제약 조건이 있으면 모든 이메일은 회사의 실제 상황에 대한 구체적인 언급으로 시작됩니다.
무엇이 문제였나 (솔직한 부분)
이 부분이 제가 가장 많은 시간을 보낸 지점입니다. 실제 프로젝트는 튜토리얼에서 절대 보여주지 않는 방식으로 망가지곤 합니다.
1. Groq 모델 지원 중단 (Deprecations) — 세 차례
llama-3.3-70b-versatile이 실패했습니다. llama3-70b-8192로 전환했습니다. 그것도 서비스가 종료되었습니다. llama3-groq-70b-8192-tool-use-preview를 시도해 보았으나, 도구 호출 (tool-calling)이 제대로 작동하지 않았습니다. 결국 더 작지만 안정적인 llama-3.1-8b-instant로 정착했습니다.
교훈: 모델 문자열을 절대 하드코딩(hardcode)하지 마세요. 운영 시스템에서는 코드를 수정하지 않고도 교체할 수 있도록 설정 파일 (config file)이나 환경 변수 (environment variable)에 두어야 합니다.
2. 도구 호출 스키마 버그 — 400번의 생성 실패
Groq이 failed_generation 400 에러와 함께 제 도구 스키마 (tool schemas)를 거부했습니다. 문제를 격리하기 위해 여러 번 시도한 끝에, 원인은 properties와 required를 별도로 추출하지 않고 input_schema를 직접 전달했기 때문이라는 것을 알게 되었습니다.
잘못된 예:
"input_schema": tool.input_schema
올바른 예:
"parameters": {
"type": "object",
"properties": tool.input_schema["properties"],
...
에러 메시지(failed_generation)가 스키마 구조에 대한 어떠한 힌트도 주지 않았기 때문에 예상보다 시간이 더 오래 걸렸습니다. 만약 이 문제에 직면했다면, 먼저 도구 스키마 (tool schema)를 확인하세요.
3. graph.py와 llm.py 사이의 인터페이스 불일치 (Interface Mismatch)
graph.py는 run_with_tools(prompt=..., system=...)를 호출하고 (text, tool_log) 튜플 (tuple)이 반환되기를 기대했습니다. 반면 llm.py는 messages=[]를 인자로 받고 딕셔너리 (dict)를 반환하도록 작성되어 있었습니다. 별개로 작성된 두 파일 사이에서 발생하는 전형적인 인터페이스 불일치 사례입니다.
이로 인해 발생한 모든 버그 — prompt 대 messages 혼동, system 키워드 인자 (kwarg) 에러, 튜플 (tuple) 대 딕셔너리 (dict) 반환 타입 문제 — 는 타입이 지정된 인터페이스 계약 (typed interface contract)이 있었다면 몇 초 만에 잡아냈을 디버깅에 수 시간을 허비하게 만들었습니다.
4. Render에서의 Python 3.14
Python 3.14용 휠 (wheel) 파일이 존재하지 않아 pydantic-core 빌드에 실패했습니다. 해결 방법: Render의 환경 변수 (environment variables)에서 PYTHON_VERSION=3.11.9를 강제로 지정하세요.
Render에 배포하는 경우: 항상 Python 버전을 명시적으로 고정(pin)하세요. Render의 기본 설정을 신뢰하지 마세요.
5. Render에 캐싱된 ML 모델
scorer.py에서 점수 가중치 (scoring weights)를 재조정했음에도 불구하고, 이전의 model.pkl이 여전히 디스크에 캐싱되어 있었습니다. 매 배포 시 강제로 재학습을 수행하도록 빌드 명령에 rm -f ml/model.pkl을 추가하기 전까지 점수는 19/100에 멈춰 있었습니다.
이 문제는 매우 미묘했습니다. 코드는 맞았지만, 모델이 틀렸던 것입니다. 로그의 그 어떤 것도 모델이 오래되었다는 사실을 알려주지 않았습니다.
내가 다르게 했을 것이라면
첫날부터 LLM 인터페이스 계약 (interface contract)을 정의했을 것입니다.
가장 큰 버그의 원인은 graph.py와 llm.py가 함수의 시그니처 (function signatures), 반환 타입 (return types), 그리고 인자 이름 (argument names)에 대해 서로 다른 가정을 하고 있었다는 점이었습니다. 그리고 이러한 가정들은 어디에도 문서화되어 있지 않았습니다.
만약 제가 오늘 SalesAgent를 다시 만든다면, 가장 먼저 만들 파일은 다음과 같습니다:
# contracts.py — 다른 어떤 코드보다 먼저 작성됨
def run_with_tools(prompt: str, system: str) -> tuple[str, list[dict]]:
...
사전에 합의된 하나의 타입 지정 파일 (typed file)입니다. 인터페이스 불일치로 인한 모든 버그는 에이전트 로직을 단 한 줄이라도 작성하기 전에 포착되었을 것입니다.
그 외에도, 프로덕션 버전(production version)이라면 다음과 같은 작업을 수행했을 것입니다:
- 대화 메모리 (conversation memory) 추가: 에이전트가 과거의 아웃리치 (outreach) 결과(무엇이 효과적이었고, 무엇이 그렇지 않았는지)로부터 학습할 수 있도록 합니다.
- 합성 학습 데이터 (synthetic training data)를 스코어러 (scorer)를 위한 실제 CRM 데이터 (성사된/실패한 거래)로 교체합니다.
- 피드백 루프 (feedback loop)를 완성하고 결과에 따라 스코어러를 재학습시키기 위해 **이메일 오픈 트래킹 (email open tracking)**을 추가합니다.
직접 체험해 보세요
라이브 데모: salesagent-theta.vercel.app
GitHub: github.com/ayush-s-tomar/salesagent
어떤 LinkedIn URL이든 붙여넣어서 무엇을 생성하는지 확인해 보세요. 이메일의 품질은 Tavily가 얼마나 많은 정보를 찾아내느냐에 따라 달라집니다. 기업에 대한 공개 뉴스가 많을수록 결과물이 더 좋아집니다.
비슷한 것을 만들고 계시거나 머신러닝 (ML) 스코어링 방식에 대한 피드백이 있다면 진심으로 듣고 싶습니다. LinkedIn을 통해 저와 연결해 주세요.
스택 (Stack): LangGraph · FastAPI · React · scikit-learn · Groq LLaMA 3.1 · Tavily · Render · Vercel
태그 (Tags): #ai #python #machinelearning #langchain #buildinpublic
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기