← 블로그 목록

피클처럼 AI 통화를 실시간 글자로 보여 주려면: AI 전화 에이전트 실시간 전사 API 만들기

피클 같은 AI 전화 대행 앱은 통화 내용을 실시간으로 보여 줘요. 클로옵스 관리형 AI 에이전트의 실시간 전사(SSE)로 같은 화면을 내 서비스에 만드는 방법, Twilio·Vapi·Retell·ElevenLabs의 실시간 전사 방식 비교, 070 실통화 이벤트 240개를 그대로 재생한 영상을 정리했어요.

안녕하세요. 클로옵스(ClawOps)에서 070 번호로 전화를 받고 거는 AI 에이전트를 만들고 있어요.

요즘 "AI가 대신 전화를 걸어 주는 앱"이 눈에 띄게 늘었어요. 최근 앱스토어 순위에 오른 피클(Pickle)이 대표적이에요. 통신사 요금제 변경이나 전화로만 되는 예약·환불 요청을 맡기면 AI가 직접 전화를 걸어요. 앱 설명에는 이런 문장이 있어요.

통화 내용은 실시간으로 확인할 수 있고, 통화가 끝나면 결과를 요약해 알려줍니다

— App Store, "피클: 나만의 AI 에이전트"

앱스토어에 공개된 첫 번째 화면을 보면 이 경험이 잘 드러나요. "고객센터와 통화 중" 카드 안에 상대방 말(왼쪽)과 피클의 말(오른쪽)이 통화하는 동안 말풍선으로 쌓이고, 통화가 끝나면 결과가 한 문단으로 정리돼요.

피클 앱스토어 스크린샷. 전화를 대신 걸어줘요 문구 아래, 고객센터와 통화 중인 카드 안에 양쪽 대화가 말풍선으로 표시된 화면
출처: Pickle, Inc., App Store "피클: 나만의 AI 에이전트" 스크린샷 (2026년 10월 23일 확인)

AI에게 전화를 맡기는 사람은 결과만 기다리지 않아요. 지금 무슨 말이 오가는지 보고 싶어 하죠. 내가 부탁한 걸 제대로 말하고 있는지, 상대가 뭐라고 답했는지 궁금하니까요.

그런데 이런 화면을 내 서비스 안에 만들려면 생각보다 할 일이 많아요. 번호를 받고, 통화를 잇고, 양쪽 말을 받아 적고, 그 글자를 끊김 없이 내 서버와 화면까지 보내야 하거든요. 클로옵스도 지금까지는 통화가 끝난 뒤의 전사와 요약만 드릴 수 있었어요. 통화 중의 대화는 에이전트 안에만 있었고, 밖으로 나가는 통로가 없었어요.

이번에 클로옵스 관리형 AI 에이전트에 실시간 전사를 열었어요. 이 글에서는 통화 중에 보이는 글자가 어떤 일을 가능하게 하는지, 다른 서비스는 실시간 전사를 어떻게 주는지, 그리고 클로옵스에서 켜고 받는 방법을 차례로 정리해 볼게요.

통화 중에 보이는 글자는 통화 후 기록과 쓰임이 달라요

통화가 끝난 뒤 받는 전사는 기록이에요. 무슨 일이 있었는지 나중에 확인하는 데 쓰죠. 통화 중에 받는 전사는 다음 행동의 근거예요. 아직 전화가 이어지고 있으니 그 글자를 보고 무언가를 바꿀 수 있어요.

왜 꼭 통화 중이어야 할까요?

통화가 살아 있을 때만 할 수 있는 일이 있기 때문이에요.

  1. 사람이 넘겨받기. AI가 엉뚱하게 답하는 순간을 운영자가 보고 끼어들거나 상담원에게 돌릴 수 있어요. Retell AI도 실시간 모니터링 문서에서 넘겨받기와 끊기는 진행 중인 통화에서만 된다고 구분해요.
  2. 키패드 입력 처리. 생년월일 6자리나 메뉴 번호는 받는 즉시 인증 통과 여부나 다음 분기를 정해야 해요.
  3. 위험 신호 감지. 보이스피싱 경고는 송금하기 전에 떠야 의미가 있어요. 과학기술정보통신부와 통신 3사·삼성전자도 통화 도중 의심 정황을 알리는 방식을 안내했어요(국내 보도).
  4. 자막. 청각 약자에게는 끝나고 받는 대본으로는 대화가 안 돼요. 지금 들리는 말이 바로 글자로 보여야 하죠. 손말이음센터(107)가 통화를 실시간 문자로 중계하는 이유이기도 해요.

피클 같은 앱이 보여 주는 "AI가 건 전화를 실시간으로 지켜보는 화면"도 같은 원리예요. 지켜보다가 이상하면 멈추거나 바로잡을 수 있으니까요.

업계는 실시간 전사를 두 가지 방식으로 줘요

공식 문서를 열어 보면 크게 두 갈래로 나뉘어요. 통화 API 회사(Twilio·Telnyx)는 통화마다 "전사 시작" 명령을 보내고, 결과를 웹훅으로 받아요. AI 에이전트 플랫폼(Vapi·Retell·ElevenLabs)은 에이전트 설정에서 받을 이벤트를 고르고, 웹훅이나 WebSocket으로 받아요.

서비스켜는 곳받는 통로요금
Twilio Real-Time Transcription통화마다 TwiML <Start><Transcription>웹훅 POST가격표의 실시간 전사 행 $0.027/분
Telnyx통화마다 transcription_start 명령웹훅음성 인식 엔진별 분당 요금
Vapi어시스턴트의 serverMessages웹훅, 클라이언트 SDK전사 별도 요금 없음(음성 인식 원가를 분당 요금에 합산)
Retell AI에이전트의 webhook_events웹훅(매번 대화 전체)분당 요금에 포함
ElevenLabs Agents에이전트의 client_eventsWebSocket분당 요금에 포함
클로옵스에이전트의 liveEventsSSE, 페이지 조회추가 요금 없음

공통점도 있어요. 말하는 도중의 조각 결과는 거의 모든 곳이 기본으로 꺼 두고, 필요한 사람만 켜게 해요. Twilio의 partialResults도 기본값이 false예요. 클로옵스도 같은 원칙을 따랐어요.

왜 웹훅이 아니라 SSE를 골랐을까요?

말하는 도중의 조각까지 받으면 한 통화에서 이벤트가 수백 개 나와요. 이걸 하나하나 웹훅 POST로 보내면 받는 서버가 요청마다 응답해야 하고, 재시도가 섞이면 순서도 흐트러지기 쉬워요.

SSE(Server-Sent Events)는 연결 하나를 열어 두고 이벤트를 순서대로 흘려보내요. 이벤트마다 번호(id)가 붙어서, 연결이 끊겨도 마지막으로 받은 번호부터 다시 받으면 돼요. 브라우저에도 EventSource라는 표준 API가 있어서 화면까지 잇기도 쉬워요. 연결을 놓친 동안의 이벤트는 같은 번호로 페이지 조회해서 채울 수 있게 했어요.

클로옵스에서는 체크박스 네 개로 켜요

에이전트 상세의 "대화" 탭에 "통화 중 이벤트" 카드가 있어요. 받을 이벤트를 고르면 다음 통화부터 반영돼요. 기본값은 아무것도 고르지 않은 꺼짐이에요.

콘솔 이름이벤트언제 오나요
확정된 말transcript.final한 발화가 끝나면 그 문장을 한 번. 에이전트와 상대방 모두
말하는 도중의 텍스트transcript.partial말이 끝나기 전에 지금까지의 문장을 계속
누른 키dtmf.received상대가 키를 누를 때마다 하나씩
AI가 보낸 키dtmf.sentAI가 상대 ARS로 키를 보낼 때마다 하나씩

하나라도 고르면 통화 시작과 끝을 알리는 stream.started·stream.ended가 함께 와요. API로 켤 때도 같은 목록을 넘기면 돼요.

curl -X PATCH https://api.claw-ops.com/v1/accounts/$ACCOUNT_ID/agents/$AGENT_ID \
  -H "Authorization: Bearer $CLAWOPS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "liveEvents": { "events": ["transcript.final", "transcript.partial", "dtmf.received"] } }'

통화가 시작되면 그 통화의 스트림을 열어요.

curl -N https://api.claw-ops.com/v1/accounts/$ACCOUNT_ID/calls/$CALL_ID/live-events/stream \
  -H "Authorization: Bearer $CLAWOPS_API_KEY"

AI 비서가 실제로 건 전화를 지켜봤어요

피클 같은 화면이 정말 이 이벤트만으로 그려지는지 확인하려고, 070 번호 두 개로 시험 통화를 걸었어요. 거는 쪽은 사용자 대신 치과 예약을 옮기는 AI 비서 "하루"이고, 받는 쪽은 시험용으로 만든 "미소치과" AI 상담원이에요. 두 에이전트 모두 GPT-Live로 만들었어요.

아래 영상은 비서 쪽 스트림에서 받은 이벤트 240개를 받은 시각 그대로 다시 재생한 거예요(2배속). 왼쪽은 그 이벤트로 그린 예시 앱이고, 오른쪽은 스트림 원문이에요. 시작하자마자 치과 ARS 안내가 한 글자씩 늘어나다가 0:05쯤 확정되고, 바로 이어서 "AI가 1번을 눌렀어요"(dtmf.sent)가 뜨는 부분을 보면 돼요.

2026년 10월 23일 시험 통화(96초)예요. 말하는 도중의 텍스트 220개, 확정된 말 17개, AI가 보낸 키 1개, 시작·종료 2개가 왔어요.

상대방 말도 말하는 도중에 한 글자씩 오는 건 GPT-Live로 만든 에이전트라서예요. 비서는 안내를 끝까지 듣고 1번을 누른 뒤, 조건(평일 오후 6시 이후, 목요일 제외)에 맞는 10월 28일 화요일 오후 7시를 골랐어요.

받는 쪽 화면은 이렇게 만들 수 있어요

이번에는 전화를 받는 병원 쪽이에요. 이해를 돕기 위해 지어낸 예시 통화예요. 왼쪽은 스트림으로 들어온 이벤트이고, 오른쪽은 그 이벤트로 그린 접수 데스크 화면이에요. 상대방이 말하는 동안 transcript.partial의 문장이 점점 길어지다가 transcript.final로 확정되는 부분을 보면 돼요.

지어낸 예시 통화. 왼쪽은 SSE 이벤트 목록, 오른쪽은 그 이벤트로 그린 접수 데스크 화면으로 대화와 진료·희망일·본인 확인 칸이 채워져 있다

키 값은 이벤트에 그대로 오지만, 화면에서는 가려서 보여 주는 편이 좋아요.

이벤트 한 건은 이런 모양이에요. id는 통화 안에서 계속 커지는 번호이고, 다시 연결할 때 Last-Event-ID로 넘기면 그다음부터 받아요.

id: 18341
event: transcript.final
data: {"id":"18341","event":"transcript.final","timestamp":"2026-10-21T06:50:59.300Z","detail":{"utteranceId":"u3","role":"agent","text":"평일 오전 9시부터 오후 6시까지입니다."}}

role은 상대방이면 user, 에이전트면 agent예요. 받는 통화든 거는 통화든 같아요. 그래서 피클처럼 AI가 전화를 거는 서비스에서도 "누가 한 말인지"를 따로 계산할 필요가 없어요.

조각은 이어 붙이지 말고 통째로 바꿔야 해요

transcript.partial의 text는 새로 나온 몇 글자가 아니라 지금까지 쌓인 문장 전체예요. 같은 utteranceId의 줄을 통째로 바꿔 그리면 돼요. 그리고 마지막엔 꼭 transcript.final로 갈아끼워야 해요. 말하다 끊긴 발화는 확정 문장이 마지막 조각보다 짧을 수 있거든요. 상대가 말을 자르고 들어오면 에이전트가 하려던 말과 실제로 한 말이 달라지니까요.

말하는 도중의 텍스트는 구성마다 달라요

에이전트 말의 조각은 모든 구성에서 말하기 시작하면 바로 와요. 상대방 말의 조각은 에이전트가 어떤 모델로 듣느냐에 따라 달라요.

에이전트 구성상대방 말의 조각
GPT-Live, Grok Voice말하는 도중에 와요
실시간 모델(OpenAI realtime)받아쓰기 모델에 따라 달라요. gpt-live-transcribe·gpt-realtime-whisper는 말하는 도중에, 기본값 gpt-4o-mini-transcribe는 말이 끝난 뒤 확정과 거의 함께 와요
STT + 텍스트 모델(파이프라인), ElevenLabs Realtime오지 않아요(확정만 와요)

자막처럼 상대방 말도 한 글자씩 보여야 하는 화면이라면 GPT-Live나 Grok Voice, 또는 받아쓰기 모델을 바꾼 실시간 모델이 맞아요. 운영자 모니터링처럼 문장 단위로 충분하다면 어떤 구성이든 "확정된 말"만 켜도 돼요.

우리가 만들면서 겪은 일

에이전트가 할 말을 SDK가 버리고 있었어요

에이전트 말을 재생 박자에 맞춰 조각으로 보내려면, 음성과 맞춰 둔 텍스트가 필요했어요. 그런데 에이전트 러너가 쓰는 SDK 안에서는 이 텍스트가 이미 만들어지고 있었는데, 다음 단계로 넘기지 않고 버리고 있었어요. 그래서 클로옵스 Python SDK에 이 텍스트를 받는 자리를 먼저 만들고(0.69.0), 러너를 그 버전으로 올렸어요. 덕분에 에이전트 말의 조각은 다섯 가지 구성 모두에서 말하기 시작하는 시점에 맞춰 나와요.

운영 통화로 확인했어요

출시 전에 실제 070 번호로 받는 통화와 거는 통화를 시험 삼아 걸어 확인했어요. 받는 시험 통화 한 건에서 이벤트 219개가 왔고, 스트림으로 받은 것과 페이지 조회한 것이 한 건도 빠지거나 겹치지 않았어요. 15초마다 연결 유지 신호(: ping)가 왔고, 75초짜리 통화가 끝날 때까지 연결 하나로 끊김 없이 받았어요. 연결 하나는 최대 60분까지 유지돼요.

내 화면에 붙이는 코드는 짧아요

한 가지 주의할 점이 있어요. 브라우저의 EventSource는 Authorization 헤더를 넣을 수 없어요. API 키를 브라우저에 두면 안 되기도 하고요. 그래서 내 서버가 클로옵스 스트림을 열고, 브라우저에는 그대로 흘려 주는 게 가장 간단해요.

// server.mjs — Node 18 이상
import http from "node:http";

const { CLAWOPS_API_KEY, ACCOUNT_ID } = process.env;

http.createServer(async (req, res) => {
  // 실제 서비스에서는 로그인한 사용자의 통화인지 먼저 확인하세요.
  const callId = new URL(req.url, "http://localhost").searchParams.get("callId");
  const abort = new AbortController();
  req.on("close", () => abort.abort());

  const lastId = req.headers["last-event-id"];
  const upstream = await fetch(
    `https://api.claw-ops.com/v1/accounts/${ACCOUNT_ID}/calls/${callId}/live-events/stream`,
    {
      headers: {
        Authorization: `Bearer ${CLAWOPS_API_KEY}`,
        ...(lastId ? { "Last-Event-ID": lastId } : {}),
      },
      signal: abort.signal,
    },
  );

  res.writeHead(upstream.status, {
    "Content-Type": "text/event-stream",
    "Cache-Control": "no-cache",
  });
  try {
    for await (const chunk of upstream.body) res.write(chunk);
  } catch {
    // 브라우저가 연결을 닫으면 여기로 와요.
  }
  res.end();
}).listen(3000);

브라우저에서는 이벤트 이름별로 받아서 그리면 돼요.

const es = new EventSource(`/live?callId=${callId}`);
const lines = new Map(); // utteranceId → { role, text, final }

const onText = (final) => (e) => {
  const { detail } = JSON.parse(e.data);
  // 조각은 누적 문장이라 통째로 바꾸고, 확정이 오면 그걸로 갈아끼워요.
  lines.set(detail.utteranceId, { role: detail.role, text: detail.text, final });
  draw(lines);
};

es.addEventListener("transcript.partial", onText(false));
es.addEventListener("transcript.final", onText(true));
es.addEventListener("dtmf.received", (e) => showKey(JSON.parse(e.data).detail.digit));
es.addEventListener("stream.ended", () => es.close());

연결이 끊기면 EventSource가 마지막 id를 Last-Event-ID에 담아 알아서 다시 붙어요. 위 서버 코드가 그 값을 클로옵스에 넘겨 주니, 놓친 이벤트부터 이어서 받아요.

자주 묻는 질문

피클 같은 AI 전화 대행 서비스를 직접 만들 수 있나요?

전화를 거는 부분과 통화를 실시간으로 보여 주는 부분은 클로옵스로 만들 수 있어요. 070 번호를 발급받고, 관리형 AI 에이전트에 할 일을 적고, POST /calls로 전화를 건 뒤 응답의 callId로 그 통화의 실시간 이벤트를 받으면 돼요. 통화가 끝나면 전사와 요약도 이벤트 웹훅으로 받을 수 있어요.

실시간 전사는 추가 요금이 있나요?

없어요. 에이전트의 분당 요금 안에서 동작해요. 실시간 모델의 받아쓰기 모델을 바꾸면 그 모델 원가가 에이전트 분당 가격에 반영돼요.

통화가 끝난 뒤에도 볼 수 있나요?

네. 같은 이벤트를 GET …/live-events?after=&limit=로 페이지 조회할 수 있어요. 말하는 도중의 조각은 1일, 나머지는 30일 동안 보관해요.

사람이 쓰는 상담원 전화에도 쓸 수 있나요?

지금은 관리형 AI 에이전트가 받거나 거는 통화에서만 동작해요. 다른 통화는 스트림이 no_events로 끝나요.

한 통화를 여러 화면에서 동시에 볼 수 있나요?

한 통화에 연결 5개, 계정 전체로 20개까지 열 수 있어요. 더 많은 화면에 보여야 한다면 내 서버가 하나만 받아서 나눠 주는 구조가 좋아요.

마치며

AI에게 전화를 맡기는 경험은 "결과를 기다리는 것"에서 "지켜보다가 필요하면 끼어드는 것"으로 바뀌고 있어요. 클로옵스 관리형 에이전트에서는 체크박스 몇 개로 그 화면에 필요한 이벤트를 받을 수 있어요.

예약 접수 화면, 운영자 모니터링, 앱 속 통화 자막처럼 통화 중에 글자가 필요한 서비스를 만들고 있다면 지금 바로 켜 보세요.

클로옵스 시작하기 → 070 번호와 AI 에이전트를 만들고 "통화 중 이벤트"를 켜 보세요. 설정 방법은 관리형 에이전트 문서의 "통화 중 전사·키패드 받기"에 있어요.


참고한 자료

  1. App Store, 피클: 나만의 AI 에이전트
  2. Pickle 도움말, Delegate tasks
  3. Twilio, TwiML <Transcription>
  4. Twilio, Voice Pricing (US)
  5. Telnyx, Transcription start
  6. Vapi, Server events
  7. Retell AI, Webhook overview
  8. Retell AI, Live monitoring
  9. ElevenLabs, Client events
  10. 바이라인네트워크, 통화 중 보이스피싱 탐지 서비스(2026-02-12)
  11. MDN, Server-sent events
이전 글

Eleven Realtime Max 에이전트 둘이 서로 전화를 걸게 했어요: AI 비서와 치과 상담원 통화 8번 실측

한국AI통신(ClawOps) AI 전화 API로 시작하기

070 번호 발급부터 AI 음성 통화까지, REST API 몇 줄이면 됩니다.