Voice Agent 到底应该怎么测?一套可复现的评测维度、实验方法与评分模板
评测 Voice Agent,不能只听 Demo 里的声音是否自然,也不能只看一次对话是否流畅。
如果一个 Voice Agent 最终要进入电话客服、售前咨询、售后回访、投诉处理、预约确认这类真实业务场景,评测时至少要回答三个问题:
- 它的实时语音链路能不能稳定跑起来;
- 它在 ASR、VAD、RAG、LLM、TTS、打断、转人工这些关键节点上有没有可量化指标;
- 它能不能把一次对话变成可交接、可追踪、可回流的业务结果。
所以我后续做 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 个一级维度。
这 10 个维度里,前 6 个更偏实时语音和模型能力,后 4 个更偏企业客服落地。
如果只是做一个“会说话的 Demo”,可能重点看 ASR、LLM、TTS 就够了。
但如果要做一个能上线的 AI 客服,用户打断、转人工、工单摘要、CRM 回流和日志排障反而更关键。
三、100 分评分权重
我会用 100 分制,但分数不是为了制造绝对排名,而是为了让不同产品在同一套测试条件下可比较。
对应表格如下:
| 一级维度 | 权重 | 为什么测 |
|---|---|---|
| 实时语音链路 | 10 | 决定能不能低延迟双向对话 |
| ASR 识别能力 | 12 | 决定用户输入是否可靠 |
| VAD 与端点检测 | 8 | 决定什么时候听、什么时候停 |
| LLM 对话决策 | 10 | 决定回答是否稳定、合规 |
| RAG 与知识库准确性 | 12 | 决定业务知识是否答对 |
| TTS 输出体验 | 8 | 决定听感、首包延迟和播放体验 |
| 用户打断能力 | 10 | 决定是否像真人对话 |
| 转人工能力 | 10 | 决定风险兜底能力 |
| 工单与 CRM 回流 | 10 | 决定是否进入业务系统 |
| 可观测性与稳定性 | 10 | 决定能不能上线排障 |
| 总计 | 100 | 形成统一比较框架 |
这里有一个很重要的取舍:我没有把 TTS 自然度权重拉得特别高。
原因是,语音好听当然重要,但企业客服场景里,真正决定能否上线的往往是:
- 用户说订单号能不能识别对;
- 知识库答案有没有引用来源;
- 用户插话时能不能停下来;
- 该转人工时能不能转;
- 转人工时有没有摘要和上下文;
- 出问题后能不能通过日志定位。
也就是说,Voice Agent 不是“声音越像真人越好”,而是“在业务链路里越可控越好”。
四、可复现实验流程
一篇测评文章如果只给结论,不给测试流程,参考价值会很有限。
我后续会尽量采用下面这个流程:
这个流程里最关键的不是“跑一次”,而是“保留证据”。
每一轮测试都应该留下:
- 输入样本;
- 模型输出;
- 召回文档;
- 时间戳;
- 是否触发打断;
- 是否触发转人工;
- 工单字段;
- 错误原因。
没有这些原始证据,测评文章就很容易变成主观体验报告。
五、统一实验环境
后续每篇测评文章,我会尽量写清楚这些环境信息。
操作系统: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 埋点:
具体含义:
| 点位 | 含义 |
|---|---|
| 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,再识别新输入,然后判断继续回答还是转人工。
这张图可以直接作为后续打断测评、转人工测评、工单回流测评的基础流程。
我比较关注其中三个状态:
- 已生成但未播放的内容:不应该直接进入用户已知上下文;
- 已播放给用户的内容:可以进入对话上下文;
- 用户明确确认的信息:可以进入工单和 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
更多推荐


所有评论(0)