자율 주행 영업 에이전트 구축: 토큰 최적화, 병렬 MCP 스로틀링(Throttling), 그리고 AI 아바타 비디오 파이프라인
요약
자율 주행 영업 에이전트인 'Ideal Customer Finder'를 구축하며 겪은 엔지니어링 도전과 해결책을 다룹니다. MCP 도구의 토큰 최적화와 병렬 호출 시 발생하는 지연 및 동시성 문제를 해결하기 위한 아키텍처 설계 과정을 설명합니다.
핵심 포인트
- MCP 도구의 검색 파라미터 최적화로 토큰 소비 57% 절감 및 속도 4.5배 향상
- 대규모 병렬 쿼리 처리를 위한 지능형 폴백 메커니즘 및 BaseConnector 패턴 구현
- 실시간 웹 검색과 AI 추론을 결합한 9개 노드 자율 상태 머신 설계
Qwen Cloud와 함께하는 Global AI Hackathon Series가 발표되었을 때, 저는 만연한 B2B 영업 문제 하나를 해결하기로 했습니다. 바로 어카운트 이그제큐티브(Account Executives)와 영업 개발 담당자(SDRs)가 업무 시간의 70% 이상을 수동 조사, 연락처 탐색, 이메일 초안 작성에 소비한다는 점입니다.
저의 목표는 Ideal Customer Finder (Track 4: Autopilot Agent)를 구축하는 것이었습니다. 이는 자연어 형태의 이상적 고객 프로필(ICP, Ideal Customer Profile)을 입력받아, 실시간 웹 검색을 수행하고, 연락처 정보를 보강하며, 구매 신호를 감지하고, 이중 AI/벡터 추론(dual AI/vector reasoning)을 사용하여 계정 점수를 매긴 뒤, 개인화된 아웃리치(outreach) 초안을 작성하는 9개 노드의 자율 상태 머신(autonomous state machine)입니다.
설계 이론에서 Qwen Cloud 및 Alibaba Cloud 기반의 실제 운영 가능한 시스템으로 넘어가는 과정에는 실제적인 엔지니어링 마찰, 심도 있는 디버깅, 그리고 결정적인 아키텍처 전환이 포함되었습니다. 여기 그 가감 없는 구축 이야기를 공개합니다.
1. 토큰 폭발 제어하기: MCP 도구 최적화
저의 파이프라인은 일치하는 기업과 임원 연락처를 찾아내기 위해 실시간 웹 검색에 의존합니다. 웹 검색을 통합하기 위해, 저는 Qwen의 Responses API를 통해 노출된 Nimble MCP 서버(type: "mcp" 도구인 nimble_search 호출)를 사용했습니다.
신호 감지를 위한 초기 테스트 실행 시, 저는 거대한 장애물에 부딪혔습니다. 단 한 번의 검색 호출이 35,000 토큰 이상을 소비했으며, 실행하는 데 97.6초가 걸렸습니다.
Nimble MCP 서버의 원시 파라미터 스키마(inspect_nimble_mcp_tools.py)를 조사한 결과, 그 원인을 발견했습니다. nimble_search가 가벼운 검색 스니펫(snippets) 대신 전체 페이지 마크다운(markdown) 웹 추출을 기본값으로 사용하고 있었던 것입니다.
저는 에이전트의 시스템 프롬프트(system prompt)를 업데이트하여 엄격한 검색 파라미터를 명시적으로 전달하도록 했습니다:
{
"search_depth": "lite",
"time_range": "month",
...
결과: 토큰 소비량이 57% 감소했으며(약 35,000개에서 약 15,000 토큰으로), 실제 실행 시간(wall-clock execution time)은 97.6초에서 21.5초로 단축되어 4.5배의 성능 향상을 이루었습니다.
2. 고동시성 도구 호출 오케스트레이션
단일 계정 테스트에서 전체 에이전트 실행으로 규모를 확장할 때, 파이프라인은 8개의 계정을 보강하고 32개의 동시 신호 탐지 쿼리(자금 조달, 채용 및 확장 이벤트 전반에 걸쳐)를 동시에 실행해야 합니다.
asyncio.gather를 통해 이 정도의 병렬 부하를 LLM 추론 엔진(reasoning engine)으로 밀어 넣을 때, 단일 MCP 도구 호출(tool-calling) 경로에만 의존하면 지연 시간 병목 현상이 발생하고 표준 클라우드 동시성 제한(concurrency limits)에 걸릴 위험이 있습니다.
엔지니어링 피벗 (The Engineering Pivot)
프로덕션급 신뢰성을 보장하기 위해, 저는 지능형 폴백(fallback) 메커니즘(USE_NIMBLE_MCP)을 갖춘 유연한 BaseConnector 패턴을 구현했습니다.
- 1단계 (계정 소싱 - 단일 쿼리): Qwen과 Nimble MCP 서버(
qwen3.7-plus)를 통해 라우팅하며, 초기 검색을 파싱하기 위해 Qwen의 네이티브 MCP 오케스트레이션(orchestration)을 활용합니다. - 2단계 및 3단계 (연락처 및 신호 - 높은 병렬 부하):
asyncio.Semaphore(6)동시성 제한을 통해 REST API로 직접 라우팅하도록 우아하게 성능을 저하시킵니다(gracefully degrades).
이 하이브리드 접근 방식은 두 가지 장점을 모두 달성했습니다. 즉, 복잡한 도구 오케스트레이션을 위해 Qwen의 추론(reasoning) 능력을 활용하는 동시에, 높은 병렬 부하 하에서도 전체 파이프라인 실행 속도(엔드 투 엔드 약 33.3초)를 유지했습니다.
3. xargs 환경 변수 함정
로컬 Docker 테스트 중에 .env 파일에 OPENAI_API_KEY가 지정되어 있음에도 불구하고, PostgreSQL에서 벡터 임베딩(vector embeddings)이 NULL을 반환하는 것을 발견했습니다.
컨테이너의 entrypoint.sh 내부에서 환경 변수는 흔히 사용되는 셸 관용구(shell idiom)를 사용하여 내보내기(export)되고 있었습니다:
# 오류 발생: xargs는 특수 문자를 기준으로 분할함
export $(grep -v '^#' /app/.env | xargs)
Dashscope API 키에 자연스럽게 특수 문자(+ 및 /)가 포함되어 있었기 때문에, xargs가 단어 분할(word-splitting) 과정에서 키 값을 잘라버렸습니다. Python 런타임은 잘못된 형식의 키를 수신하게 되었고, 이로 인해 embeddings.py가 조용히 None을 반환했습니다.
저는 로딩 로직을 셸 네이티브(shell-native) 변수 내보내기 방식으로 교체했습니다:
# 수정 완료: 특수 문자를 원활하게 처리함
set -a
source /app/.env
...
적용된 후, text-embedding-v4는 ICP(Ideal Customer Profile)와 계정 모두에 대해 1536차원 벡터를 생성하기 시작했으며, 이를 통해 UI에서 이중 점수 산출(dual scoring)이 가능해졌습니다: AI 추론 적합도 (AI Reasoning Fit) (Qwen LLM) + 의미론적 유사도 (Semantic Closeness) (pgvector 코사인 유사도).
4. 음성 I/O: 멀티모달 SDK 탐색하기
에이전트 경험을 진정으로 자율적으로 만들기 위해, 저는 완전한 음성 I/O를 원했습니다. 즉, 영업 담당자가 자신의 ICP를 받아쓰기(Speech-to-Text, STT)하고, 에이전트가 진행 상황을 설명하는 것을 듣고(Text-to-Speech, TTS), 심지어 비디오 아웃리치(outreach)를 위해 **자신의 목소리를 복제(clone)**할 수 있는 기능입니다.
STT: OSS 업로드 함정 피하기
마이크 입력을 구현하기 위해, 저는 초당 비용이 1센트의 아주 작은 부분에 불과하면서도 목적에 맞게 제작된 고정밀 ASR(Automatic Speech Recognition) 모델인 qwen3-asr-flash를 활용했습니다.
Next.js 프론트엔드에 MediaRecorder를 연결하여 오디오 블롭(audio blob)을 FastAPI로 전송하고, 이를 임시 파일로 저장한 뒤 Dashscope SDK에 전달하도록 구성했습니다. 그러자 즉시 오류가 발생했습니다: UploadFileException: Get upload certificate failed.
해결 방법: Dashscope SDK가 로컬 file:// URI를 감지하면, 모델에 URL을 전달하기 전에 파일을 Alibaba Cloud OSS 버킷으로 업로드하려고 시도하는 편리한(하지만 여기서는 곤란한) 동작을 수행합니다. 제 API 키는 추론(inference) 용도로만 엄격히 제한되어 있었기 때문에, 이 스토리지 요청은 거부되었습니다. 이를 해결하기 위해 임시 파일을 완전히 우회했습니다. 백엔드에서 오디오 블롭을 데이터 URI(data:audio/webm;base64,...)로 base64 인코딩함으로써, SDK가 형식을 인식하고 JSON 페이로드 내에 오디오 바이트를 인라인(inline)으로 스마트하게 전송하도록 만들었습니다. 즉각적인 전사(transcript)가 가능해졌고, 외부 스토리지는 전혀 필요하지 않게 되었습니다.
TTS: 실시간 내레이션
출력을 위해, Qwen3-TTS-Flash-Realtime을 사용하여 WebSocket 기반의 실시간 오디오를 통합했습니다.
[사용자 로그인] ──► 대시보드 ──► 오디오: "환영합니다, Solo Han님. 승인을 기다리는 5개의 아웃리치 초안이 있습니다." (0.8x 속도)
[에이전트 실행 중] ──► 잠재 고객 UI ──► 오디오: "구매 신호를 찾기 위해 라이브 웹을 스캔 중입니다..." (1.0x 속도)
최신 브라우저의 자동 재생 정책(사용자의 사전 동작 없이 AudioContext를 차단함)을 준수하기 위해, 초기 WebSocket 연결을 사용자의 로그인 버튼 클릭 이벤트에 체이닝(chaining)하여 브라우저 정책 오류 없이 오디오가 부드럽게 재생되도록 했습니다.
음성 복제 (Voice Cloning): 퍼블릭 URL 및 코덱의 특이사항
비디오 훅(video hooks)을 진정으로 개인화하기 위해, cosyvoice-v3-plus를 통한 음성 복제(voice cloning) 기능을 추가했습니다. 영업 담당자가 브라우저에서 10초 분량의 샘플을 녹음하면, Qwen이 이를 복제하여 아바타의 음성을 구동합니다.
문제점 (퍼블릭 URL 제약): base64 데이터 URI를 허용하는 STT API와 달리, Dashscope의 VoiceEnrollmentService는 참조 오디오를 가져오기 위해 엄격하게 퍼블릭 HTTP/HTTPS URL을 요구합니다.
해결책: Nginx 리버스 프록시(reverse proxy)를 활용했습니다. 백엔드는 사용자의 오디오 블롭(audio blob)을 디스크에 임시로 저장하고, Dashscope가 이를 가져가서 voice_id를 생성할 수 있을 만큼 충분한 시간(약 30초) 동안 퍼블릭 엔드포인트를 통해 제공한 뒤, finally 블록에서 안전하게 삭제합니다.
문제점 (코덱 화이트리스트 vs 브라우저 표준): 브라우저의 MediaRecorder는 기본적으로 압축된 오디오를 사용하며, 이는 음성 복제의 음향 품질을 약간 저하시킬 수 있습니다. 저는 audio/webm;codecs=pcm을 사용하여 브라우저가 비압축 오디오를 출력하도록 강제하려 했습니다. 하지만 Dashscope에 전달했을 때, API는 PCM 데이터를 담고 있는 Matroska/WebM 컨테이너를 UnsupportedFileFormat 오류와 함께 완전히 거부했습니다.
해결책: 브라우저의 기본 네이티브 코덱으로 되돌렸습니다. 이는 프론트엔드 Web API와 백엔드 AI 모델을 연결할 때, 엄격한 클라우드 포맷 화이트리스트로 인해 표준 브라우저 폴백(fallback)에 의존할 수밖에 없는 경우가 많다는 점을 극명하게 상기시켜 주었습니다.
5. 멀티모달의 성배: 3개 모델 기반 토킹 헤드(Talking-Head) 비디오 파이프라인 체이닝
해커톤 마감 기한이 연장됨에 따라, 저는 멀티모달 개인화에 모든 것을 걸기로 했습니다. 영업 담당자가 이메일 초안을 검토하고 승인하면, 해당 이메일에 첨부할 수 있는 개인화된 토킹 헤드(talking-head) 비디오 훅을 생성할 수 있도록 만들고 싶었습니다.
이를 위해서는 완전히 별개인 세 개의 Qwen/Dashscope 모델을 비동기 순차 체인(asynchronous sequential chain)으로 오케스트레이션(orchestrating)해야 했습니다.
[Value Hypothesis]
│
▼
...
이 파이프라인이 프로덕션급(production-grade)의 자연스러운 비디오를 생성하도록 만드는 과정에는 매우 구체적인 엔지니어링 장애물들을 해결해야 하는 과제가 따랐습니다.
문제 A: 다국어 엣지 케이스(Edge Cases) 및 정규 표현식 리듬 해킹 (Regex Cadence Hack)
표준 LLM(Large Language Models)은 귀가 아닌 눈에 최적화된 텍스트를 작성합니다. 말하기 위한 스크립트를 생성하기 위해 저는 qwen-flash-character를 활용했습니다. Qwen 모델은 매우 뛰어난 다국어 엔진이기 때문에, 때때로 영어 스크립트에 비-ASCII(non-ASCII) 문자를 삽입하곤 했습니다.
이러한 문자들이 TTS(Text-to-Speech) 엔진에 직접 전달되면 흐름을 방해하게 됩니다. 하지만 단순히 프로그래밍 방식으로 이 문자들을 제거해 버리면, 인접한 단어들이 공백 없이 서로 붙어버리는 문제가 발생했습니다.
해결책: 저는 생성된 스크립트가 TTS 엔진에 도달하기 전에 가로채는 Python 기반의 정규 표현식(regex) 후처리 필터를 작성했습니다:
# 모든 비-ASCII 문자를 쉼표와 공백으로 교체
script = re.sub(r'[^\x00-\x7F]+', ', ', script)
# 중복 공백 제거
...
이를 통해 포맷팅 엣지 케이스를 주요 자산으로 전환할 수 있었습니다. 비-ASCII 문자가 오디오 상에서 깔끔한 언어적 휴지(comma + space)로 변환되어, 화자가 자연스러운 리듬(cadence)을 가질 수 있게 되었습니다.
문제 B: 프로그래밍 방식의 립싱크 패딩 (Programmatic Lip-Sync Padding)
wan2.7-i2v를 사용한 초기 테스트 과정에서 우리는 매우 거슬리는 시각적 아티팩트(artifact)를 발견했습니다. 생성된 비디오의 입술이 0번 프레임에서 즉시 툭 하고 열리고, 오디오가 끝나는 순간 툭 끊기듯 멈춰버리는 현상이었습니다.
이를 해결하기 위해 저는 Python으로 인메모리(in-memory) 오디오 패딩 루틴을 작성했습니다. 이 스크립트는 qwen3-tts-flash-realtime이 생성한 원본 PCM/WAV 오디오를 가져와서, 비디오 모델로 전송될 최종 base64 데이터 URI로 변환하기 전에 앞부분(head)에 0.3초의 순수 무음을 프로그래밍 방식으로 추가하고, 뒷부분(tail)에 0.2초의 무음을 추가합니다. 이를 통해 비디오 생성기가 부드럽고 자연스러운 얼굴 전환을 애니메이션화하는 데 필요한 시각적 버퍼를 확보할 수 있었습니다.
문제 C: 장시간 실행되는 생성 작업의 UX
15초 분량의 고해상도 비디오를 생성하는 데는 110초에서 150초가 소요됩니다. 이 시간 동안 표준 UI 진행 표시줄(Progress bar)이나 카운트다운을 사용하는 것은 매우 오해를 불러일으킬 수 있습니다. 만약 API에서 지연(Latency)이 발생하면, 작업이 여전히 실행 중임에도 불구하고 진행 표시줄이 멈추거나 0%에 도달하여 시스템이 고장 난 것처럼 보이기 때문입니다.
진행 표시줄로 "거짓말"을 하는 대신, 저는 프론트엔드에 경과 시간을 실시간으로 보여주는 **라이브 타이머(live-ticking elapsed timer)**를 구축하여 시간이 올라가도록(0:47 / ~2:00) 만들었습니다. 저는 이를 Tailwind CSS의 tabular-nums 클래스로 스타일링했는데, 이는 숫자가 올라갈 때 화면에서 숫자가 흔들리거나 너비가 변하는 것을 방지하여 견고하고 전문적인 UI를 유지해 줍니다.
6. 배포 아키텍처: Alibaba Cloud ECS (홍콩)
프로덕션 백엔드 호스팅을 위해, 저는 Alibaba Cloud Elastic Compute Service (ECS) (ecs.e-c1m2.large - 2 vCPU / 4 GiB RAM)를 선택했습니다.
┌─────────────────────────────────────────────────────────────────────────────────┐
│ ALIBABA CLOUD ECS (China — Hong Kong, Ubuntu 22.04 LTS) │
│ │
...
HTTPS / 마이크 제약 사항
핵심 백엔드 로직은 기술적으로 일반 HTTP를 통해 작동하지만, 음성-텍스트 변환 (STT, Speech-to-Text) 및 음성 복제 (Voice cloning) 기능을 구현하면서 엄격한 아키텍처 요구 사항이 발생했습니다. 현대의 브라우저는 하드웨어 접근을 위해 보안 컨텍스트(window.isSecureContext)를 강제합니다. 로컬 개발 중 http://localhost:8080에서는 MediaRecorder와 getUserMedia가 완벽하게 작동했지만, 일반 ECS 퍼블릭 IP로 배포하자 마이크 접근이 완전히 차단되었습니다.
이러한 브라우저 보안 정책은 ECS 호스트에 직접 **Nginx 리버스 프록시 (Reverse proxy)**와 Let's Encrypt SSL 인증서를 배치하여 Docker 컨테이너 전면에 두게 만든 결정적인 원인이 되었습니다.
왜 중국(홍콩)인가?
- 저지연 라우팅 (Low Latency Routing): 홍콩은 중국 본토의 Dashscope API 엔드포인트로 직접 연결되는 저지연 광섬유 연결(CN2 GIA)을 제공합니다.
- ICP 비안 (ICP Filing) 지연 없음: 홍콩은 중국 본토 규제 구역 밖에 있기 때문에, 80, 443, 8080 포트의 공개 웹 트래픽을 중국 본토의 ICP 라이선스 승인을 위해 몇 주씩 기다릴 필요 없이 즉시 서비스할 수 있습니다.
Docker 컨테이너 업데이트 시 데이터 지속성 (Data Persistence)을 보장하기 위해, PostgreSQL 17 및 pgvector 0.8.0은 ECS 호스트에 직접 설치되었으며, 애플리케이션 서비스 (Next.js, FastAPI, Celery, Redis)는 단일 supervisord 관리 Docker 컨테이너 내부에서 실행됩니다.
핵심 요약: 이를 구축하며 배운 점
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기