即時語音
即時語音可將 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 處理程序。
安裝整合套件,以及用於語音活動偵測與回合偵測的 LiveKit plugin:
- npm
- pnpm
- Yarn
- Bun
npm install @mastra/livekit @livekit/agents @livekit/agents-plugin-silero @livekit/agents-plugin-livekitpnpm add @mastra/livekit @livekit/agents @livekit/agents-plugin-silero @livekit/agents-plugin-livekityarn add @mastra/livekit @livekit/agents @livekit/agents-plugin-silero @livekit/agents-plugin-livekitbun add @mastra/livekit @livekit/agents @livekit/agents-plugin-silero @livekit/agents-plugin-livekit在
.env檔案中設定 LiveKit 憑證。你可以在 LiveKit Cloud 建立免費專案,或使用livekit-server --dev執行本機伺服器:.envLIVEKIT_URL=wss://your-project.livekit.cloudLIVEKIT_API_KEY=your-api-keyLIVEKIT_API_SECRET=your-api-secret將 Voice Agent 加入 Mastra 執行個體,並公開連線路由。
liveKitConnectionRoute()helper 會加入POST /voice/livekit/connection-detailsendpoint,用來簽發 LiveKit token 並將 Agent dispatch 至 room:src/mastra/index.tsimport { 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' })],},})建立 worker。它會以獨立處理程序執行、回應 LiveKit 工作階段,並在每個回合呼叫 Agent。Worker API 位於
@mastra/livekit/workerentry point,因此 Mastra 伺服器不會載入 LiveKit Agents runtime。此範例使用 LiveKit Inference 模型字串進行語音轉文字與文字轉語音,因此不需要 Provider plugin:src/mastra/voice-worker.tsimport { 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 提供服務。下載回合偵測與語音活動偵測模型(只需下載一次)。接著在一個終端機中執行 worker,並在另一個終端機中執行 Mastra 伺服器:
npx livekit-agents download-filesnpx tsx src/mastra/voice-worker.ts dev- npm
- pnpm
- Yarn
- Bun
npm run devpnpm run devyarn devbun run devWorker 會向 LiveKit 伺服器註冊並等待工作階段,而
mastra dev則會提供連線路由。與你的 Agent 對話。開啟託管的 LiveKit Agents Playground,並將其連線至你的專案,即可在不建置前端的情況下開始通話。
若要改為連接自己的應用程式,請呼叫連線路由以取得 token。
POST /voice/livekit/connection-details的 request body 可接受選用的agentId、threadId與resourceId欄位,並傳回:{"serverUrl": "wss://your-project.livekit.cloud","roomName": "mastra-voice-a1b2c3d4","participantName": "user-1","participantToken": "eyJhbGci..."}此回應符合 LiveKit 前端 starter 使用的介面約定,因此使用 agent-starter-react 或 LiveKit React 元件 建置的應用程式無須修改即可運作。
回合偵測與中斷「回合偵測與中斷」的直接連結
LiveKit 會判斷使用者何時說完,以及 Agent 何時遭到中斷。預設值通常已足夠,你也可以使用 turnHandling 進行調整:
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 回合偵測文件。
每次通話的聲音與轉錄「每次通話的聲音與轉錄」的直接連結
頂層的 stt 與 tts 選項會套用至每通電話。若要為每通電話選擇設定,例如為各 tenant 指定一種聲音或語言,請改為設定 configuration.stt 與 configuration.tts resolver。每個 resolver 都會在每通電話中執行一次,並接收 dispatch metadata、request context、room 名稱與 job context,再傳回對應頂層選項可接受的值。此值可以是 plugin 執行個體或 inference 模型字串。傳回 undefined 即會改用頂層選項。
下列範例會依據 dispatch metadata 中的 tenant 項目,為每個 tenant 指定各自的文字轉語音聲音:
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 播放簡短語句:
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 保持無狀態:
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:
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.AgentSession 的 llm slot。Mastra 應用程式、Agent loop、Tool、Memory 與 Observability 會在 Mastra 伺服器上執行,worker 則透過 HTTP 存取。Worker 處理程序不需要 Mastra 應用程式、資料庫或模型 Provider 金鑰。
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 主動掛斷:搭配使用 onToolCall 與 runEndCall()。
請勿同時使用 memory 選項與 LiveKit 的 preemptiveGeneration;自行建置的工作階段預設會啟用後者。如果推測性回合在 LiveKit 捨棄前執行完畢,就會將一則使用者訊息和一則從未播放的回應保存至 thread。請設定 turnHandling: { preemptiveGeneration: { enabled: false } },或在不使用 memory 的情況下執行,並於每個回合傳入完整轉錄內容。
MastraLLM 也接受處理程序內的 Mastra agent 執行個體,讓你不必進行第二次部署即可掌控工作階段;也可以接受自訂 generate 函式。遠端傳輸功能亦可透過 createRemoteAgentReplyGenerator() 獨立使用,並能接入 createLiveKitWorker 的 generate 選項,讓功能完整的 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:
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:
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_URL、LIVEKIT_API_KEY 及 LIVEKIT_API_SECRET。
LiveKit 關於規模設定、正常關閉與託管的指引可直接套用。請參閱部署 Agent。Worker 會主動向外連線至 LiveKit,因此不需要對內開放連接埠。
運作方式「運作方式」的直接連結
LiveKit 語音工作階段包含三個部分:
- Mastra 伺服器簽發 LiveKit 存取 token,並將 Agent dispatch 至 room。Dispatch 會攜帶 Mastra Agent ID、Memory thread 與 resource 等 metadata。
- LiveKit Agent worker(獨立且長時間執行的處理程序)接收 job 並執行音訊 pipeline。音訊會透過 WebRTC 在瀏覽器與 worker 之間傳輸,絕不會經過 Mastra HTTP 伺服器。
- 每當使用者結束一個回合,worker 就會以新的輸入呼叫 Mastra Agent 的
stream(),並播放串流文字。使用者插話時,LiveKit 會取消串流,Mastra 也會停止產生內容。
對話記錄位於 Mastra Memory,因此語音工作階段與文字聊天可共用同一個 thread。