跳到主要内容

实时语音

实时语音可将 Mastra Agent 变成用户能在浏览器或电话中随时插话的实时通话。Mastra 基于开源实时音视频 WebRTC 平台 LiveKit 构建此功能。

@mastra/livekit 软件包将 Mastra Agent 连接到 LiveKit Agents 框架:LiveKit 负责语音活动检测、流式语音转文本、语义轮次检测、插话和文本转语音等音频循环;Mastra Agent 则使用自己的模型、Tool 和 memory 生成每条回复。

需要低延迟、可打断的语音对话时,请使用实时语音。若要使用不依赖 LiveKit、基于 Provider 的语音转语音,请参阅语音转语音

快速入门
快速入门的直接链接

以下步骤将从空项目开始,构建一个可与之交谈的语音 Agent。这里需要设置语音会话的两个部分:Mastra 服务器上分发访问 token 的 API 路由,以及运行音频管道并在每个轮次调用 Agent 的独立 worker 进程。

  1. 安装集成软件包,以及用于语音活动检测和轮次检测的 LiveKit 插件:

    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 实例添加语音 Agent 并公开连接路由。liveKitConnectionRoute() helper 会添加 POST /voice/livekit/connection-details 端点,用于签发 LiveKit token 并将 Agent 分派到 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 入口,因此 Mastra 服务器不会加载 LiveKit Agents 运行时。此示例将 LiveKit Inference 模型字符串用于语音转文本和文本转语音,因此无需 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 响应每个会话。可以像示例一样传入固定 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 在请求正文中接受可选的 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 的语义轮次结束模型。它读取实时转录内容,避免在用户还没说完时将其打断。若要改用基于静音的端点检测,请使用 'vad''stt'
  • endpointing:限制用户停止说话后 Agent 的等待时间。
  • interruption:控制插话。用户在 Agent 说话时插话,LiveKit 会停止播放并取消进行中的 Mastra 流,因此 token 生成也会停止。
  • preemptiveGeneration:在用户即将说完时开始生成 Mastra Agent 的回复,以隐藏首个 token 延迟。Worker 默认禁用此功能:每次预生成尝试都会使用临时转录内容运行 Mastra Agent,而且每次运行都会持久化用户消息,导致 thread 中出现重复消息。如果延迟比精确的 thread 历史更重要,可通过 preemptiveGeneration: { enabled: true } 重新启用。

所有选项请参阅 LiveKit 轮次检测文档

每次通话的语音和转录
每次通话的语音和转录的直接链接

顶层 stttts 选项适用于每次通话。若要按通话选择,例如为每个租户指定一种语音或语言,请改为设置 configuration.sttconfiguration.tts resolver。每个 resolver 在每次通话中运行一次,可访问 dispatch metadata、请求上下文、room 名称和任务上下文,并返回对应顶层选项接受的值。该值可以是插件实例或 inference 模型字符串。返回 undefined 会回退到顶层选项。

以下示例根据 dispatch metadata 中的 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 以同样方式支持按通话转录,例如为每个租户使用不同的转录模型或语言。问候语也有对应的按通话形式:configuration.greeting.text 接受具有相同通话上下文的 resolver,因此一个 worker 可以使用各租户自己的措辞开场。

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 会创建一个标题为“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 时播报简短语句:

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 流式输出的是结构化步骤事件,而不是文本。要在 token 生成时将其播报,回复步骤需将 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 流式输出的首个 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.AgentSessionllm 插槽。Mastra 应用、Agent 循环、Tool、memory 和可观测性都在 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' })
}

两种方式在底层共享同一回复管道;请根据会话应由谁管理来选择:

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 主动挂断只需几行代码:将 onToolCallrunEndCall() 配合使用。

注意

不要将 memory 选项与 LiveKit 的 preemptiveGeneration 结合使用;在自行构建的会话中,LiveKit 默认启用后者。如果推测轮次在 LiveKit 丢弃它之前完成,用户消息和尚未播报的回复都会持久化到 thread。请设置 turnHandling: { preemptiveGeneration: { enabled: false } },或在不使用 memory 的情况下运行,并在每轮传入完整转录内容。

MastraLLM 也接受进程内 Mastra agent 实例,从而无需第二次部署即可管理会话;它还接受自定义 generate 函数。远程传输可通过 createRemoteAgentReplyGenerator() 独立使用,也可接入 createLiveKitWorkergenerate 选项,让功能完备的 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 可以共享一个存储:

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

默认启用追踪。向 createLiveKitWorker 传入 observability: false 可将其关闭。

部署
部署的直接链接

Worker 与 Mastra 服务器是不同进程,因此 mastra build 需要将其作为独立入口输出。将它添加到 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

将该目录部署为单个构件,并使用各自的命令启动每个进程:

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 分派到 room。分派中包含 Mastra Agent ID、memory thread 和 resource 等 metadata。
  2. LiveKit Agent worker(独立的长期运行进程)接收任务并运行音频管道。音频通过 WebRTC 在浏览器与 worker 之间流动,绝不会经过 Mastra HTTP 服务器。
  3. 每当用户结束一轮,worker 都会使用新输入调用 Mastra Agent 的 stream() 并播报流式文本。用户打断时,LiveKit 会取消流,Mastra 也会停止生成。

对话历史保存在 Mastra Memory 中,因此语音会话和文本聊天可以共享同一个 thread。