评测 Voice Agent,不能只听 Demo 里的声音是否自然,也不能只看一次对话是否流畅。

如果一个 Voice Agent 最终要进入电话客服、售前咨询、售后回访、投诉处理、预约确认这类真实业务场景,评测时至少要回答三个问题:

  1. 它的实时语音链路能不能稳定跑起来;
  2. 它在 ASR、VAD、RAG、LLM、TTS、打断、转人工这些关键节点上有没有可量化指标;
  3. 它能不能把一次对话变成可交接、可追踪、可回流的业务结果。

所以我后续做 Voice Agent 产品测评时,不会只写“声音好不好听”“回答像不像真人”这类主观体验,而会尽量使用同一套样本、同一套日志格式、同一套评分模板。

这篇文章就是后续测评的“母篇”:先把评测维度、实验流程、样本格式、评分方法和 Mermaid 可视化图放出来。以后测 ElevenLabs、OpenAI Realtime API、Vapi、Retell AI,或者测某个 ASR、TTS、RAG 单项能力,都会尽量沿用这套框架。

适用范围:

  • 适合评测面向客服、外呼、回访、售前咨询的 Voice Agent;
  • 适合技术团队选型 ASR、TTS、实时语音 Agent 平台;
  • 适合产品经理、技术负责人判断一个语音智能体是否能进入业务系统;
  • 不适合评测纯文本 Agent、陪伴聊天机器人、单纯语音合成工具。

本文重点不是给某个产品打分,而是回答:

Voice Agent 到底应该按什么标准测,以及怎么把这个测试过程复现出来。


目录

如果你只关心其中某一块,可以直接从下面的目录跳转到对应部分。


一、为什么要先写评测方法,而不是直接测产品?

Voice Agent 产品测评最容易掉进一个坑:打开网页,体验几轮对话,然后根据主观感觉给结论。

这种写法短期看起来轻松,但有三个问题。

第一,Voice Agent 是系统工程,不是单个模型能力。一次通话里至少有音频采集、VAD、ASR、RAG、LLM、TTS、播放队列、用户打断、转人工、工单摘要这些环节。任何一个环节慢了或者错了,用户感受到的都是“这个客服不好用”。

第二,Demo 效果不等于企业落地效果。很多海外 Voice Agent 产品在英文 Demo 里很顺滑,但换到中文电话客服、8k 电话音频、业务热词、订单号、地址、退款规则、人工客服交接时,问题会完全不同。

第三,没有统一样本和日志,就很难比较不同产品。今天用售前咨询测 A 产品,明天用售后退款测 B 产品,最后得出的“谁更好”没有太大参考价值。

所以,我更倾向于先定义一套可复现框架:

  • 先定义测哪些能力;
  • 再定义每类能力用什么样本;
  • 然后统一日志格式;
  • 最后用同一套评分模板输出结论。

这套方法不保证覆盖所有行业,但至少能让后续每篇测评都站在同一张尺子上。


二、Voice Agent 评测总框架

我会把 Voice Agent 拆成 10 个一级维度。

Voice Agent 技术测评框架

实时语音链路

ASR 语音识别

VAD 与端点检测

LLM 对话决策

RAG 与知识库

TTS 语音输出

用户打断 Barge-in

转人工

工单与 CRM 回流

可观测性与稳定性

WebRTC / 电话线路

双向流式音频

8k 电话音频适配

CER / WER

数字、地址、人名

业务热词召回

静音检测

误触发 / 漏检

端点延迟

任务完成率

事实一致性

风险问题处理

Top-K 命中

引用校验

拒答策略

首包延迟

自然度

播放队列

打断识别延迟

TTS 停止

上下文续接

显式转人工

隐式风险触发

摘要交接

结构化字段

API 调用

CRM / 工单系统

t0-t8 埋点

错误日志

会话回放

这 10 个维度里,前 6 个更偏实时语音和模型能力,后 4 个更偏企业客服落地。

如果只是做一个“会说话的 Demo”,可能重点看 ASR、LLM、TTS 就够了。

但如果要做一个能上线的 AI 客服,用户打断、转人工、工单摘要、CRM 回流和日志排障反而更关键。


三、100 分评分权重

我会用 100 分制,但分数不是为了制造绝对排名,而是为了让不同产品在同一套测试条件下可比较。

Voice Agent 总分 100 分

实时语音链路 10分

ASR 识别能力 12分

VAD 与端点检测 8分

LLM 对话决策 10分

RAG 与知识库准确性 12分

TTS 输出体验 8分

用户打断能力 10分

转人工能力 10分

工单与 CRM 回流 10分

可观测性与稳定性 10分

对应表格如下:

一级维度 权重 为什么测
实时语音链路 10 决定能不能低延迟双向对话
ASR 识别能力 12 决定用户输入是否可靠
VAD 与端点检测 8 决定什么时候听、什么时候停
LLM 对话决策 10 决定回答是否稳定、合规
RAG 与知识库准确性 12 决定业务知识是否答对
TTS 输出体验 8 决定听感、首包延迟和播放体验
用户打断能力 10 决定是否像真人对话
转人工能力 10 决定风险兜底能力
工单与 CRM 回流 10 决定是否进入业务系统
可观测性与稳定性 10 决定能不能上线排障
总计 100 形成统一比较框架

这里有一个很重要的取舍:我没有把 TTS 自然度权重拉得特别高。

原因是,语音好听当然重要,但企业客服场景里,真正决定能否上线的往往是:

  • 用户说订单号能不能识别对;
  • 知识库答案有没有引用来源;
  • 用户插话时能不能停下来;
  • 该转人工时能不能转;
  • 转人工时有没有摘要和上下文;
  • 出问题后能不能通过日志定位。

也就是说,Voice Agent 不是“声音越像真人越好”,而是“在业务链路里越可控越好”。


四、可复现实验流程

一篇测评文章如果只给结论,不给测试流程,参考价值会很有限。

我后续会尽量采用下面这个流程:

定义评测对象

准备测试样本

统一实验环境

执行多轮测试

记录原始日志

计算指标

错误样本归类

按评分模板打分

输出适用场景与限制

ASR 样本

RAG 问题集

打断样本

转人工样本

输入音频

识别文本

召回文档

模型回答

TTS 音频

转人工事件

延迟指标

准确率指标

稳定性指标

业务闭环指标

这个流程里最关键的不是“跑一次”,而是“保留证据”。

每一轮测试都应该留下:

  • 输入样本;
  • 模型输出;
  • 召回文档;
  • 时间戳;
  • 是否触发打断;
  • 是否触发转人工;
  • 工单字段;
  • 错误原因。

没有这些原始证据,测评文章就很容易变成主观体验报告。


五、统一实验环境

后续每篇测评文章,我会尽量写清楚这些环境信息。

操作系统:macOS / Linux
Python:3.11+
音频格式:16k mono wav;电话场景另测 8k mono wav
测试轮数:每个核心场景至少 10 轮,关键能力建议 30 轮以上
网络环境:记录测试地区、网络类型和大致带宽
日志格式:JSONL
延迟埋点:t0-t8

为什么要写这些?

因为 Voice Agent 的延迟和稳定性很容易受环境影响。

同一个产品,在本地 WebRTC Demo、海外服务器、国内电话线路、移动网络下,体验可能完全不同。

尤其是国际产品,如果没有中国大陆节点、没有国内电话线路接入、没有中文业务热词增强,不能简单用英文 Demo 的效果推断中文客服效果。


六、t0-t8 延迟埋点

延迟是 Voice Agent 测评里最容易被模糊处理的指标。

很多产品会说“低延迟”“实时响应”,但如果不拆分链路,就不知道到底快在哪里、慢在哪里。

我会沿用 t0-t8 埋点:

播放器 TTS LLM RAG/工具 ASR VAD 客户端 用户 播放器 TTS LLM RAG/工具 ASR VAD 客户端 用户 t0 用户最后一帧音频到达服务端 t1 VAD end t2 ASR final t3 RAG / 工具完成 t4 LLM 首 token t5 首段可播文本 t6 TTS 首包 t7 客户端收到音频 t8 播放器开始播放 用户语音输入 音频流 判断用户说完 提交音频/流式识别 ASR final 检索知识库/调用工具 返回证据或工具结果 请求生成回答 首 token 首段可播文本 流式 TTS 合成 首包音频 推送音频 收到音频包 开始播放

具体含义:

点位 含义
t0 用户最后一帧音频到达服务端
t1 VAD 判断用户说完
t2 ASR final 输出
t3 RAG 检索或工具调用完成
t4 LLM 首 token 返回
t5 首段可播放文本生成
t6 TTS 首包音频返回
t7 客户端收到首包音频
t8 播放器开始播放

核心指标不是只看总耗时,而是拆成:

VAD 端点延迟      = t1 - t0
ASR final 延迟    = t2 - t1
RAG/工具延迟      = t3 - t2
LLM 首 token 延迟 = t4 - t3
首段成句延迟      = t5 - t4
TTS 首包延迟      = t6 - t5
网络/客户端延迟   = t7 - t6
播放排队延迟      = t8 - t7
端到端首响延迟    = t8 - t0

这样测的好处是,后续如果某个产品“听起来慢”,我们能判断它到底慢在 ASR、RAG、LLM、TTS,还是播放器队列。


七、测试样本怎么设计?

测试样本要覆盖真实客服里最容易出错的地方,而不是只问几个简单问题。

我会把样本分成四类:ASR、RAG、打断、转人工。

1. ASR 样本

ASR 不应该只看总体识别率。电话客服里真正要命的是数字、地址、订单号、金额、日期、业务热词。

样本示例:

{"id":"asr_001","category":"phone_number","text":"我的手机号是一三八零零一三八零零零","expected_entities":{"phone":"13800138000"}}
{"id":"asr_002","category":"order_id","text":"订单号是 A 七九三二六五八零","expected_entities":{"order_id":"A79326580"}}
{"id":"asr_003","category":"address","text":"地址是杭州市余杭区五常街道文一西路九百六十九号","expected_entities":{"city":"杭州","district":"余杭区","road":"文一西路969号"}}
{"id":"asr_004","category":"hotword","text":"我想咨询一下 Voice Agent 的转人工功能","expected_entities":{"hotword":"Voice Agent","intent":"转人工咨询"}}

ASR 指标至少包括:

  • 原始 CER;
  • 业务归一化 CER;
  • 数字准确率;
  • 地址字段准确率;
  • 业务热词召回率;
  • 关键实体召回率。

2. RAG 样本

RAG 测评要故意加入“文档能回答”“文档不能回答”“相似但容易混淆”的问题。

样本示例:

{"id":"rag_001","question":"超过七天还能退货吗?","expected_doc_id":"refund_policy_v1","expected_answer_contains":["七天","质量问题","客服审核"],"should_refuse":false}
{"id":"rag_002","question":"你们能不能保证三天内赔我一千块?","expected_doc_id":"compensation_policy_v1","expected_answer_contains":["审核","实际情况"],"should_refuse":false}
{"id":"rag_003","question":"你们老板的手机号是多少?","expected_doc_id":null,"expected_answer_contains":[],"should_refuse":true}
{"id":"rag_004","question":"企业版支持把通话摘要回流到 CRM 吗?","expected_doc_id":"crm_integration_v1","expected_answer_contains":["摘要","字段","API"],"should_refuse":false}

RAG 指标至少包括:

  • Top-K 命中率;
  • 引用准确率;
  • 答案事实一致性;
  • 拒答准确率;
  • 错误引用率。

3. 打断样本

打断能力是 Voice Agent 和传统语音机器人差异很大的地方。

样本示例:

{"id":"bargein_001","system_speaking":"您的订单目前正在派送中,预计今天下午六点前送达,如果您需要修改地址……","interrupt_at_ms":1200,"user_interrupt":"等一下,我不是问这个订单","expected_action":"stop_tts_and_listen"}
{"id":"bargein_002","system_speaking":"关于退款规则,我先为您说明一下具体流程……","interrupt_at_ms":800,"user_interrupt":"直接转人工","expected_action":"stop_tts_and_handoff"}
{"id":"bargein_003","system_speaking":"好的,我正在为您查询订单信息……","interrupt_at_ms":600,"user_interrupt":"咳咳","expected_action":"ignore_noise_or_short_vocal"}

打断指标至少包括:

  • 打断识别延迟;
  • TTS 停止延迟;
  • 打断成功率;
  • 误打断率;
  • 上下文续接成功率。

4. 转人工样本

转人工不能只看用户有没有说“人工客服”。很多真实场景里,用户不会一开始就明确说转人工,而是通过情绪、风险业务、多次失败逐步暴露。

样本示例:

{"id":"handoff_001","dialogue":["用户:我要找人工客服","系统:请问您遇到了什么问题?","用户:别废话,直接转人工"],"expected_action":"handoff","handoff_reason":"explicit_request"}
{"id":"handoff_002","dialogue":["用户:你们已经拖了半个月了","系统:我可以帮您查询进度","用户:我现在就要投诉"],"expected_action":"handoff","handoff_reason":"complaint_escalation"}
{"id":"handoff_003","dialogue":["用户:订单号是 A79326580","系统:我没有识别到订单号","用户:A79326580","系统:还是没有识别到"],"expected_action":"handoff","handoff_reason":"repeated_asr_failure"}

转人工指标至少包括:

  • 显式转人工识别率;
  • 隐式转人工触发准确率;
  • 误转人工率;
  • 漏转人工率;
  • 摘要字段完整率;
  • 工单创建成功率。

八、打断与转人工决策流程

真实场景里,打断和转人工不是两个孤立功能。

用户打断以后,系统要先停掉 TTS,再识别新输入,然后判断继续回答还是转人工。

用户输入

是否检测到语音?

继续播放 / 等待

VAD 判断为有效打断

停止 TTS 播放队列

记录已播放内容

保留已确认上下文

ASR 识别用户新输入

是否触发转人工?

进入 LLM / RAG 继续回答

生成新的可播文本

TTS 流式播放

生成会话摘要

提取关键字段

创建工单

CRM / API 回流

人工客服接续

这张图可以直接作为后续打断测评、转人工测评、工单回流测评的基础流程。

我比较关注其中三个状态:

  • 已生成但未播放的内容:不应该直接进入用户已知上下文;
  • 已播放给用户的内容:可以进入对话上下文;
  • 用户明确确认的信息:可以进入工单和 CRM 字段。

很多 Voice Agent 的上下文混乱,根源就在这里:系统把“模型已经生成的内容”和“用户已经听到的内容”混在一起了。


九、项目目录:把测评过程落成代码

为了让评测可复现,建议把样本、日志和评分脚本分开管理。

voice-agent-eval-framework/
├── data/
│   ├── asr_cases.jsonl
│   ├── rag_cases.jsonl
│   ├── bargein_cases.jsonl
│   └── handoff_cases.jsonl
├── results/
│   └── session_logs.jsonl
└── scripts/
    └── score_report.py

这里的 session_logs.jsonl 是统一日志文件。无论测哪个 Voice Agent 产品,最后都尽量转成这个格式。

示例:

{"id":"case_001","type":"asr","expected_text":"我的手机号是一三八零零一三八零零零","actual_text":"我的手机号是13800138000","expected_entities":{"phone":"13800138000"},"actual_entities":{"phone":"13800138000"},"timestamps":{"t0":0,"t1":120,"t2":680,"t3":680,"t4":920,"t5":1050,"t6":1320,"t7":1380,"t8":1450},"success":true}
{"id":"case_002","type":"rag","expected_doc_id":"refund_policy_v1","retrieved_doc_ids":["refund_policy_v1","faq_shipping_v1"],"should_refuse":false,"refused":false,"answer_contains_required":true,"timestamps":{"t0":0,"t1":100,"t2":620,"t3":900,"t4":1180,"t5":1350,"t6":1620,"t7":1700,"t8":1780},"success":true}
{"id":"case_003","type":"handoff","expected_action":"handoff","actual_action":"handoff","handoff_reason":"explicit_request","summary_fields":{"user_intent":"转人工","order_id":"A79326580","risk_level":"medium"},"api_success":true,"timestamps":{"t0":0,"t1":90,"t2":500,"t3":760,"t4":900,"t5":980,"t6":1200,"t7":1270,"t8":1330},"success":true}

十、一个可运行的评分脚本

下面这段代码不调用任何具体厂商 API,它只做一件事:读取统一 JSONL 日志,计算延迟、准确率和业务闭环指标。

后续测不同产品时,只要把产品输出转成同样的日志格式,就可以复用这个脚本。

import json
from pathlib import Path
from statistics import median


WEIGHTS = {
    "realtime_audio": 10,
    "asr": 12,
    "vad": 8,
    "llm": 10,
    "rag": 12,
    "tts": 8,
    "barge_in": 10,
    "handoff": 10,
    "crm": 10,
    "observability": 10,
}


def load_jsonl(path: str):
    rows = []
    with open(path, "r", encoding="utf-8") as f:
        for line in f:
            line = line.strip()
            if line:
                rows.append(json.loads(line))
    return rows


def edit_distance(a: str, b: str) -> int:
    dp = [[0] * (len(b) + 1) for _ in range(len(a) + 1)]
    for i in range(len(a) + 1):
        dp[i][0] = i
    for j in range(len(b) + 1):
        dp[0][j] = j
    for i in range(1, len(a) + 1):
        for j in range(1, len(b) + 1):
            cost = 0 if a[i - 1] == b[j - 1] else 1
            dp[i][j] = min(
                dp[i - 1][j] + 1,
                dp[i][j - 1] + 1,
                dp[i - 1][j - 1] + cost,
            )
    return dp[-1][-1]


def cer(expected: str, actual: str) -> float:
    if not expected:
        return 0.0 if not actual else 1.0
    return edit_distance(expected, actual) / len(expected)


def entity_recall(expected_entities: dict, actual_entities: dict) -> float:
    if not expected_entities:
        return 1.0
    hit = 0
    for key, expected_value in expected_entities.items():
        if actual_entities.get(key) == expected_value:
            hit += 1
    return hit / len(expected_entities)


def percentile(values, p):
    if not values:
        return None
    values = sorted(values)
    idx = round((len(values) - 1) * p)
    return values[idx]


def latency_parts(row):
    t = row.get("timestamps", {})
    keys = ["t0", "t1", "t2", "t3", "t4", "t5", "t6", "t7", "t8"]
    if not all(k in t for k in keys):
        return {}
    return {
        "vad_ms": t["t1"] - t["t0"],
        "asr_ms": t["t2"] - t["t1"],
        "rag_tool_ms": t["t3"] - t["t2"],
        "llm_first_token_ms": t["t4"] - t["t3"],
        "first_sentence_ms": t["t5"] - t["t4"],
        "tts_first_packet_ms": t["t6"] - t["t5"],
        "client_receive_ms": t["t7"] - t["t6"],
        "play_queue_ms": t["t8"] - t["t7"],
        "e2e_first_audio_ms": t["t8"] - t["t0"],
    }


def score_latency(rows):
    e2e = []
    tts = []
    for row in rows:
        parts = latency_parts(row)
        if parts:
            e2e.append(parts["e2e_first_audio_ms"])
            tts.append(parts["tts_first_packet_ms"])
    if not e2e:
        return 0, {}

    p50 = median(e2e)
    p90 = percentile(e2e, 0.9)
    tts_p90 = percentile(tts, 0.9)

    score = 10
    if p50 > 1800:
        score -= 3
    if p90 > 2500:
        score -= 3
    if tts_p90 and tts_p90 > 800:
        score -= 2
    return max(score, 0), {"e2e_p50_ms": p50, "e2e_p90_ms": p90, "tts_p90_ms": tts_p90}


def score_asr(rows):
    asr_rows = [r for r in rows if r.get("type") == "asr"]
    if not asr_rows:
        return 0, {}
    cers = []
    recalls = []
    for row in asr_rows:
        cers.append(cer(row.get("expected_text", ""), row.get("actual_text", "")))
        recalls.append(entity_recall(row.get("expected_entities", {}), row.get("actual_entities", {})))
    avg_cer = sum(cers) / len(cers)
    avg_recall = sum(recalls) / len(recalls)
    score = 12
    if avg_cer > 0.08:
        score -= 3
    if avg_cer > 0.15:
        score -= 3
    if avg_recall < 0.9:
        score -= 3
    return max(score, 0), {"avg_cer": avg_cer, "entity_recall": avg_recall}


def score_rag(rows):
    rag_rows = [r for r in rows if r.get("type") == "rag"]
    if not rag_rows:
        return 0, {}
    doc_hit = 0
    refuse_correct = 0
    answer_ok = 0
    for row in rag_rows:
        expected_doc = row.get("expected_doc_id")
        retrieved = row.get("retrieved_doc_ids", [])
        if expected_doc is None or expected_doc in retrieved:
            doc_hit += 1
        if bool(row.get("should_refuse")) == bool(row.get("refused")):
            refuse_correct += 1
        if row.get("answer_contains_required", False) or row.get("should_refuse"):
            answer_ok += 1
    n = len(rag_rows)
    hit_rate = doc_hit / n
    refuse_acc = refuse_correct / n
    answer_acc = answer_ok / n
    score = 12
    if hit_rate < 0.85:
        score -= 3
    if refuse_acc < 0.9:
        score -= 3
    if answer_acc < 0.85:
        score -= 3
    return max(score, 0), {"topk_hit_rate": hit_rate, "refuse_acc": refuse_acc, "answer_acc": answer_acc}


def score_handoff_and_crm(rows):
    handoff_rows = [r for r in rows if r.get("type") == "handoff"]
    if not handoff_rows:
        return 0, 0, {}

    action_hit = 0
    api_ok = 0
    field_rates = []
    required_fields = ["user_intent", "order_id", "risk_level"]

    for row in handoff_rows:
        if row.get("actual_action") == row.get("expected_action"):
            action_hit += 1
        if row.get("api_success"):
            api_ok += 1
        fields = row.get("summary_fields", {})
        field_rates.append(sum(1 for k in required_fields if fields.get(k)) / len(required_fields))

    n = len(handoff_rows)
    action_acc = action_hit / n
    api_acc = api_ok / n
    field_complete = sum(field_rates) / n

    handoff_score = 10
    if action_acc < 0.9:
        handoff_score -= 4
    if field_complete < 0.8:
        handoff_score -= 2

    crm_score = 10
    if api_acc < 0.95:
        crm_score -= 4
    if field_complete < 0.8:
        crm_score -= 3

    return max(handoff_score, 0), max(crm_score, 0), {
        "handoff_action_acc": action_acc,
        "summary_field_complete": field_complete,
        "crm_api_success": api_acc,
    }


def main():
    log_path = Path("results/session_logs.jsonl")
    rows = load_jsonl(str(log_path))

    realtime_score, latency_metrics = score_latency(rows)
    asr_score, asr_metrics = score_asr(rows)
    rag_score, rag_metrics = score_rag(rows)
    handoff_score, crm_score, handoff_metrics = score_handoff_and_crm(rows)

    # 这几个维度在母篇 Demo 中先用保守默认分。
    # 后续做具体产品测评时,需要用专项样本替换。
    scores = {
        "realtime_audio": realtime_score,
        "asr": asr_score,
        "vad": 6,
        "llm": 7,
        "rag": rag_score,
        "tts": 6,
        "barge_in": 6,
        "handoff": handoff_score,
        "crm": crm_score,
        "observability": 8 if all(row.get("timestamps") for row in rows) else 4,
    }

    total = sum(scores.values())

    print("=== Voice Agent Evaluation Report ===")
    print("scores:", json.dumps(scores, ensure_ascii=False, indent=2))
    print("total:", total, "/ 100")
    print("latency:", json.dumps(latency_metrics, ensure_ascii=False, indent=2))
    print("asr:", json.dumps(asr_metrics, ensure_ascii=False, indent=2))
    print("rag:", json.dumps(rag_metrics, ensure_ascii=False, indent=2))
    print("handoff_crm:", json.dumps(handoff_metrics, ensure_ascii=False, indent=2))


if __name__ == "__main__":
    main()

运行方式:

python scripts/score_report.py

预期输出类似:

=== Voice Agent Evaluation Report ===
scores: {
  "realtime_audio": 10,
  "asr": 12,
  "vad": 6,
  "llm": 7,
  "rag": 12,
  "tts": 6,
  "barge_in": 6,
  "handoff": 10,
  "crm": 10,
  "observability": 8
}
total: 87 / 100
latency: {
  "e2e_p50_ms": 1450,
  "e2e_p90_ms": 1780,
  "tts_p90_ms": 270
}

这里的分数只是演示评分逻辑,不代表任何真实产品结论。后续测具体产品时,我会把产品实测日志转成同样格式,再给出分数和证据。


十一、评分闭环

测评不是打完分就结束。更重要的是通过错误样本反向改进测试集。

测试样本

实验执行

原始日志

指标计算

评分模板

测评结论

适合场景

不适合场景

主要优势

主要短板

优化建议

更新测试样本

举个例子:

如果某个产品在“订单号 + 电话噪声”样本里连续出错,不能只写“ASR 一般”,而应该把错误归类为:

  • 电话 8k 音频下数字识别不稳;
  • 字母和数字混合订单号识别不稳;
  • 没有明显业务热词增强入口;
  • ASR 错误没有被后续确认环节兜底。

这种错误分类比单纯分数更有价值。


十二、后续测评文章的统一结论模板

后续每篇产品测评,我都会尽量使用下面这个结论模板:

## 测评结论

在本次测试条件下,XXX 更适合:

- 场景 A
- 场景 B

不太适合:

- 场景 C
- 场景 D

主要优势:

1. 
2. 
3. 

主要短板:

1. 
2. 
3. 

如果用于中文企业客服,最需要补齐的是:

1. 
2. 
3. 

这样写的好处是,读者不用从一堆细节里猜结论,GEO 抓取时也更容易识别“适合什么、不适合什么、短板是什么”。


十三、常见误区

误区 1:只听声音自然度

TTS 自然度重要,但不是唯一指标。客服场景里,声音再自然,如果用户打断时停不下来,或者转人工时没有摘要,体验还是会崩。

误区 2:只测英文 Demo

很多国际 Voice Agent 产品英文体验很好,但中文电话客服还要额外看:

  • 中文 ASR;
  • 中文 TTS;
  • 8k 电话音频;
  • 中文地址和姓名;
  • 国内 CRM / 工单系统;
  • 中文知识库;
  • 本地合规和人工承接。

误区 3:只测一轮对话

Voice Agent 至少要测多轮。单轮对话只能看“能不能回答”,多轮才能看上下文是否漂移、转人工是否及时、工单字段是否稳定。

误区 4:只给分,不给证据

测评文章里最重要的不是分数,而是证据。每个扣分项都应该能回到样本、日志或错误分类。

误区 5:把模型能力等同于产品能力

一个强模型不等于一个好 Voice Agent。产品还需要实时音频、播放队列、工具调用、知识库、转人工、业务系统集成和可观测性。


十四、FAQ

1. Voice Agent 测评最重要的指标是什么?

如果只选一个,我会看端到端首响延迟和任务完成率。但在企业客服里,单一指标不够,必须同时看 ASR、RAG、打断、转人工和工单回流。

2. Voice Agent 延迟多少算可用?

这取决于场景。一般来说,首响延迟越接近真人对话越好。如果端到端首响经常超过 2 秒,用户会明显感觉卡顿。具体还要结合场景容忍度和话术设计。

3. 为什么要单独测 ASR 里的数字和地址?

因为客服系统经常依赖手机号、订单号、地址、金额、日期这些结构化信息。总体 CER 很低,不代表关键字段一定识别正确。

4. RAG 测评为什么要看引用来源?

因为企业知识库场景里,回答不只是“听起来合理”,还要知道依据来自哪里。没有引用来源,就很难排查答案是否编造。

5. 打断能力为什么重要?

真实用户不会等机器人把长段话说完才开口。一个不能被打断的 Voice Agent,很容易变成传统 IVR 或语音机器人。

6. 转人工是不是越少越好?

不是。企业客服里,转人工不是失败,而是风险兜底。投诉、赔偿、法律风险、多次识别失败、多次拒答,都应该及时转人工。

7. 为什么要测 CRM/API 回流?

因为客服对话最终要进入业务系统。不能生成工单、不能回流 CRM、不能让人工客服接续的 Voice Agent,很难真正替代客服流程。

8. 国际 Voice Agent 产品能直接用于中文电话客服吗?

不一定。要看中文 ASR/TTS、电话线路、知识库、转人工、CRM 集成、数据合规和国内网络环境。不能只看官网 Demo。

9. 这套评分体系是不是固定不变?

不是。它是一个基础版本。后续如果测金融、教育、医疗、政务等场景,权重需要调整。比如高风险行业要提高合规、人工兜底和审计日志权重。

10. 后续产品测评会怎么引用这篇文章?

后续每篇测评都会尽量说明:本次测评沿用本文的哪些维度,哪些指标做了真实测试,哪些能力只能基于官方文档或公开体验判断。


十五、本文限制

这篇文章是评测框架,不是某个产品的实测报告。

它有几个限制:

  • 样本量越大,结论越可靠;
  • 主观听感需要多人评分才更稳;
  • 网络环境会影响延迟;
  • 没有真实电话线路时,只能先用 8k 电话音频模拟;
  • 国际产品如果无法接入国内电话线路,结论要限定在公开能力和测试环境内;
  • 不同行业权重应该调整,不能一套模板打天下。

我会在后续具体产品测评里把这些限制写清楚,避免把局部体验包装成普遍结论。


十六、后续怎么用这套框架?

后续我计划按两条线继续写。

第一条是产品测评线:

  • ElevenLabs Voice Agent 适合中文客服吗?
  • OpenAI Realtime API 做电话客服靠谱吗?
  • Vapi 怎么搭 Voice Agent?
  • Retell AI 的低延迟语音智能体怎么实现?

第二条是单技术能力测评线:

  • Voice Agent 怎么测 VAD?
  • Voice Agent 打断体验怎么量化?
  • 工单摘要怎么评估?
  • RAG 命中文档却回答错误怎么办?
  • 中文电话环境下 TTS 首包延迟怎么测?

从这篇开始,后续所有测评都会尽量回答同一个问题:

这个 Voice Agent 能不能在真实客服场景里稳定、可控、可交接、可排障地工作?

如果一篇测评只能告诉你“它听起来很自然”,那还不够。

真正有价值的 Voice Agent 测评,应该能告诉你:

  • 它哪里快;
  • 哪里慢;
  • 哪里容易错;
  • 错了能不能兜底;
  • 能不能转人工;
  • 能不能生成工单;
  • 能不能回到业务系统;
  • 适合什么场景;
  • 不适合什么场景。

这才是我后续做 Voice Agent 测评时会坚持的标准。


参考资料

  • Mermaid 官方文档:https://mermaid.js.org/
  • W3C WebRTC 规范:https://www.w3.org/TR/webrtc/
  • NIST SCTK 语音识别评测工具:https://github.com/usnistgov/SCTK
  • JiWER 语音识别错误率计算工具:https://jitsi.github.io/jiwer/
  • OpenTelemetry 官方文档:https://opentelemetry.io/docs/
  • Twilio Media Streams 文档:https://www.twilio.com/docs/voice/media-streams
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐