跳至主要內容

即時語音

即時語音可將 Mastra Agent 轉換成使用者能在瀏覽器或電話上交談並隨時插話的即時通話。Mastra 以開放原始碼的即時音訊與視訊 WebRTC 平台 LiveKit 為基礎建構此功能。

@mastra/livekit 套件會將 Mastra Agent 連接至 LiveKit Agents framework:LiveKit 負責語音活動偵測、串流語音轉文字、語意回合偵測、插話及文字轉語音等音訊迴圈。Mastra Agent 則使用自己的模型、Tool 與 Memory 產生每次回應。

需要低延遲且可中斷的語音對話時,請使用即時語音。若要在不使用 LiveKit 的情況下使用以 Provider 為基礎的語音轉語音功能,請參閱語音轉語音

快速入門
「快速入門」的直接連結

以下步驟會從空白專案開始,建立可與之交談的 Voice Agent。語音工作階段包含兩個需要設定的部分:在 Mastra 伺服器上發放存取 token 的 API 路由,以及執行音訊 pipeline 並在每個回合呼叫 Agent 的獨立 worker 處理程序。

  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. 將 Voice Agent 加入 Mastra 執行個體,並公開連線路由。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。它會以獨立處理程序執行、回應 LiveKit 工作階段,並在每個回合呼叫 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 選項會選擇要由哪個 Mastra Agent 回應各工作階段。你可以如範例所示傳入固定 key,也可以省略此選項,改用 dispatch metadata 中的 agentId,讓同一個 worker 為 Mastra 執行個體上的所有 Agent 提供服務。

  5. 下載回合偵測與語音活動偵測模型(只需下載一次)。接著在一個終端機中執行 worker,並在另一個終端機中執行 Mastra 伺服器:

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

    Worker 會向 LiveKit 伺服器註冊並等待工作階段,而 mastra dev 則會提供連線路由。

  6. 與你的 Agent 對話。開啟託管的 LiveKit Agents Playground,並將其連線至你的專案,即可在不建置前端的情況下開始通話。

    若要改為連接自己的應用程式,請呼叫連線路由以取得 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 使用的介面約定,因此使用 agent-starter-reactLiveKit React 元件 建置的應用程式無須修改即可運作。

回合偵測與中斷
「回合偵測與中斷」的直接連結

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 串流,因此也會停止產生 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 執行個體或 inference 模型字串。傳回 undefined 即會改用頂層選項。

下列範例會依據 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 接受具有相同通話 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 會提供歷史記錄、語意回憶與 working memory。若要將工作階段固定至現有 thread,請在連線要求的 body 中傳入 threadId;需要透過語音延續文字對話時,這項功能相當實用。在 Studio 中,從已開啟的聊天開始通話會將該通話繫結至該 thread,並在每次對話後將轉錄內容填入聊天。

當使用者打斷 Agent 時,執行中的產生作業會中止,當下不會保存該回合的任何內容。LiveKit 會在轉錄內容中保留使用者實際聽到的部分;下一個回合中,worker 會重新傳送這段只有使用者聽到的內容,讓 thread 回填成與通話一致。如果使用者在打斷後立刻掛斷,最後這段內容便不會留下記錄。如需詳細資訊與資料校正方式,請參閱遭到中斷的回合

Tool 執行時播放語音
「Tool 執行時播放語音」的直接連結

執行速度較慢的 Tool 時,語音對話不能長時間沉默。當 Mastra Agent 開始呼叫 Tool 時,請使用 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 產生時直接播放,回應 step 必須將其 Agent 的文字透過管線傳入該 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 串流文字時,從最終執行結果取得回應的備援方式。透過 writer 串流可縮短第一個 token 的等待時間,因此建議優先使用。
  • abortSignal:將 step 的 abortSignal 傳入 agent.stream(),讓插話能立即停止產生內容。使用者插話時,worker 會取消該次執行。
  • generate:若要完全掌控行為,請改為傳入 generate 函式。它可以是任何將一個回合轉換成文字串流的回應產生器。

使用 Workflow 時,worker 不會像 Agent 的 stream() 一樣自動保存回合。請在 Workflow 內保存對話記錄,或將 LiveKit 轉錄內容作為真實資料來源,並在每個回合傳入。

將 Mastra 作為 LLM 元件
「將 Mastra 作為 LLM 元件」的直接連結

createLiveKitWorker() 會替你掌控 LiveKit 工作階段。若要自行掌控工作階段,請改用 MastraLLM:這是標準的 LiveKit LLM plugin,會將 Mastra Agent 放入你自己的 voice.AgentSessionllm slot。Mastra 應用程式、Agent loop、Tool、Memory 與 Observability 會在 Mastra 伺服器上執行,worker 則透過 HTTP 存取。Worker 處理程序不需要 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;請依工作階段的擁有者進行選擇:

createLiveKitWorker()MastraLLM
工作階段擁有權Worker helper 建立及管理 AgentSession由你的程式碼建立工作階段;你可掌控所有 LiveKit 選項與 hook
Mastra 應用程式執行位置Worker 處理程序中Mastra 伺服器上,透過 HTTP 存取(或透過 agent 在處理程序內存取)
Worker 處理程序需要Mastra 應用程式、Storage 與模型 Provider 金鑰僅需 LiveKit SDK 及對伺服器的網路存取權
內建便利功能問候語、同意機制、由 Agent 主動掛斷、thread 初始化、Observability 彙總使用工作階段 helper自行建置所需功能
最適合以最快方式建立可用的 Voice Agent;Studio 語音模式現有 LiveKit 應用程式,以及需要完整掌控工作階段的情境

Tool 會保留在 Mastra Agent 上並於伺服器執行。傳入工作階段的 LiveKit 端 Tool 會被忽略。Tool 活動會透過 toolFeedback(播放填補語)、onToolCall(在每次 Tool 呼叫開始時觸發)與 onTurnComplete(在每次回應後觸發,並提供文字、Tool 呼叫及 token 用量)傳送至 worker。只需幾行程式碼即可讓 Agent 主動掛斷:搭配使用 onToolCallrunEndCall()

警告

請勿同時使用 memory 選項與 LiveKit 的 preemptiveGeneration;自行建置的工作階段預設會啟用後者。如果推測性回合在 LiveKit 捨棄前執行完畢,就會將一則使用者訊息和一則從未播放的回應保存至 thread。請設定 turnHandling: { preemptiveGeneration: { enabled: false } },或在不使用 memory 的情況下執行,並於每個回合傳入完整轉錄內容。

MastraLLM 也接受處理程序內的 Mastra agent 執行個體,讓你不必進行第二次部署即可掌控工作階段;也可以接受自訂 generate 函式。遠端傳輸功能亦可透過 createRemoteAgentReplyGenerator() 獨立使用,並能接入 createLiveKitWorkergenerate 選項,讓功能完整的 worker 搭配遠端伺服器執行。

由伺服器發起的工作階段
「由伺服器發起的工作階段」的直接連結

使用 dispatchVoiceSession() 從自己的程式碼將 Voice 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' },
})

Observability
「Observability」的直接連結

Mastra 執行個體設定 Observability 後,worker 會追蹤每通電話。每個工作階段都會開啟一個 voice call span,並將所有項目巢狀置於其中:

  • 每個回合的 Mastra Agent 執行,包括模型產生、Tool 呼叫與 Memory 操作,記錄方式與文字聊天完全相同。
  • 每項 LiveKit pipeline 指標各有一個子 span:語音轉文字、文字轉語音、語句結束(回合偵測)、語音活動偵測,以及模型產生第一個 token 的時間。這些 span 會記錄文字 Trace 無法呈現的延遲與音訊測量資料。
  • 各模型的用量彙總(整通電話的 token、字元與音訊總量),並在工作階段結束時寫入 span。

Worker 是獨立處理程序,因此請將 Storage 指向可接受伺服器與 worker 同時寫入的後端。以 SQLite 為基礎的 LibSQL 可以正常運作,僅允許單一寫入者的 Storage 則不行。Trace、Memory 與 thread 可以共用同一個 Storage:

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: false 傳入 createLiveKitWorker 即可停用。

部署
「部署」的直接連結

Worker 與 Mastra 伺服器是不同的處理程序,因此 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 現在會將兩個處理程序都寫入 .mastra/output,共用一個 package.json 與同一份相依套件安裝:

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

將該目錄部署為單一 artifact,並使用各自的指令啟動每個處理程序:

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,因此不需要對內開放連接埠。

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

LiveKit 語音工作階段包含三個部分:

  1. Mastra 伺服器簽發 LiveKit 存取 token,並將 Agent dispatch 至 room。Dispatch 會攜帶 Mastra Agent ID、Memory thread 與 resource 等 metadata。
  2. LiveKit Agent worker(獨立且長時間執行的處理程序)接收 job 並執行音訊 pipeline。音訊會透過 WebRTC 在瀏覽器與 worker 之間傳輸,絕不會經過 Mastra HTTP 伺服器。
  3. 每當使用者結束一個回合,worker 就會以新的輸入呼叫 Mastra Agent 的 stream(),並播放串流文字。使用者插話時,LiveKit 會取消串流,Mastra 也會停止產生內容。

對話記錄位於 Mastra Memory,因此語音工作階段與文字聊天可共用同一個 thread。