跳至主要內容

即時語音

即時語音可將 Mastra Agent 變成用戶能在瀏覽器或電話上隨時插話的即時通話。Mastra 以開源的即時音訊及視訊 WebRTC 平台 LiveKit 建構這項功能。

@mastra/livekit 套件會將 Mastra Agent 連接至 LiveKit Agents framework:LiveKit 負責音訊循環,包括語音活動偵測、串流語音轉文字、語意輪次偵測、插話及文字轉語音。你的 Mastra Agent 則使用本身的模型、Tool 及 Memory 產生每個回應。

需要低延遲、可中斷的語音對話時,請使用即時語音。如要使用不依賴 LiveKit、以 Provider 為基礎的語音對語音功能,請參閱語音對語音

快速開始
快速開始 的直接連結

以下步驟會由空白項目開始,建立一個可對話的語音 Agent。語音 session 包含兩個需要在此設定的運作部分:Mastra 伺服器上用來發出存取 token 的 API route,以及執行音訊 pipeline 並在每個輪次呼叫 Agent 的獨立 worker process。

  1. 安裝整合套件,以及用於語音活動偵測及輪次偵測的 LiveKit plugin:

    npm install @mastra/livekit @livekit/agents @livekit/agents-plugin-silero @livekit/agents-plugin-livekit
  2. .env 檔案中設定 LiveKit 憑證。你可以在 LiveKit Cloud 建立免費項目,或使用 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 instance 加入語音 Agent,並公開連線 route。liveKitConnectionRoute() helper 會加入 POST /voice/livekit/connection-details endpoint,以簽發 LiveKit token,並將 Agent dispatch 至 room:

    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. 建立 worker。它會以獨立 process 執行、回應 LiveKit session,並在每個輪次呼叫你的 Agent。Worker API 位於 @mastra/livekit/worker entry point,因此 Mastra 伺服器不會載入 LiveKit Agents runtime。這個範例使用 LiveKit Inference 模型字串進行語音轉文字及文字轉語音,無需安裝 Provider plugin:

    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 選項會選擇回應每個 session 的 Mastra Agent。你可以像範例般傳入固定 key,或省略此選項以使用 dispatch metadata 中的 agentId,讓一個 worker 為 Mastra instance 上的所有 Agent 提供服務。

  5. 下載輪次偵測及語音活動偵測模型一次。然後在一個終端機執行 worker,並在另一個終端機執行 Mastra 伺服器:

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

    Worker 會向 LiveKit 伺服器註冊並等候 session,而 mastra dev 則提供連線 route。

  6. 與 Agent 對話。開啟託管的 LiveKit Agents Playground 並連接至你的項目,便可在無需建構前端的情況下開始通話。

    如要改為連接自己的應用程式,請呼叫連線 route 取得 token。POST /voice/livekit/connection-details 會接受 request body 中選填的 agentIdthreadIdresourceId 欄位,並傳回:

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

    此回應符合 LiveKit 前端 starter 使用的 contract,因此以 agent-starter-reactLiveKit React components 建構的應用程式無需修改即可運作。

輪次偵測及中斷
輪次偵測及中斷 的直接連結

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':在本機 CPU 上執行 LiveKit 的語意輪次結束模型。它會讀取即時轉錄,避免在用戶尚未表達完整意思時打斷對方。如要改用以靜音為基礎的 endpointing,請使用 'vad''stt'
  • endpointing:限制用戶停止說話後 Agent 的等候時間。
  • interruption:控制插話。當用戶在 Agent 說話期間發言,LiveKit 會停止播放並取消進行中的 Mastra stream,因此 token 產生亦會停止。
  • preemptiveGeneration:在用戶仍在結束發言時開始產生 Mastra Agent 的回應,藉此隱藏首個 token 的等候時間。Worker 預設停用此功能:每次預先產生嘗試都會使用暫時轉錄執行 Mastra Agent,而且每次執行都會保存用戶訊息,令 thread 出現重複訊息。如果延遲比精確的 thread 歷史記錄更重要,可使用 preemptiveGeneration: { enabled: true } 重新啟用。

如要了解所有選項,請參閱 LiveKit 輪次偵測文件

每次通話的聲線及轉錄
每次通話的聲線及轉錄 的直接連結

頂層 stttts 選項會套用至每次通話。如要按通話選擇,為每個 tenant 指定一種聲線或語言,請改為設定 configuration.sttconfiguration.tts resolver。每個 resolver 在每次通話中執行一次,並接收 dispatch metadata、request context、room 名稱及 job context,再傳回相應頂層選項所接受的值。該值可以是 plugin instance 或 inference 模型字串。傳回 undefined 則會使用頂層選項作為 fallback。

以下範例按照 dispatch metadata 中的 tenant 項目,為每個 tenant 指定專屬的文字轉語音聲線:

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 以相同方式為每次通話設定轉錄,例如為每個 tenant 指定不同的轉錄模型或語言。問候語亦有相應的按通話形式:configuration.greeting.text 接受具有相同 call context 的 resolver,讓一個 worker 可使用各 tenant 專屬的措辭開始對話。

Memory 及 thread
Memory 及 thread 的直接連結

當解析出的 Mastra Agent 已設定 Memory,每次通話都會成為一個 Memory thread:

  • thread 預設使用 dispatch metadata 中的 threadId,如沒有則使用 room 名稱。
  • resource 預設使用 dispatch metadata 中的 resourceId,如沒有則使用 thread。請在此傳送最終用戶的 ID,令通話歸入正確用戶。Mastra Studio 會傳送 Agent ID,與側邊欄列出 thread 的方式一致。
  • 如果 thread 尚不存在,worker 會建立標題為「語音通話」、metadata 為 { source: 'livekit' } 的 thread,並將播放的問候語儲存為第一則 assistant 訊息,令 thread 可作為完整通話轉錄閱讀(可使用 persistGreeting: false 停用)。

每個輪次只會傳送新的用戶輸入;Mastra Memory 會提供歷史記錄、語意回憶及工作記憶。你可以在連線 request body 傳入 threadId,將 session 固定至現有 thread,這對以語音繼續文字對話十分有用。在 Studio 中,從已開啟的對話開始通話會將該通話綁定至該 thread,而每次交流後,轉錄內容都會填入對話。

當用戶打斷 Agent,進行中的產生程序會中止,該輪次當下不會保存任何內容。LiveKit 會在轉錄中保留用戶實際聽到的部分;下一個輪次時,worker 會重新傳送這段只包含已聽內容的片段,讓 thread 補回內容以配合通話。如果用戶在打斷後立即掛線,最後一段片段便不會記錄。如要了解詳情及協調處理方法,請參閱已中斷的輪次

Tool 執行期間播放語音
Tool 執行期間播放語音 的直接連結

語音對話不能在執行緩慢的 Tool 時陷入靜默。Mastra Agent 開始 Tool call 時,可使用 toolFeedback 播放一句簡短說話:

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 產生回應 的直接連結

Worker 預設使用 Mastra Agent 產生每個回應。如要在每個輪次執行多步驟邏輯(例如分類意圖、路由、依次呼叫 Tool,再組合回應),請改用 Mastra Workflow 產生回應。以 workflow 取代 agent 即可。

LiveKit 仍負責音訊循環,並在每個輪次呼叫 Mastra 一次,因此 Workflow 每個輪次都會執行至完成。Workflow 無法暫停或恢復,對話狀態亦不會在輪次之間延續。請透過 workflowInput 傳入轉錄,讓 Workflow 維持無狀態:

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 串流的是結構化 step event,而非文字。如要在 token 產生時播放語音,reply step 需將其 Agent 的文字 pipe 至該 step 的 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:將語音輸出限制在一個 step。省略此選項,即會播放每個寫入其 writer 的 step。
  • resultText:當沒有 step 串流文字時,從最終執行結果衍生回應的 fallback。透過 writer 串流可縮短首個 token 的等候時間,因此應優先使用。
  • abortSignal:將 step 的 abortSignal 轉交至 agent.stream(),讓插話能迅速停止產生內容。用戶插話時,worker 會取消執行。
  • generate:如要完全控制,可改為傳入 generate function。它可以是任何將一個輪次轉換成文字 stream 的回應 generator。

使用 Workflow 時,worker 不會像 Agent 的 stream() 般自動保存輪次。請在 Workflow 內保存對話歷史記錄,或以 LiveKit 轉錄作為真實資料來源,並在每個輪次傳入。

將 Mastra 用作 LLM 元件
將 Mastra 用作 LLM 元件 的直接連結

createLiveKitWorker() 會代你管理 LiveKit session。如要自行管理 session,請改用 MastraLLM:這是標準 LiveKit LLM plugin,會將 Mastra Agent 放入 llm slot,而該 slot 位於你自己的 voice.AgentSession。Mastra 應用程式、Agent loop、Tool、Memory 及可觀測性均在 Mastra 伺服器上執行,worker 則透過 HTTP 存取。Worker process 不需要 Mastra 應用程式、資料庫或模型 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' })
}

兩種方式在底層共用相同的回應 pipeline;請按哪一方應管理 session 作出選擇:

createLiveKitWorker()MastraLLM
Session 管理權Worker helper 建構並管理 AgentSession由你的程式碼建構 session;所有 LiveKit 選項及 hook 均由你控制
Mastra 應用程式執行位置Worker process 內Mastra 伺服器上,透過 HTTP 存取(或透過 agent 在 process 內存取)
Worker process 所需項目Mastra 應用程式、儲存空間及模型 Provider 金鑰只需 LiveKit SDK 及連接伺服器的網絡存取權
內置便利功能問候語、同意閘控、由 Agent 主動掛線、thread bootstrap、可觀測性匯總使用 session helper 重新建構所需功能
最適合最快速建立可運作語音 Agent 的方式;Studio 語音模式現有 LiveKit 應用程式及完整 session 控制權

Tool 會保留在 Mastra Agent 上並於伺服器執行。傳入 session 的 LiveKit 端 Tool 會被忽略。Tool 活動會透過 toolFeedback(播放的填充語句)、onToolCall(每次 Tool call 開始時觸發)及 onTurnComplete(每次回應後觸發,並附帶文字、Tool call 及 token 用量)傳至 worker。由 Agent 主動掛線只需數行程式碼:將 onToolCallrunEndCall() 配合使用。

注意

請勿將 memory 選項與 LiveKit 的 preemptiveGeneration 一同使用;在你自行建構的 session 中,LiveKit 預設會啟用後者。如果推測輪次在 LiveKit 捨棄之前已完成,便會將一則用戶訊息及從未播放的回應保存至 thread。請設定 turnHandling: { preemptiveGeneration: { enabled: false } },或不使用 memory,並在每個輪次傳入完整轉錄。

MastraLLM 亦接受 process 內的 Mastra agent instance,讓你無需第二次部署即可管理 session;它也接受自訂 generate function。Remote transport 可透過 createRemoteAgentReplyGenerator() 獨立使用,亦可接入 createLiveKitWorkergenerate 選項,以功能齊全的 worker 連接遠端伺服器。

由伺服器啟動的 session
由伺服器啟動的 session 的直接連結

使用 dispatchVoiceSession(),可從你自己的程式碼將語音 Agent 加入 room,例如加入現有 room 或發起對外 SIP 通話

import { dispatchVoiceSession } from '@mastra/livekit'

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

可觀測性
可觀測性 的直接連結

當 Mastra instance 已設定可觀測性,worker 會為每次通話建立 Trace。它會為每個 session 開啟一個 voice call span,並將所有內容置於其下:

  • 每個輪次的 Mastra Agent 執行,包括模型產生、Tool call 及 Memory 操作,記錄方式與文字對話完全相同。
  • 每項 LiveKit pipeline 指標的 child span:語音轉文字、文字轉語音、語句結束(輪次偵測)、語音活動偵測,以及模型產生首個 token 的時間。這些 span 包含文字 Trace 無法顯示的延遲及音訊測量資料。
  • 按模型匯總的用量(整次通話的 token、字元及音訊總量),並在 session 結束時寫入 span。

Worker 是獨立 process,因此請將儲存空間指向可同時接受伺服器及 worker 寫入的後端。以 SQLite 為基礎的 LibSQL 可以使用,只支援單一 writer 的儲存空間則不可以。Trace、Memory 及 thread 可共用一個儲存空間:

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()],
},
},
}),
})

Tracing 預設啟用。傳入 observability: falsecreateLiveKitWorker 即可停用。

部署
部署 的直接連結

Worker 與 Mastra 伺服器是不同的 process,因此 mastra build 需要將它輸出為獨立 entry。請將它加入 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 現在會將兩個 process 寫入 .mastra/output,並共用一個 package.json 及一組已安裝的依賴套件:

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

將該目錄作為單一 artifact 部署,並以各自的命令啟動每個 process:

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

Worker 需要與伺服器相同的環境變數,另加 LIVEKIT_URLLIVEKIT_API_KEYLIVEKIT_API_SECRET

LiveKit 有關容量規劃、平順關閉及託管的指引可直接套用。請參閱部署 Agent。Worker 會向外連接 LiveKit,因此不需要 inbound port。

運作方式
運作方式 的直接連結

LiveKit 語音 session 包含三個部分:

  1. Mastra 伺服器簽發 LiveKit 存取 token,並將 Agent dispatch 至 room。Dispatch 會攜帶 Mastra Agent ID、Memory thread 及 resource 等 metadata。
  2. LiveKit Agent worker(獨立且長時間執行的 process)接收 job 並執行音訊 pipeline。音訊經 WebRTC 在瀏覽器與 worker 之間傳送,絕不會經過 Mastra HTTP 伺服器。
  3. 每當用戶完成一個輪次,worker 都會使用新輸入呼叫 Mastra Agent 的 stream(),並播放串流文字。用戶插話時,LiveKit 會取消 stream,而 Mastra 會停止產生內容。

對話歷史記錄儲存在 Mastra Memory,因此語音 session 及文字對話可以共用同一個 thread。