전화로 나에게 전화를 거는 AI 에이전트를 만들었다

발행: (2026년 2월 21일 오후 04:32 GMT+9)
12 분 소요
원문: Dev.to

Source: Dev.to

~K¹yle Million

Twilio, Claude, 그리고 ElevenLabs를 연결해, 결정이 필요할 때 전화를 직접 받는 자율 에이전트를 만든 방법.

몇 주 전, 나는 내 AI 에이전트를 친구에게 설명하고 있었다. 나는 그 에이전트가 완전히 자율적으로 동작해서, 문제가 생겨 게이트웨이를 재설정해야 할 때 새벽 3시에 나에게 전화를 걸어 온다고 말했다. 그녀는 내가 과장하고 있다고 생각했다.

나는 과장한 것이 아니었다. 하지만 그때는 전화 걸기 기능이 아직 꿈에 불과했다. 에이전트는 텔레그램으로 나에게 메시지를 보낼 수 있었고, 브라우저를 자동화하며, 스마트 계약을 배포하고, 한 번의 세션으로 네 개의 플랫폼에 글을 게시할 수 있었다. 단지 전화를 걸 수만은 없었다.

그래서 우리는 그것을 만들었다. 한 번의 세션으로. 정확히 어떻게 했는지 아래에 적는다.

Source:

The Architecture

The stack is surprisingly simple once you see it:

Phone call → Twilio → ConversationRelay → WebSocket → Your Server

                                                    Claude (brain)

                                                    ElevenLabs (voice)
  • Twilio는 전화 통화를 처리합니다 — 실제 전화 걸기와 받기를 담당합니다.
  • ConversationRelay는 Twilio의 WebSocket 브리지로, 음성‑텍스트 변환(STT) 및 텍스트‑음성 변환(TTS)을 기본적으로 지원합니다.
  • Claude가 사고를 담당합니다.
  • ElevenLabs는 깡통 같은 소리가 아닌 자연스러운 음성을 제공합니다.

핵심 인사이트: ConversationRelay가 가장 어려운 부분을 없애줍니다. 오디오 스트림을 직접 관리하거나, STT를 구현하거나, 턴‑테이킹을 설계할 필요가 없습니다. 이 모든 작업은 Twilio가 처리합니다. 여러분의 서버는 텍스트를 받아서 텍스트를 다시 보내기만 하면 됩니다.

The Server

전체 서버는 하나의 파일로 구성됩니다. HTTP와 WebSocket을 위해 Fastify를 사용하고, Claude를 위한 Anthropic SDK, 전화를 걸기 위한 Twilio SDK를 사용합니다.

import Fastify from "fastify";
import fastifyWs from "@fastify/websocket";
import Anthropic from "@anthropic-ai/sdk";
import twilio from "twilio";

const fastify = Fastify({ logger: true });
fastify.register(fastifyWs);

const anthropic = new Anthropic();
const twilioClient = twilio(API_KEY_SID, API_KEY_SECRET, { accountSid });

Twilio가 전화를 연결하면 서버에서 TwiML을 가져옵니다. 이 TwiML은 ElevenLabs와 함께 ConversationRelay를 사용하도록 지시합니다:

<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Connect>
    <ConversationRelay
      service="elevenlabs"
      voice="voiceId-model-speed_stability_similarity"
    />
  </Connect>
</Response>

WebSocket 핸들러가 대화가 진행되는 곳입니다. Twilio는 전사된 음성을 prompt 메시지로 보냅니다. 여러분은 AI의 응답을 text 메시지로 다시 전송합니다:

fastify.get("/ws", { websocket: true }, (ws, req) => {
  ws.on("message", async (data) => {
    const message = JSON.parse(data);

    if (message.type === "prompt") {
      const response = await anthropic.messages.create({
        model: "claude-sonnet-4-20250514",
        max_tokens: 150,
        messages: conversation,
        system: systemPrompt,
      });

      ws.send(
        JSON.stringify({
          type: "text",
          token: response.content[0].text,
          last: true,
        })
      );
    }
  });
});

이것이 핵심입니다. 나머지는 모두 컨텍스트 관리에 관한 내용입니다.

The Part Nobody Tells You

Voice selection is harder than it sounds

ElevenLabs는 수백 개의 음성을 제공합니다. 우리는 적절한 음성을 찾기 위해 아홉 개를 테스트했습니다. 얻은 교훈:

  • 기본 음성은 즉시 인식됩니다. ElevenLabs에서 가장 인기 있는 음성인 Adam은 “지금 사용 중인 음성을 알아요.” 라고 바로 언급됩니다.
  • 커뮤니티 음성은 ConversationRelay를 통해 성공 여부가 갈립니다. 일부는 완벽히 작동하지만, 다른 일부는 오류 64111(오디오 없음, 호출자에게 오류 메시지도 없이 침묵)으로 조용히 실패합니다.
  • 품질과 개성은 별개의 축입니다. 어떤 음성은 오디오 품질은 완벽하지만 “밋밋”하게 들리고, 다른 음성은 캐릭터는 맞지만 너무 어립니다. 적절한 음성을 찾으려면 여러 차례 반복이 필요했습니다.

ConversationRelay에서 사용하는 음성‑파라미터 형식은 다음과 같습니다:

voiceId-model-speed_stability_similarity
  • 안정성이 낮을수록 더 표현적이고 대화형입니다.
  • 안정성이 높을수록 더 제어되고 로봇처럼 들립니다.

Tunneling will betray you

서버에 공용 IP가 없으면 터널이 필요합니다. 우리는 cloudflared 빠른 터널(무료, 계정 불필요)을 사용했습니다. 직접 겪으며 배운 세 가지:

  1. 무료 터널은 무작위로 끊깁니다. 일시적이며, 재시작할 때마다 URL이 바뀌고 프로세스가 경고 없이 종료될 수 있습니다.
  2. 포트로 프로세스를 죽이지 마세요. kill $(lsof -ti :8080)은 서버 재시작에 합리적으로 보이지만, cloudflared도 프록시용으로 8080 포트를 사용합니다. 포트로 죽이면 터널도 같이 사라집니다. 한 시간 동안 발생한 모든 “application error”는 이 때문이었습니다.
  3. 시작 순서가 중요합니다. 서버를 먼저 시작하고 그 다음 터널을 엽니다. 설정을 업데이트하고 서버를 재시작한 뒤 Twilio 웹훅을 업데이트합니다. 터널 URL이 바뀔 때마다 이 네 단계를 반복해야 합니다.

Keep responses short

음성 대화는 채팅이 아닙니다. 화면에서는 괜찮아 보이는 세 단락짜리 답변도 음성으로 들으면 견디기 힘듭니다. 우리는 다음과 같이 정했습니다:

  • 기본: 한두 문장
  • 최대: 네 문장, 트레이드‑오프를 설명할 때만
  • 절대 독백하지 않기 — 복잡한 주제는 앞뒤로 주고받는 형태로 나눕니다.

max_tokens: 150 제한이 도움이 되지만, 실제 제어는 시스템 프롬프트에 있습니다. 예시:

STAY ON TOPIC. Every response must directly relate to the user's last request and be concise.

호출 목적 및 이유

유용하게 만들기: 컨텍스트 주입

채팅이 가능한 음성 에이전트는 신기한 기술입니다.
당신이 어떤 작업을 하고 있는지 아는 음성 에이전트는 실제 도구가 됩니다.

우리 에이전트는 호출 시 두 개의 파일을 읽습니다:

  • MEMORY.md – 세션 간 지속되는 지식(우리가 누구인지, 무엇을 만들었는지, 무엇이 실패했는지)
  • current-task.md – 에이전트가 호출을 결정했을 때 현재 진행 중이던 작업

아웃바운드 호출의 경우, API는 구조화된 컨텍스트를 받습니다:

curl -X POST http://localhost:8080/call \
  -H "Content-Type: application/json" \
  -d '{
    "task": "website migration",
    "need": "pick a domain approach",
    "options": ["GitHub Pages", "Cloudflare Pages"]
  }'

인사는 자동으로 생성됩니다:

“Hey K. I’m working on website migration and I need you to pick a domain approach.”

AI는 통화 내내 그 목적에 집중합니다.

모든 통화는 자동으로 전사되어 Markdown 형식으로 저장됩니다. 전사 내용은 향후 세션을 위해 에이전트의 컨텍스트에 다시 반영됩니다.

비용

이것은 대략 월 $6 정도로 실행됩니다:

  • Twilio – ~$0.014 / 분 통화당, ~$1 / 월 전화번호당
  • Anthropic – Claude Sonnet API 사용량(응답당)
  • ElevenLabs – ConversationRelay(트윌리오 통합)를 통해 포함

전용 서버 없음, GPU 인스턴스 없음, 월간 SaaS 구독 없음—Node.js 프로세스 하나와 터널, 그리고 세 개의 API 키만 있으면 됩니다.

Source:

무엇이 바뀌었는가

전화가 울리고 목소리가 5분 전 내가 작업하고 있던 내용과 맥락상 관련된 말을 했을 때—그것은 무언가를 바꾸었습니다. 기술적인 것이 아니라 심리적인 것이었습니다.

  • 당신에게 메시지를 보내는 AI는 알림입니다.
  • 당신에게 전화를 거는 AI는 동료입니다.

음성 AI 에이전트를 위한 인프라는 현재 존재하며, 개별 개발자도 접근할 수 있습니다. 어려운 부분은 예상과 다릅니다 (오디오 처리, 음성 인식) — Twilio가 이를 모두 추상화합니다. 어려운 부분은 음성 선택, 터널 관리, 그리고 응답을 백과사전식이 아니라 대화식으로 유지하는 것입니다.

자율 에이전트를 구축하고 있는데 아직 음성을 추가하지 않았다면, 장벽은 생각보다 낮습니다. ROI는 기술에 있는 것이 아니라 관계에 있습니다.

Kyle Million은 IntuiTek에서 AI 시스템을 구축합니다. 이 글에서 설명하는 에이전트는 Aegis — 스마트 계약, 브라우저 자동화, 콘텐츠 게시, 그리고 이제는 전화 통화를 아우르는 자체 개선형 자율 에이전트입니다.
GitHub – Aegis

0 조회
Back to Blog

관련 글

더 보기 »

서브넷팅 설명

Subnetting이란 무엇인가? 큰 아파트 건물을 여러 층으로 나누는 것과 같다. 각 층 서브넷은 자체 번호가 매겨진 유닛(hosts)을 가지고, 그리고 건물…