하나의 에이전트, 네 개의 채널: Qwen Cloud 기반 구축 노트
요약
Qwen Cloud를 활용하여 WhatsApp, 이메일, 웹, 음성 등 4개 채널을 통합 관리하는 의료용 AI 에이전트 'MainDesk' 구축 사례를 소개합니다. LangGraph와 FastAPI를 사용하여 긴급 상황 시 인간 개입(human-in-the-loop)이 가능하도록 설계되었습니다.
핵심 포인트
- LangGraph 기반의 상태 머신을 통한 의도 분류 및 에스컬레이션 구현
- WhatsApp, 이메일, 웹, Twilio 음성을 통합하는 멀티 채널 에이전트 구축
- Qwen Cloud 사용 시 국제 워크스페이스를 위한 올바른 엔드포인트 설정 주의
- 의료 긴급 상황 대응을 위한 Human-in-the-loop 워크플로우 적용
한 환자가 오후 8:47에 우리에게 WhatsApp 메시지를 보냈습니다:
"한 시간 동안 가슴을 짓누르는 듯한 통증이 있어요. 어떻게 해야 하나요?"
약 400밀리초(milliseconds) 만에 두 가지 일이 일어났습니다. 에이전트 — FastAPI 뒤에서 실행되며 DashScope의 OpenAI 호환 엔드포인트를 통해 qwen3.7-plus를 호출하는 LangGraph 상태 머신(state machine) — 는 의도(intent)를 escalate(에스컬레이션)로 분류하고, 자신의 답변 생성을 중단한 뒤 /staff에 있는 human-in-the-loop(인간 개입) 큐에 레드 카드를 게시했습니다. 환자에게는 "상담원을 연결해 드릴 테니 잠시만 기다려 주세요"라는 짧은 확인 메시지가 전달되었습니다. 당직 의료진이 카드를 확인하고 "지금 즉시 응급실로 가세요. 거기서 저희에게 전화해 주세요"라고 입력하자, 1초도 안 되어 동일한 WhatsApp 스레드에 메시지가 도착했습니다.
이것이 제가 지난 일주일 동안 Qwen Cloud Hackathon Track 4를 위해 구축한 데모입니다. 이 포스트는 상용구(boilerplate)가 아닌, 흥미로운 부분들을 다룹니다.
MainDesk의 실체
MainDesk는 환자가 사용할 수 있는 모든 채널 — WhatsApp, 이메일, 웹 채팅, 그리고 음성(Twilio를 통한 실제 전화번호 또는 다운로드가 필요 없는 브라우저 통화 위젯) — 에서 영어 또는 중국어로 하루 종일 응대하는 클리닉의 자율 프런트 데스크입니다. 실제 예약 가능 여부를 확인하기 위해 Google Calendar 연동 기능이 내장되어 있으며, 아직 캘린더를 연결하지 않은 클리닉을 위한 폴백(fallback)용으로 로컬 Postgres 스케줄러가 준비되어 있습니다(이 데모에서는 아직 연결하지 않았습니다). 모델의 신뢰도(confidence)가 0.45 미만으로 떨어지거나 의도가 의료적 긴급 상황으로 보일 경우 인간 대시보드로 에스컬레이션합니다.
전체 시스템은 싱가포르에 있는 월 $32짜리 Alibaba Cloud ECS 인스턴스에서 실행되며, maindesk.otito.site를 통해 Let's Encrypt 기반의 TLS를 사용합니다. 위젯에서 你好,我想预约下周二의 检查(안녕하세요, 다음 주 화요일 검진을 예약하고 싶습니다)라고 입력해 보세요. 작동하며, 실제 예약 가능한 시간대와 함께 중국어로 답변이 돌아옵니다.
코드: github.com/Otitodev/Maindesk.
이제, 흥미로운 부분입니다.
한 시간을 허비하게 만든 Qwen Cloud의 함정
인터넷에 있는 모든 DashScope 튜토리얼은 동일한 코드 스니펫을 보여줍니다:
client = OpenAI(
api_key=os.getenv("DASHSCOPE_API_KEY"),
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1"
...
Qwen Cloud 워크스페이스(기존 Model Studio와는 다름)에서는 두 가지가 잘못되었습니다:
- 엔드포인트 (Endpoint):
dashscope가 아니라dashscope-intl.aliyuncs.com입니다. 중국 본토 엔드포인트는 국제 워크스페이스 키를 사용할 경우 혼란스러운 문구의401 invalid_api_key에러를 반환합니다. 실제 문서에서-intl을 찾아내는 데 20분이 걸렸습니다. - 모델 ID (Model IDs):
qwen-plus와qwen-turbo는 여전히 레거시 별칭(legacy aliases)으로 작동하지만, 문서에서 적극적으로 권장하는 최신 세대 제품군은qwen3.7-plus(균형 잡힌 모델),qwen3.7-max(고성능 추론 모델), 그리고qwen3.6-flash(빠르고 저렴한 모델)입니다. 임베딩(Embedding) 또한text-embedding-v3에서text-embedding-v4로 업그레이드되었으며, 이 과정에서 새로운 지원 차원(dimensions: 128, 256, 1536, 2048)이 조용히 추가되었습니다.
현재 Qwen Cloud 기반으로 구축 중이라면, 실제로 작동하는 설정은 다음과 같습니다:
# app/config.py
dashscope_api_key: str = ""
qwen_api_base: str = "https://dashscope-intl.aliyuncs.com/compatible-mode/v1"
...
네, 환경 변수는 DASHSCOPE_API_KEY입니다. 플랫폼 이름은 Qwen Cloud이지만, 인증 헤더(auth header)는 DashScope의 이름을 상속받습니다. 저는 문서의 관례에 맞추기 위해 Pydantic 필드 이름을 dashscope_api_key로 명명했는데, 이렇게 하니 코드가 Qwen Cloud에 맞춰 적응한 것이 아니라 마치 Qwen Cloud를 위해 작성된 것처럼 읽히기 시작했습니다. 사소한 부분이지만, 심사위원들이 리포지토리(repo)를 검토할 때는 중요한 차이를 만듭니다.
하나의 오케스트레이터, 네 개의 게이트웨이
설계상의 핵심 베팅은 다음과 같았습니다: 네 개의 채널 모두가 동일한 LangGraph 오케스트레이터(orchestrator), 동일한 도구 계층(tool layer), 동일한 pgvector 메모리, 동일한 비밀 정보 삭제기(secret redactor)를 통과한다. 파싱(parsing) 이외의 채널별 비즈니스 로직은 전혀 두지 않았습니다.
WhatsApp 어댑터, 이메일 어댑터, 웹 어댑터, 음성 어댑터 — 각 어댑터는 약 30줄 내외입니다. 이들은 인바운드 페이로드(inbound payload)를 {session_id, content, phone} 트리플(triple)로 정규화하여 다음으로 밀어 넣습니다:
result = await graph.ainvoke(
{"messages": [HumanMessage(content=text)], "session_id": sid, "phone": phone},
config={"configurable": {"thread_id": sid}},
...
그 이후의 모든 과정 — 분류 (triage), 메모리 회상 (memory recall), 도구 호출 (tool calls), 응답 생성 (response generation), 외부 유출 비밀 정보 삭제 (outbound secret redaction) — 은 모든 채널에서 동일하게 실행됩니다. 웹 위젯에서 방금 예약을 마친 중국어 메시지는, 나이지리아 환자가 클리닉 번호로 전화를 걸었든 브라우저 탭에서 전화 걸기를 클릭했든 상관없이 음성 통화와 정확히 동일한 코드 경로를 실행합니다.
이러한 동일성은 미적인 요소가 아니라, 핵심적인 목적입니다. 이는 인간 개입 (HITL, Human-in-the-loop) 에스컬레이션이 모든 채널에서 동일한 방식으로 작동함을 의미합니다.
qwen3.7-plus는 단 하나의 로케일 (locale) 프롬프트 없이도 유창한 중국어를 구사합니다. 데모 예약 흐름(booking flow)은 별도의 지시 없이도 적절하게 형식화된 중국어 날짜(2026年7月3日 上午 9:00)를 반환합니다. 시스템 프롬프트에는 중국어를 단 한 글자도 넣지 않았습니다.- 전체 예약 왕복 (round-trip) 과정은 한 번이 아니라 두 번의 LLM 호출로 이루어집니다. 분류 (Triage) → 회상 (recall) → 도구 (tool) → 생성 (generate) 순서입니다. 콜드 스타트 (Cold-start) 시 약 8초, 웜 (warm) 상태 시 약 5~6초가 소요됩니다. 만약 3초 미만의 응답 속도를 원한다면, 분류 (triage) 단계를 Flash 모델로 라우팅해야 합니다.
- Alibaba Cloud CLI의
--output json플래그는 테이블 전용입니다. JSON이 기본값입니다. 저는bad flag format --output with field cols= required오류 때문에 15분을 허비했습니다. 지구상의 모든 CLI는--output json을 지원하지만, 이 도구는 그것이 반대로 되어 있습니다. 이 사실을 근육 기억 (muscle memory)에 저장해 두세요.
검색(grepping)을 위한 기술 스택
- 오케스트레이터 (Orchestrator): LangGraph 1.2 +
AsyncPostgresSaver체크포인트 - LLM: 생성용
qwen3.7-plus, 분류용qwen3.6-flash(DashScope OpenAI 호환 엔드포인트 사용) - 임베딩 (Embeddings): Postgres 16의 pgvector에 1024 차원의
text-embedding-v4사용 - 음성 (Voice): Pipecat (Twilio 전화 + 자체 호스팅 브라우저 콜 위젯) +
qwen3.7-plusLLM + Deepgram STT + ElevenLabs TTS - 인그레스 (Ingress): FastAPI + slowapi 속도 제한기 (rate limiter) + Caddy 2 TLS 리버스 프록시 (reverse proxy)
- 배포 (Deployment): 싱가포르의 Alibaba Cloud ECS
ecs.e-c1m2.large상의 Docker Compose - 인간 개입 (Human-in-loop):
/staff경로의 FastAPI + HTMX + SSE 대시보드 - 테스트 (Tests): 217개 통과, 그리고 세 가지 중국어 케이스를 포함한 33개 케이스의 의도 평가 (intent eval, 정확도 97%)
여섯 개의 복잡한 컨테이너, 약 7,200줄의 Python 코드, 주말 내내 이어진 Alibaba Cloud 콘솔 클릭, 그리고 PowerShell의 인자 파서 (argument parser)를 향한 약간의 고함.
만약 당신도 Qwen Cloud 기반으로 구축한다면
과거의 나에게 해주고 싶은 두 가지가 있습니다:
docs.qwencloud.com/developer-guides/getting-started/introduction을 먼저 읽으세요. 그 한 페이지에 엔드포인트 (endpoint), 환경 변수 (env var), 그리고 모델 ID (model IDs)가 모두 들어 있습니다. 저는 Qwen Cloud 리브랜딩 이전의 오래된 DashScope 튜토리얼을 읽었기 때문에, 원래 제 코드에는 이 세 가지가 모두 잘못 설정되어 있었습니다.- 에이전트 루프 (agent loop)를 직접 구현하지 마세요. LangGraph와 OpenAI SDK를 사용하면 90%의 기능이 즉시 제공됩니다. 흥미로운 엔지니어링 요소는 LangGraph가 이미 제공하는 기능을 재구현하는 것이 아니라, 도구 (tools) (캘린더, 메모리, 개인정보 삭제기(redactor))와 탈출구 (escape hatches) (인간 개입 (HITL), 신뢰도 임계값 (confidence thresholds))에 있습니다.
이것이 구축 과정입니다.
- 리포지토리 (Repo): github.com/Otitodev/Maindesk
- 라이브 데모 (Live demo): maindesk.otito.site/chat
- 문서 (Docs): DEPLOY_ALIBABA_ECS.md, DEMO_SCRIPT.md
질문은 댓글로 남겨주세요. 만약 여러분도 해커톤을 위해 고군분투 중이라면, 행운을 빕니다 — 제출은 7월 20일에 마감됩니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기