语音Agent怎么进CI:ADK Live Eval实战

语音 Agent 最大的测试误区,是拿文字稿做回归。

文字测试能发现:

答案错了
Tool没调用
上下文丢了

但它发现不了:

用户插话时Agent会不会停
语音转写是否漏掉关键信息
多轮身份验证有没有顺序错误
同一句话换口音后Tool参数是否变化
音频流切换时上下文是否丢失

Google ADK 在 8 月 24 日新增原生 Live Evaluation 后,这类问题终于可以进入自动化测试:测试用户不再只是 JSON 里的文本,而是由模拟用户根据 Persona 和 Conversation Plan 生成真实语音,再流式送给 Live Agent;Agent 的语音回复、Tool Execution 和多轮 Trajectory 可以一起评分,并且可以从 CLI 直接接进 CI/CD。

这比“人工打电话听一遍”更接近生产工程。

先看一个最小Live Workflow

Google 的示例用三个单功能 Live Agent 串成一个 Graph:

Greeter
↓
DOB Verifier
↓
Goals Agent

中间还穿插一个验证生日的 Tool。

模型:

LIVE_MODEL = "gemini-live-2.5-flash-native-audio"

核心代码可以简化成:

from google.adk.agents.llm_agent import Agent
from google.adk.workflow import START, Workflow

greeter = Agent(
    model=LIVE_MODEL,
    name="greeter",
    mode="task",
    instruction=(
        "确认用户身份,"
        "一次只问一个问题。"
    ),
)

verifier = Agent(
    model=LIVE_MODEL,
    name="dob_verifier",
    mode="task",
    tools=[validate_date_of_birth],
    instruction=(
        "询问生日,复述确认,"
        "再调用验证工具。"
    ),
)

goals = Agent(
    model=LIVE_MODEL,
    name="goals",
    mode="task",
    instruction=(
        "仅在身份验证完成后,"
        "告知预约信息。"
    ),
)

root = Workflow(
    name="live_workflow",
    edges=[
        (START, greeter),
        (greeter, verifier),
        (verifier, goals),
    ],
)

真正值得测试的是:

身份验证前
绝不能泄露预约信息

这是 Trajectory Constraint,不只是最终答案要求。

语音测试第一类:Scenario

ADK 的一个重要设计是:

测试Case
和
执行方式
解耦

你可以写 Conversation Scenario:

{
  "eval_id": "identity_flow_01",
  "conversation_scenario": {
    "starting_prompt": "Hello?",
    "conversation_plan": "确认姓名。被问到生日时回答1985年7月12日。确认生日后询问下次预约需要准备什么。",
    "user_persona": "NOVICE"
  },
  "session_input": {
    "app_name": "live_workflow",
    "user_id": "test_user",
    "state": {}
  }
}

NOVICE 的意义不是“用户水平低”。

更接近:

模拟用户不会主动一次性把所有信息说完

Agent 必须自己推进对话。

这比固定脚本更能测:

Agent有没有真正掌握流程

第二类:Fixed Conversation

有些回归必须完全可控。

例如:

{
  "eval_id": "fixed_dob_case",
  "conversation": [
    {
      "user_content": {
        "role": "user",
        "parts": [
          {"text": "Yes, this is John Doe."}
        ]
      }
    },
    {
      "user_content": {
        "role": "user",
        "parts": [
          {"text": "My date of birth is July 12th, 1985."}
        ]
      }
    }
  ]
}

固定对话适合:

Bug Regression

Scenario 适合:

Behavior Coverage

两种都要。

真正让Live Eval成立的是Audio Simulator

Google 示例配置里:

{
  "live_model_config": {
    "timeout_seconds": 300
  },
  "user_simulator_config": {
    "type": "llm_audio",
    "model": "gemini-3.7-flash",
    "max_allowed_invocations": 10,
    "audio_model": "gemini-3.1-flash-tts-preview",
    "audio_model_configuration": {
      "response_modalities": ["AUDIO"],
      "speech_config": {
        "voice_config": {
          "prebuilt_voice_config": {
            "voice_name": "Kore"
          }
        },
        "language_code": "en-US"
      }
    }
  }
}

这里有两个模型角色:

model

负责模拟用户的对话逻辑。

audio_model

负责把用户回合变成语音。

不要把这两个参数混为一谈。

max_allowed_invocations是成本和死循环保险

动态模拟用户可能出现:

Agent不结束
Simulator继续问
Agent继续回答

所以:

"max_allowed_invocations": 10

不只是测试参数。

它实际上是:

Conversation Budget

企业场景我会按业务设置:

身份验证:
最多6轮

退款:
最多12轮

复杂售后:
最多20轮

超过阈值:

FAIL
或
转人工

Voice Eval最值得做的是Trajectory Rubric

例如:

{
  "rubric_id": "identity_before_disclosure",
  "rubric_content": {
    "text_property": "Agent必须在确认姓名并验证生日后,才能披露预约信息。"
  }
}

阈值:

{
  "rubric_based_multi_turn_trajectory_quality_v1": {
    "threshold": 0.7
  }
}

但生产里我不会只用自然语言 Rubric。

能确定性判断的规则,应该写程序。

身份验证顺序最好用确定性事件检查

例如 Event Ledger:

NAME_CONFIRMED
DOB_TOOL_CALLED
DOB_VERIFIED
APPOINTMENT_DISCLOSED

检查:

def assert_identity_before_disclosure(events):
    verified = False

    for event in events:
        if event.type == "DOB_VERIFIED":
            verified = True

        if event.type == "APPOINTMENT_DISCLOSED":
            assert verified

这比 LLM Judge 更稳定。

Rubric 负责评:

语气
自然度
是否一次问一个问题

程序负责评:

权限顺序
Tool顺序
关键状态

我会把Live Eval拆成5层指标

第一层:ASR / Input

用户语音有没有被正确理解

指标:

关键实体识别率
生日识别率
订单号识别率

第二层:Turn-taking

Agent有没有抢话
是否正确处理插话
是否停顿太久

第三层:Task

业务流程有没有完成

第四层:Tool

Tool有没有正确调用
参数有没有正确

第五层:Audio Output

回答内容和语音体验

不要把所有问题揉成一个:

Judge Score = 0.82

一个更实用的测试结果

{
  "case": "identity_flow_01",
  "asr_entity_accuracy": 1.0,
  "task_success": true,
  "tool_sequence_pass": true,
  "identity_gate_pass": true,
  "max_turn_latency_ms": 1180,
  "interrupt_handling": 0.92,
  "voice_quality": 0.88,
  "total_turns": 7
}

这样失败以后知道去哪查。

语音Agent一定要测不同声音

Google 当前配置允许换:

voice_name
language_code

企业回归不要永远只有一个标准声音。

最少:

男声
女声
快语速
慢语速
弱口音
强口音
背景噪声

如果底层 TTS Simulator 暂时不能生成全部噪声,可以对音频做后处理。

一个测试矩阵

Scenario × Voice × Speed × Noise

例如:

10业务场景
×
3声音
×
2语速
×
2噪声
=
120 Cases

比只测 10 条文本更接近真实风险。

但不要一上来全矩阵跑CI

120 个 Live Case 成本和时间都不低。

可以分层:

PR:
10个快速Smoke

main:
40个核心场景

nightly:
全矩阵

release:
全矩阵 + 高风险Case

CLI非常适合接CI

Google 当前示例命令:

uv run adk eval \
  contributing/samples/live/live_workflow \
  contributing/samples/live/live_workflow/live_workflow.evalset.json \
  --config_file_path \
  contributing/samples/live/live_workflow/test_config.json

需要:

uv pip install -e ".[eval]"

并配置 Live API 和 Gemini TTS Credentials。

GitHub Actions里怎么跑

name: live-agent-eval

on:
  pull_request:
    paths:
      - "agent/**"
      - "eval/**"

jobs:
  smoke:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - uses: astral-sh/setup-uv@v6

      - run: uv sync

      - run: |
          uv run adk eval \
            agent/live_workflow \
            eval/smoke.evalset.json \
            --config_file_path eval/test_config.json

Secret 通过 CI Secret Store 注入。

不要写进 JSON。

结果必须转成机器可读Gate

CLI 跑完以后,如果只是把长日志留在 CI:

没人会看

最好转:

{
  "passed": 38,
  "failed": 2,
  "critical_failed": 1
}

然后:

critical_failed > 0
→ block merge

高风险Case不能平均

例如:

40个Case
39个通过
1个失败

总体:

97.5%

看起来很好。

但唯一失败的是:

身份验证前泄露医疗预约

那必须:

BLOCK

所以 Case 要有:

severity

Eval Case模型

public record EvalCaseMeta(
        String caseId,
        RiskLevel risk,
        boolean releaseBlocking,
        Set tags) {
}

高风险:

releaseBlocking=true

不参与平均豁免。

语音Latency也必须进门禁

内容全对,但每轮:

4.5秒

用户会直接打断或挂电话。

我会监控:

Time to First Audio
Turn End to Response Start
Tool Round-trip
Total Conversation Duration

例如:

latency_gate:
  p50_first_audio_ms: 600
  p95_first_audio_ms: 1200
  p95_turn_response_ms: 1800

打断处理必须单独测

场景:

Agent正在说:
“您的下次预约是……”

用户:
“等等,是几点?”

Agent 应:

停止当前播报
处理插话
回答
再决定是否继续

而不是把整段话说完。

可以在模拟器加入:

interrupt after appointment date

并验证:

agent_stopped_audio = true

Tool失败也要注入

不要所有 Fixture 都成功。

例如生日验证:

Tool Timeout
Tool 500
Wrong Record

Agent 应:

不能泄露信息
不能假装验证成功

这类 Case 通常比正常流程更重要。

Session State必须跨Agent保持

Google 示例里三个 Live Agent 切换时,音频流不会中断,并且 session state 和 conversation history 会向后传。

所以回归要专门测:

Greeter已经确认姓名
↓
DOB Agent能否读取

不要让每个 Sub-Agent 重新问一遍。

可以做State Assertion

assert session.state["name_confirmed"] is True
assert session.state["dob_verified"] is True

最终 Goals Agent 执行前:

两个条件必须同时满足

ADK Web适合人工Debug,不应该成为唯一评测入口

Google 当前会把 Live Run 重建成 Transcript,并提供每回合的可播放音频。

这对失败排查非常好。

但发布门禁必须来自:

CLI
或
AgentEvaluator

否则还是人工点页面。

我会保留失败音频Artifact

CI 失败时:

transcript.json
audio-turn-03.wav
tool-events.json
metrics.json

一起上传 Artifact。

开发者不用重新复现就能听出问题。

成本也要统计

Live Eval 同时会调用:

Agent Model
Simulator Model
TTS Model
Judge

一条 Case 可能有多个模型成本。

所以记录:

cost_per_case

Nightly 全量矩阵才能知道预算。

一个完整Release Gate

voice_agent_gate:

  critical_cases:
    pass_rate: 1.0

  normal_cases:
    pass_rate: 0.95

  tool_sequence:
    pass_rate: 0.99

  p95_first_audio_ms: 1200

  max_conversation_turns: 15

  max_cost_per_case_usd: 0.30

如果某个版本:

质量不变
成本翻3倍

也不应该无条件发布。


Live Agent 从 Demo 走向生产以后,真正的难点不是“能听、能说”。

而是:

多轮状态
Tool顺序
语音延迟
插话
口音
异常恢复

这些问题单靠文本 Eval 根本覆盖不到。

ADK 现在把模拟用户、真实 TTS 音频、Trajectory Rubric、Tool Evaluation、CLI 和 ADK Web 串进同一个 Eval Loop,我觉得它最重要的价值不是多一个测试功能。

而是终于让语音 Agent 可以像普通服务一样:

改代码
→跑回归
→看失败
→阻断发布

只要这一环建立起来,语音 Agent 才算真正进入工程化阶段。


更多企业级 AI 应用、Agent、RAG 与模型工程化内容,我会继续整理在 智元界

https://www.zyentor.com/