实时语音
实时语音可将 Mastra Agent 变成用户能在浏览器或电话中随时插话的实时通话。Mastra 基于开源实时音视频 WebRTC 平台 LiveKit 构建此功能。
@mastra/livekit 软件包将 Mastra Agent 连接到 LiveKit Agents 框架:LiveKit 负责语音活动检测、流式语音转文本、语义轮次检测、插话和文本转语音等音频循环;Mastra Agent 则使用自己的模型、Tool 和 memory 生成每条回复。
需要低延迟、可打断的语音对话时,请使用实时语音。若要使用不依赖 LiveKit、基于 Provider 的语音转语音,请参阅语音转语音。
快速入门快速入门的直接链接
以下步骤将从空项目开始,构建一个可与之交谈的语音 Agent。这里需要设置语音会话的两个部分:Mastra 服务器上分发访问 token 的 API 路由,以及运行音频管道并在每个轮次调用 Agent 的独立 worker 进程。
安装集成软件包,以及用于语音活动检测和轮次检测的 LiveKit 插件:
- 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向 Mastra 实例添加语音 Agent 并公开连接路由。
liveKitConnectionRoute()helper 会添加POST /voice/livekit/connection-details端点,用于签发 LiveKit token 并将 Agent 分派到 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/worker入口,因此 Mastra 服务器不会加载 LiveKit Agents 运行时。此示例将 LiveKit Inference 模型字符串用于语音转文本和文本转语音,因此无需 Provider 插件: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在请求正文中接受可选的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 的语义轮次结束模型。它读取实时转录内容,避免在用户还没说完时将其打断。若要改用基于静音的端点检测,请使用'vad'或'stt'。endpointing:限制用户停止说话后 Agent 的等待时间。interruption:控制插话。用户在 Agent 说话时插话,LiveKit 会停止播放并取消进行中的 Mastra 流,因此 token 生成也会停止。preemptiveGeneration:在用户即将说完时开始生成 Mastra Agent 的回复,以隐藏首个 token 延迟。Worker 默认禁用此功能:每次预生成尝试都会使用临时转录内容运行 Mastra Agent,而且每次运行都会持久化用户消息,导致 thread 中出现重复消息。如果延迟比精确的 thread 历史更重要,可通过preemptiveGeneration: { enabled: true }重新启用。
所有选项请参阅 LiveKit 轮次检测文档。
每次通话的语音和转录每次通话的语音和转录的直接链接
顶层 stt 和 tts 选项适用于每次通话。若要按通话选择,例如为每个租户指定一种语音或语言,请改为设置 configuration.stt 和 configuration.tts resolver。每个 resolver 在每次通话中运行一次,可访问 dispatch metadata、请求上下文、room 名称和任务上下文,并返回对应顶层选项接受的值。该值可以是插件实例或 inference 模型字符串。返回 undefined 会回退到顶层选项。
以下示例根据 dispatch metadata 中的 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 以同样方式支持按通话转录,例如为每个租户使用不同的转录模型或语言。问候语也有对应的按通话形式:configuration.greeting.text 接受具有相同通话上下文的 resolver,因此一个 worker 可以使用各租户自己的措辞开场。
Memory 和 threadMemory 和 thread的直接链接
解析出的 Mastra Agent 配置了 memory 时,每次通话都会成为一个 memory thread:
thread默认使用 dispatch metadata 中的threadId,其次使用 room 名称。resource默认使用 dispatch metadata 中的resourceId,其次使用 thread。请在这里发送最终用户 ID,以便将通话归到正确用户下。Mastra Studio 会发送 Agent ID,这与其侧边栏列出 thread 的方式一致。- 如果 thread 尚不存在,worker 会创建一个标题为“Voice call”、metadata 为
{ source: 'livekit' }的 thread,并将说出的问候语保存为第一条助手消息,使 thread 呈现完整的通话记录(可用persistGreeting: false禁用)。
每个轮次只发送新的用户输入;Mastra Memory 提供历史记录、语义召回和工作记忆。可以在连接请求正文中传入 threadId,将会话固定到现有 thread,这有助于通过语音继续文本对话。在 Studio 中,从打开的聊天开始通话会将通话绑定到该 thread,并在每次交流后将转录内容填入聊天。
用户打断 Agent 时,进行中的生成会中止,该轮次此刻不会持久化任何内容。LiveKit 会在转录中保留用户实际听到的部分;下一轮中,worker 会重新发送这段仅已播放的片段,让 thread 回填并与通话一致。如果用户打断后立即挂断,最后的片段不会被记录。详细信息和对账方案请参阅被打断的轮次。
Tool 运行时播报Tool 运行时播报的直接链接
运行缓慢 Tool 时,语音对话不能陷入沉默。使用 toolFeedback,在 Mastra Agent 开始调用 Tool 时播报简短语句:
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 流式输出的是结构化步骤事件,而不是文本。要在 token 生成时将其播报,回复步骤需将 Agent 文本通过管道写入该步骤的 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:将播报输出限制在一个步骤。省略时,会播报所有写入其writer的步骤。resultText:当没有步骤流式输出文本时,从最终运行结果派生回复的回退方案。通过writer流式输出的首个 token 延迟更低,因此应优先使用。abortSignal:将步骤的abortSignal转发给agent.stream(),使插话能及时停止生成。用户打断时,worker 会取消运行。generate:若要完全控制,请改为传入generate函数。它可以是任何将轮次转换为文本流的回复生成器。
使用 Workflow 时,worker 不会像 Agent 的 stream() 那样自动持久化轮次。请在 Workflow 内持久化对话历史,或将 LiveKit 转录作为事实来源并在每轮传入。
将 Mastra 用作 LLM 组件将 Mastra 用作 LLM 组件的直接链接
createLiveKitWorker() 会代你管理 LiveKit 会话。若要自行管理会话,请改用 MastraLLM:这是一个标准 LiveKit LLM 插件,可将 Mastra Agent 放入自有 voice.AgentSession 的 llm 插槽。Mastra 应用、Agent 循环、Tool、memory 和可观测性都在 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' })
}
两种方式在底层共享同一回复管道;请根据会话应由谁管理来选择:
createLiveKitWorker() | MastraLLM | |
|---|---|---|
| 会话管理方 | Worker helper 构建并管理 AgentSession | 你的代码构建会话;可以控制每个 LiveKit 选项和 hook |
| Mastra 应用运行位置 | Worker 进程中 | Mastra 服务器上,通过 HTTP 访问(或通过 agent 在进程内访问) |
| Worker 进程需要 | Mastra 应用、存储和模型 Provider 密钥 | 仅需要 LiveKit SDK 和访问服务器的网络连接 |
| 内置便利功能 | 问候语、同意门控、Agent 主动挂断、thread 引导和可观测性汇总 | 使用会话 helper重新构建所需功能 |
| 最适合 | 最快构建可用语音 Agent;Studio 语音模式 | 现有 LiveKit 应用以及完全控制会话 |
Tool 保留在 Mastra Agent 上并在服务器执行。传给会话的 LiveKit 侧 Tool 会被忽略。Tool 活动通过 toolFeedback(播报填充语)、onToolCall(每次 Tool 调用开始时触发)和 onTurnComplete(每次回复后触发,并提供文本、Tool 调用和 token 用量)传给 worker。Agent 主动挂断只需几行代码:将 onToolCall 与 runEndCall() 配合使用。
不要将 memory 选项与 LiveKit 的 preemptiveGeneration 结合使用;在自行构建的会话中,LiveKit 默认启用后者。如果推测轮次在 LiveKit 丢弃它之前完成,用户消息和尚未播报的回复都会持久化到 thread。请设置 turnHandling: { preemptiveGeneration: { enabled: false } },或在不使用 memory 的情况下运行,并在每轮传入完整转录内容。
MastraLLM 也接受进程内 Mastra agent 实例,从而无需第二次部署即可管理会话;它还接受自定义 generate 函数。远程传输可通过 createRemoteAgentReplyGenerator() 独立使用,也可接入 createLiveKitWorker 的 generate 选项,让功能完备的 worker 对接远程服务器。
服务器发起的会话服务器发起的会话的直接链接
使用 dispatchVoiceSession() 从自己的代码向 room 添加语音 Agent,例如加入现有 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 实例配置了可观测性后,worker 会追踪每次通话。它为每个会话打开一个 voice call span,并将所有内容嵌套在其中:
- 每轮 Mastra Agent 运行,包括模型生成、Tool 调用和 memory 操作,记录方式与文本聊天完全相同。
- 为每项 LiveKit 管道指标创建子 span:语音转文本、文本转语音、话语结束(轮次检测)、语音活动检测以及模型的首个 token 延迟。这些 span 包含文本 Trace 无法显示的延迟和音频测量值。
- 会话结束时写入 span 的按模型用量汇总(整次通话的 token、字符和音频总量)。
Worker 是独立进程,因此请将存储指向可接受服务器和 worker 并发写入的后端。基于 SQLite 的 LibSQL 可以使用,单写入方存储则不行。Trace、memory 和 thread 可以共享一个存储:
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()],
},
},
}),
})
默认启用追踪。向 createLiveKitWorker 传入 observability: false 可将其关闭。
部署部署的直接链接
Worker 与 Mastra 服务器是不同进程,因此 mastra build 需要将其作为独立入口输出。将它添加到 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
将该目录部署为单个构件,并使用各自的命令启动每个进程:
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 分派到 room。分派中包含 Mastra Agent ID、memory thread 和 resource 等 metadata。
- LiveKit Agent worker(独立的长期运行进程)接收任务并运行音频管道。音频通过 WebRTC 在浏览器与 worker 之间流动,绝不会经过 Mastra HTTP 服务器。
- 每当用户结束一轮,worker 都会使用新输入调用 Mastra Agent 的
stream()并播报流式文本。用户打断时,LiveKit 会取消流,Mastra 也会停止生成。
对话历史保存在 Mastra Memory 中,因此语音会话和文本聊天可以共享同一个 thread。