본문으로 건너뛰기

실시간 음성

실시간 음성은 Mastra Agent를 사용자가 브라우저나 전화를 통해 대화할 수 있는 실시간 통화로 전환합니다. Mastra는 그것을 기반으로 합니다.LiveKit, 실시간 오디오 및 비디오를 위한 오픈 소스 WebRTC 플랫폼입니다.

그만큼@mastra/livekit package connects Mastra agents to the LiveKit Agents framework: LiveKit은 음성 활동 감지, 음성-텍스트 스트리밍, 의미론적 전환 감지, 바지인 및 텍스트-음성 변환과 같은 오디오 루프를 소유합니다. Mastra Agent는 자체 Model, Tool 및 Memory를 사용하여 모든 응답을 생성합니다.

대기 시간이 짧고 중단 가능한 음성 대화가 필요한 경우 실시간 음성을 사용하세요. LiveKit을 사용하지 않는 공급자 기반 음성 음성 변환은 다음을 참조하세요.Speech to Speech.

빠른 시작
빠른 시작에 대한 직접 링크

이 단계는 빈 프로젝트에서 대화할 수 있는 음성 Agent로 이동합니다. 음성 세션에는 여기에서 설정하는 두 가지 이동 부분이 있습니다. 즉, 액세스 토큰을 전달하는 Mastra 서버의 API 경로와 오디오 파이프라인을 실행하고 매 턴마다 Agent를 호출하는 별도의 작업자 프로세스입니다.

  1. 음성 활동 감지 및 회전 감지를 위해 LiveKit 플러그인과 함께 통합 패키지를 설치합니다.

    npm install @mastra/livekit @livekit/agents @livekit/agents-plugin-silero @livekit/agents-plugin-livekit
  2. LiveKit 자격 증명을 내부에 설정하세요..env file. Create a free project on LiveKit Cloud, or run a local server with livekit-server --dev:

    .env
    LIVEKIT_URL=wss://your-project.livekit.cloud
    LIVEKIT_API_KEY=your-api-key
    LIVEKIT_API_SECRET=your-api-secret
  3. Mastra 인스턴스에 음성 Agent를 추가하고 연결 경로를 노출하세요. 그만큼liveKitConnectionRoute() helper adds a POST /voice/livekit/connection-details 엔드포인트는 LiveKit 토큰을 발급하고 Agent를 방으로 디스패치합니다:

    src/mastra/index.ts
    import { Mastra } from '@mastra/core/mastra'
    import { Agent } from '@mastra/core/agent'
    import { liveKitConnectionRoute } from '@mastra/livekit'

    const supportAgent = new Agent({
    id: 'support',
    name: 'Support',
    instructions: 'You are a friendly phone support agent. Keep replies short and conversational.',
    model: 'openai/gpt-5-mini',
    })

    export const mastra = new Mastra({
    agents: { support: supportAgent },
    server: {
    apiRoutes: [liveKitConnectionRoute({ agentName: 'mastra-voice' })],
    },
    })
  4. 작업자를 생성합니다. 별도의 프로세스로 실행되고 LiveKit 세션에 응답하며 매 턴마다 Agent에 전화를 겁니다. 작업자 API는@mastra/livekit/worker 진입점을 사용하므로 Mastra 서버가 LiveKit Agents 런타임을 로드하지 않습니다. 이 예제에서는 음성 텍스트 변환과 텍스트 음성 변환에 LiveKit Inference Model 문자열을 사용하므로 Provider 플러그인이 필요하지 않습니다:

    src/mastra/voice-worker.ts
    import { fileURLToPath } from 'node:url'
    import { createLiveKitWorker, runLiveKitWorker } from '@mastra/livekit/worker'
    import { mastra } from './index'

    export default createLiveKitWorker({
    mastra,
    agent: 'support',
    stt: 'deepgram/nova-3',
    tts: 'cartesia/sonic-3',
    turnDetection: 'multilingual',
    greeting: 'Hi! How can I help you today?',
    })

    if (process.argv[1] === fileURLToPath(import.meta.url)) {
    runLiveKitWorker({ entry: import.meta.url, agentName: 'mastra-voice' })
    }

    그만큼agent 옵션은 각 세션에 응답할 Mastra Agent를 선택합니다. 표시된 것처럼 고정 키를 전달하거나, 다음을 사용하려면 생략하세요: agentId 를 디스패치 메타데이터에서 가져오므로 하나의 워커가 Mastra 인스턴스의 모든 Agent를 처리할 수 있습니다.

  5. 회전 감지 및 음성 활동 감지 Model을 한 번 다운로드하세요. 그런 다음 한 터미널에서는 작업자를 실행하고 다른 터미널에서는 Mastra 서버를 실행합니다.

    npx livekit-agents download-files
    npx tsx src/mastra/voice-worker.ts dev
    npm run dev

    작업자는 LiveKit 서버에 등록하고 세션을 기다립니다.mastra dev serves the connection route.

  6. 대리인과 상담하세요. 호스팅을 엽니다.LiveKit Agents Playground 를 프로젝트에 연결하여 프런트엔드를 구축하지 않고도 통화를 시작하세요.

    대신 자신의 앱을 연결하려면 토큰에 대한 연결 경로를 호출하세요.POST /voice/livekit/connection-details accepts optional agentId, threadId, and resourceId fields in the request body and returns:

    {
    "serverUrl": "wss://your-project.livekit.cloud",
    "roomName": "mastra-voice-a1b2c3d4",
    "participantName": "user-1",
    "participantToken": "eyJhbGci..."
    }

    이 응답은 LiveKit의 프런트엔드 스타터에서 사용하는 계약과 일치하므로 앱은 다음에서 빌드됩니다.agent-starter-react or the LiveKit React components work without changes.

회전 감지 및 방해
회전 감지 및 방해에 대한 직접 링크

LiveKit은 사용자가 말하기를 마친 시점과 Agent가 중단된 시점을 결정합니다. 기본값은 잘 작동합니다. 그들을 조정turnHandling:

src/mastra/voice-worker.ts
export default createLiveKitWorker({
mastra,
agent: 'support',
stt: 'deepgram/nova-3',
tts: 'cartesia/sonic-3',
turnDetection: 'multilingual',
turnHandling: {
endpointing: { mode: 'dynamic', minDelay: 300, maxDelay: 3000 },
interruption: { minDuration: 500, resumeFalseInterruption: true },
},
})
  • turnDetection: 'multilingual': LiveKit의 의미론적 턴 종료 Model을 CPU에서 로컬로 실행합니다. 사용자가 생각을 중단하는 것을 방지하기 위해 실시간 기록을 읽습니다. 사용'vad' or 'stt' for silence-based endpointing instead.
  • endpointing: 사용자가 말하기를 멈춘 후 Agent가 기다리는 시간을 제한합니다.
  • interruption: 참여를 제어합니다. 사용자가 Agent를 통해 말하면 LiveKit은 재생을 중지하고 진행 중인 Mastra 스트림을 취소하므로 토큰 생성도 중지됩니다.
  • preemptiveGeneration: 사용자가 완료하는 동안 Mastra Agent의 응답을 시작하여 첫 번째 토큰까지의 시간을 숨깁니다. 작업자는 기본적으로 이를 비활성화합니다. 각 선점 시도는 중간 기록에서 Mastra Agent를 실행하고, 모든 실행은 스레드에서 메시지를 복제하는 사용자 메시지를 유지합니다. 다음을 사용하여 다시 활성화하세요.preemptiveGeneration: { enabled: true } 정확한 스레드 기록보다 지연 시간이 더 중요하다면 사용합니다.

참조LiveKit turn detection docs for all options.

통화별 음성 및 전사
통화별 음성 및 전사에 대한 직접 링크

최상위 수준stt and tts 옵션은 모든 호출에 적용됩니다. 호출별로 선택하거나 테넌트마다 하나의 음성 또는 언어를 지정하려면 configuration.stt and configuration.tts 리졸버를 대신 설정하세요. 각 리졸버는 디스패치 메타데이터, 요청 컨텍스트, 룸 이름, 작업 컨텍스트와 함께 호출마다 한 번 실행되며, 일치하는 최상위 옵션에서 허용하는 값을 반환합니다. 이 값은 플러그인 인스턴스 또는 추론 Model 문자열입니다. 다음을 반환하세요: undefined to fall back to the top-level option.

다음 예에서는 각 테넌트에게 고유한 텍스트 음성 변환 음성을 제공합니다.tenant entry in the dispatch metadata:

src/mastra/voice-worker.ts
import * as cartesia from '@livekit/agents-plugin-cartesia'

// One voice id per tenant, resolved from the dispatch metadata on each call.
const tenantVoices: Record<string, string> = {
meridian: 'your-cartesia-voice-id-1',
coastal: 'your-cartesia-voice-id-2',
}
// The resolver runs during call setup, so cache plugin instances across calls.
const ttsByVoice = new Map<string, cartesia.TTS>()

export default createLiveKitWorker({
mastra,
agent: 'support',
stt: 'deepgram/nova-3',
tts: 'cartesia/sonic-3',
configuration: {
tts: ({ requestContext }) => {
const voice = tenantVoices[requestContext?.tenant as string]
if (!voice) return undefined // fall back to the top-level `tts`
let tts = ttsByVoice.get(voice)
if (!tts) {
tts = new cartesia.TTS({ voice })
ttsByVoice.set(voice, tts)
}
return tts
},
},
})

configuration.stt호출별 전사와 동일한 방식으로 작동합니다(예: 테넌트별 다른 전사 Model 또는 언어). 인사말에는 일치하는 통화별 형식이 있습니다.configuration.greeting.text 동일한 호출 컨텍스트를 받는 리졸버를 허용하므로, 하나의 워커가 각 테넌트에 맞는 고유한 문구로 대화를 시작할 수 있습니다.

Memory와 스레드
Memory와 스레드에 대한 직접 링크

확인된 Mastra Agent에 Memory가 구성되어 있으면 각 호출은 하나의 Memory 스레드가 됩니다.

  • thread기본값은threadId 디스패치 메타데이터에서 가져오고, 그다음에는 룸 이름에서 가져옵니다.
  • resource기본값은resourceId 디스패치 메타데이터에서 가져오고, 그다음에는 스레드에서 가져옵니다. 호출이 올바른 사용자 아래에 그룹화되도록 여기에 최종 사용자의 ID를 보내세요. Mastra Studio는 Agent ID를 전송하며, 이는 사이드바에서 스레드를 나열하는 방식과 일치합니다.
  • 스레드가 아직 존재하지 않으면 작업자는 메타데이터를 사용하여 "음성 통화"라는 제목으로 스레드를 생성합니다.{ source: 'livekit' }, 음성 인사말은 첫 번째 어시스턴트 메시지로 저장되므로 스레드가 전체 통화 기록으로 표시됩니다(비활성화하려면 persistGreeting: false).

각 차례에서는 새로운 사용자 입력만 보냅니다. Mastra Memory는 역사, 의미 기억 및 작업 기억을 제공합니다. 전달하여 기존 스레드에 세션을 고정합니다.threadId 를 연결 요청 본문에 지정할 수 있으며, 이는 텍스트 대화를 음성으로 이어갈 때 유용합니다. Studio에서 열린 채팅으로 통화를 시작하면 통화가 해당 스레드에 연결되고, 각 대화가 끝날 때마다 대화 기록이 채팅에 채워집니다.

사용자가 Agent를 중단하면 진행 중인 생성이 중단되고 해당 턴의 어떤 것도 그 순간 지속되지 않습니다. LiveKit은 사용자가 실제로 들은 부분을 녹취록에 보관하고, 다음 차례에 작업자는 들리는 부분만 다시 보내 스레드가 호출과 일치하도록 다시 채웁니다. 중단 후 바로 전화를 끊은 사용자는 마지막 부분을 녹음하지 않은 채 남겨둡니다. 보다interrupted turns for the details and a reconciliation recipe.

Tool이 실행되는 동안 말하기
Tool이 실행되는 동안 말하기에 대한 직접 링크

느린 Tool이 실행되는 동안 음성 대화는 자동으로 진행될 수 없습니다. 사용toolFeedback 를 사용하여 Mastra Agent가 Tool 호출을 시작할 때 짧은 문구를 말하게 할 수 있습니다:

src/mastra/voice-worker.ts
export default createLiveKitWorker({
mastra,
agent: 'support',
stt: 'deepgram/nova-3',
tts: 'cartesia/sonic-3',
toolFeedback: ({ toolName }) =>
toolName === 'searchOrders' ? 'Let me look that up.' : undefined,
})

해당 문구는 응답의 일부로 말하고 성적표에 기록됩니다.

Workflow를 사용하여 응답 생성
Workflow를 사용하여 응답 생성에 대한 직접 링크

기본적으로 작업자는 Mastra Agent를 사용하여 각 응답을 생성합니다. 턴별로 다단계 로직을 실행하려면(예: 의도 분류, 라우팅, Tool을 순서대로 호출한 후 응답 작성) Mastra를 사용하여 응답을 생성하세요.workflow instead. Set workflow in place of agent.

LiveKit은 여전히 ​​오디오 루프를 소유하고 턴당 한 번씩 Mastra를 호출하므로 Workflow는 턴마다 완료될 때까지 실행됩니다. Workflow는 일시 중지하거나 재개할 수 없으며 턴 간에 대화 상태가 전달되지 않습니다. 성적표를 다음을 통해 전달하세요.workflowInput so the workflow stays stateless:

src/mastra/voice-worker.ts
import { createLiveKitWorker, chatContextToMessages } from '@mastra/livekit/worker'
import { mastra } from './index'

export default createLiveKitWorker({
mastra,
workflow: 'phoneConversation',
workflowInput: ({ chatCtx }) => ({ history: chatContextToMessages(chatCtx) }),
replyStep: 'generateResponse',
stt: 'deepgram/nova-3',
tts: 'cartesia/sonic-3',
turnDetection: 'multilingual',
})

Workflow는 텍스트가 아닌 구조화된 단계 이벤트를 스트리밍합니다. 생성된 토큰을 말하기 위해 응답 단계는 Agent의 텍스트를 단계로 파이프합니다.writer:

src/mastra/workflows/phone-conversation.ts
const generateResponse = createStep({
id: 'generateResponse',
// input and output schemas omitted
execute: async ({ inputData, mastra, writer, abortSignal }) => {
const stream = await mastra.getAgent('voice').stream(inputData.history, { abortSignal })
await stream.textStream.pipeTo(writer)
return { assistantMessage: await stream.text }
},
})
  • replyStep: 음성 출력을 한 단계로 제한합니다. 그것에 쓰는 모든 단계를 말하는 것을 생략하십시오.writer.
  • resultText: 텍스트를 스트리밍하는 단계가 없을 때 최종 실행 결과에서 응답을 파생하는 대체입니다. 스트리밍을 통해writer gives lower time-to-first-token, so prefer it.
  • abortSignal: 단계를 앞으로abortSignal into agent.stream() 를 사용하면 끼어들기 시 생성이 즉시 중지됩니다. 사용자가 끼어들면 워커가 실행을 취소합니다.
  • generate: 완전한 제어를 위해서는 다음을 전달하십시오.generate 함수를 대신 사용하세요. 턴을 텍스트 스트림으로 변환하는 어떤 응답 생성기든 사용할 수 있습니다.

Workflow를 사용하면 작업자가 지속되지 않고 Agent의 방식대로 자동으로 전환됩니다.stream() 가 수행합니다. Workflow 내부에 대화 기록을 유지하거나, LiveKit 대화 기록을 정보의 기준으로 유지하고 각 턴마다 전달하세요.

Mastra를 LLM 구성 요소로 사용
Mastra를 LLM 구성 요소로 사용에 대한 직접 링크

createLiveKitWorker()귀하를 위해 LiveKit 세션을 소유합니다. 세션을 직접 소유하려면 다음을 사용하세요.MastraLLM 를 대신 사용하세요. Mastra Agent를 llm slot of your own voice.AgentSession에 배치하는 표준 LiveKit LLM 플러그인입니다. Mastra 앱, Agent 루프, Tool, Memory, Observability는 Mastra 서버에서 실행되며, 워커는 HTTP를 통해 이에 접근합니다. 워커 프로세스에는 Mastra 앱, 데이터베이스 또는 Model Provider 키가 필요하지 않습니다.

src/mastra/voice-worker-plugin.ts
import { fileURLToPath } from 'node:url'
import { defineAgent, voice } from '@livekit/agents'
import * as silero from '@livekit/agents-plugin-silero'
import { MastraLLM } from '@mastra/livekit/plugin'
import { runLiveKitWorker } from '@mastra/livekit/worker'

export default defineAgent({
entry: async ctx => {
await ctx.connect()

const session = new voice.AgentSession({
llm: new MastraLLM({
remote: { baseUrl: process.env.MASTRA_URL!, agentId: 'support' },
memory: { thread: ctx.room.name!, resource: 'user-7' },
}),
stt: 'deepgram/nova-3',
tts: 'cartesia/sonic-3',
vad: await silero.VAD.load(),
// Required with `memory`: LiveKit enables preemptive generation by default.
turnHandling: { preemptiveGeneration: { enabled: false } },
})

await session.start({
// These instructions never reach the Mastra agent; its own instructions apply.
agent: new voice.Agent({ instructions: 'Replies come from the Mastra agent.' }),
room: ctx.room,
})

session.say('Hi! How can I help you today?')
},
})

if (process.argv[1] === fileURLToPath(import.meta.url)) {
runLiveKitWorker({ entry: import.meta.url, agentName: 'mastra-voice' })
}

두 경로 모두 아래에서 동일한 응답 파이프라인을 공유합니다. 세션을 소유할 사람을 선택하세요.

createLiveKitWorker()MastraLLM
세션 소유권워커 헬퍼가 다음을 구성하고 관리함: AgentSession코드에서 세션을 구성하며 모든 LiveKit 옵션과 훅을 직접 관리함
Mastra 앱 실행 위치워커 프로세스 내부HTTP를 통해 접근하는 Mastra 서버(또는 다음을 통한 인프로세스 방식: agent)
워커 프로세스 요구 사항Mastra 앱, 스토리지 및 Model Provider 키LiveKit SDK와 서버에 대한 네트워크 액세스만 필요
기본 제공 편의 기능인사말, 동의 게이팅, Agent 주도 통화 종료, 스레드 부트스트랩, Observability 롤업다음을 사용하여 필요한 기능을 다시 구현: session helpers
적합한 용도작동하는 음성 Agent를 가장 빠르게 구현하는 경로, Studio 음성 모드기존 LiveKit 앱 및 세션에 대한 완전한 제어

Tool은 Mastra Agent에 유지되고 서버에서 실행됩니다. 세션에 전달된 LiveKit 측 Tool은 무시됩니다. Tool 활동은 다음을 통해 작업자에게 도달합니다.toolFeedback (spoken filler), onToolCall (fires as each tool call starts), and onTurnComplete (텍스트, Tool 호출 및 토큰 사용량과 함께 각 응답 후 실행됨). Agent 주도 통화 종료는 몇 줄이면 구현할 수 있습니다. 다음을 함께 사용하세요: onToolCall with runEndCall().

경고

결합하지 마십시오memory option with LiveKit's preemptiveGeneration, 직접 구성한 세션에서는 LiveKit이 이를 기본적으로 활성화합니다. LiveKit이 폐기하기 전에 완료된 추측성 턴은 사용자 메시지와 실제로 음성 출력되지 않은 응답을 스레드에 유지합니다. 다음을 설정하세요: turnHandling: { preemptiveGeneration: { enabled: false } }, or run without memory and pass the full transcript each turn.

MastraLLM또한 진행 중인 Mastra도 허용합니다.agent 인스턴스, 두 번째 배포 없이 세션 소유권 확보 또는 사용자 지정 generate 함수입니다. 원격 전송 기능은 다음으로도 독립적으로 제공됩니다: createRemoteAgentReplyGenerator(), which also plugs into createLiveKitWorker's generate 옵션을 사용하여 모든 기능이 포함된 워커를 원격 서버에 연결해 실행할 수 있습니다.

서버에서 시작된 세션
서버에서 시작된 세션에 대한 직접 링크

사용dispatchVoiceSession() 를 사용하여 자체 코드에서 룸에 음성 Agent를 추가할 수 있습니다. 예를 들어 기존 룸에 참여시키거나 아웃바운드 SIP call:

import { dispatchVoiceSession } from '@mastra/livekit'

await dispatchVoiceSession({
roomName: 'support-call-42',
agentName: 'mastra-voice',
metadata: { agentId: 'support', threadId: 'thread-42', resourceId: 'user-7' },
})

Observability
Observability에 대한 직접 링크

Mastra 인스턴스에observability 가 구성되어 있으면 워커가 각 호출을 추적합니다. 호출마다 하나의 voice call span을 열고 모든 항목을 그 아래에 중첩합니다:

  • 매 턴의 Mastra Agent는 Model 생성, Tool 호출 및 Memory 작업을 통해 텍스트 채팅이 기록하는 것과 똑같이 실행됩니다.
  • 각 LiveKit 파이프라인 지표에 대한 하위 범위: 음성-텍스트, 텍스트-음성, 발화 끝(회전 감지), 음성 활동 감지 및 Model의 첫 번째 토큰 시간. 이는 텍스트 추적으로 표시할 수 없는 대기 시간 및 오디오 측정값을 전달합니다.
  • 세션이 종료될 때 범위에 기록되는 Model별 사용량 롤업(전체 호출에 대한 토큰, 문자 및 오디오 합계)입니다.

작업자는 별도의 프로세스이므로 서버와 작업자 모두의 동시 쓰기를 허용하는 백엔드의 저장소를 가리킵니다. SQLite 지원LibSQL 는 작동합니다. 단일 작성자 스토어는 작동하지 않습니다. Trace, Memory 및 스레드는 하나의 스토어를 공유할 수 있습니다:

src/mastra/index.ts
import { Mastra } from '@mastra/core/mastra'
import { LibSQLStore } from '@mastra/libsql'
import { Observability, MastraStorageExporter } from '@mastra/observability'

export const mastra = new Mastra({
storage: new LibSQLStore({ id: 'voice-agent-storage', url: 'file:./voice-agent.db' }),
observability: new Observability({
configs: {
default: {
serviceName: 'voice-agent',
exporters: [new MastraStorageExporter()],
},
},
}),
})

추적은 기본적으로 켜져 있습니다. 통과하다observability: false to createLiveKitWorker to turn it off.

전개
전개에 대한 직접 링크

작업자는 Mastra 서버와 별도의 프로세스이므로mastra build needs to emit it as its own entry. Add it to bundler.entries:

src/mastra/index.ts
import { Mastra } from '@mastra/core'

export const mastra = new Mastra({
bundler: {
entries: { 'voice-worker': './voice-worker.ts' },
// Keep LiveKit's native modules out of the bundle. `mastra build` only applies
// this default when you set no other bundler options, so set it explicitly here.
externals: true,
},
})

mastra build이제 두 프로세스를 모두 작성합니다..mastra/output, sharing one package.json and one dependency install:

.mastra/output/
index.mjs # Mastra server
voice-worker.mjs # LiveKit worker

해당 디렉터리를 단일 아티팩트로 배포하고 자체 명령을 사용하여 각 프로세스를 시작합니다.

node .mastra/output/index.mjs # server
node .mastra/output/voice-worker.mjs start # worker

작업자는 서버와 동일한 환경 변수가 필요하며,LIVEKIT_URL, LIVEKIT_API_KEY, and LIVEKIT_API_SECRET.

크기 조정, 정상적인 종료 및 호스팅에 대한 LiveKit의 지침은 변경되지 않고 적용됩니다. 보다Deploying agents. 워커는 LiveKit에 아웃바운드로 연결되므로 인바운드 포트가 필요하지 않습니다.

작동 원리
작동 원리에 대한 직접 링크

LiveKit 음성 세션에는 다음 세 가지 부분이 포함됩니다.

  1. Mastra 서버는 LiveKit 액세스 토큰을 발행하고 Agent를 방으로 파견합니다. 디스패치는 Mastra Agent ID, Memory 스레드 및 리소스와 같은 메타데이터를 전달합니다.
  2. LiveKit Agent 작업자(별도의 장기 실행 프로세스)가 작업을 수신하고 오디오 파이프라인을 실행합니다. 오디오는 WebRTC를 통해 브라우저와 작업자 간에 흐르며 결코 Mastra HTTP 서버를 통과하지 않습니다.
  3. 사용자가 차례를 마칠 때마다 직원은 마스트라 Agent에게 전화를 겁니다.stream() 를 새 입력으로 실행하고 스트리밍된 텍스트를 음성으로 출력합니다. 사용자가 끼어들면 LiveKit이 스트림을 취소하고 Mastra가 생성을 중지합니다.

대화 기록은 Mastra Memory에 저장되므로 음성 세션과 문자 채팅이 하나의 스레드를 공유할 수 있습니다.