把 Voice Agent 的费用除以通话分钟数,得到一个稳定下降的数字,很容易让人觉得系统越来越省。但同一段通话可能包含 ASR、模型推理、流式 TTS、工具调用、失败重试、短信或人工接管;更关键的是,用户任务是否真的被解决,和通话时长并不成正比。一通很短的“地址已经改好”电话,若 CRM 实际没有确认,成本再低也没有业务价值。

我不建议用“每分钟成本”做 Voice Agent 的主经营指标。分钟成本适合观察线路或语音资源效率;业务决策更应看成功任务成本:为了让一个任务安全闭环,系统一共消耗了多少资源。这个分子必须包含失败尝试和补偿,分母必须事先定义什么叫成功,尤其要明确完整转人工是否算作安全完成。

本文给出一个不依赖厂商价格的成本模型。示例金额均为合成数据,只验证分摊和分母政策,不表示任何模型、电话、CRM 或人工服务的实际报价、性能和单位经济性。

目录

分钟成本为什么会把问题看反

分钟是一种资源计量,不是用户结果。两种看似节省分钟的做法,反而可能抬高真实成本:

  • 机器人太早结束,把原本一次能解释清的请求变成二次来电;
  • 工具失败后不转人工,用重复追问耗掉更多模型与 TTS 轮次,最后仍需要坐席补救。

反过来,一次带完整摘要的人工交接会增加人工成本,却可能避免用户重新叙述、减少重复来电,也比错误承诺更安全。FinOps 对单位经济性的建议同样强调区分资源效率指标与业务单位指标;后者可以是每笔交易、每个服务请求或每个已解决案例的成本。FinOps Foundation:Unit Economics

因此,“按分钟看不看”不是问题。应该把它放在资源层,而不是让它单独决定自动化是否有效。

先定义“成功任务”,再谈成本

成功任务的分母政策必须公开。对任务型客服,一个保守定义可以是:

resolved:工具/业务系统确认完成,用户收到与事实一致的回复。
handoff_completed:已转人工,摘要、当前字段状态与失败原因完整,人工队列已接收。
failed / abandoned:没有形成可确认完成或完整交接。

是否将 handoff_completed 纳入成功,需要按业务场景决定。对于“查询物流”这类可以自动回答的任务,长期大量转人工通常不是目标;对于身份争议、敏感操作或高价值投诉,正确转人工可能正是应当计入成功的安全结果。无论选哪种政策,不能在成本高时把人工接管剔除、在完成率低时又把它加回去。

这个定义还要和 0825 的 SLO 口径一致。一个任务若在 SLO 中被视为“完整交接”,在成本模型里也应能追到同一个 task_id 和状态,不然会出现运营报表说任务完成、财务报表却把它当失败的两套事实。

分子必须带上失败、重试和人工

一次任务的总成本不应只取最后一次成功调用。推荐按可审计组件累积:

[
C_{task}=C_{telephony}+C_{asr}+C_{llm}+C_{tts}+C_{tool}+C_{human}+C_{retry}
]

其中 C_retry 并不是额外买来的某一项,它表示重试所产生的 ASR、模型、工具等重复消耗。实现时更可靠的方式是保留每次尝试的明细,而不是试图把“重试费用”从最终账单里猜出来。

周期层面的成功任务成本为:

[
\text{Cost per successful task}=\frac{\sum C_{task};(包含失败与放弃任务)}{\text{成功任务数}}
]

分子包含失败任务,分母只算成功任务,看起来会更严格。这正是它有价值的原因:频繁失败、重试或无人接管不会悄悄消失在平均分钟数中。

组件建议关联字段不能只记什么
电话/媒体task_id、渠道、计费单位、开始/结束单纯通话秒数
ASR/TTS流标识、用量单位、取消原因只记最终转写或最终音频
LLM路由版本、token/请求单位、工具轮次模型名称
工具/CRM工具名、尝试序号、结果状态只记最终 200
人工队列、接管/完成状态、核算规则“转人工”布尔值

敏感原文、完整号码和完整地址不应该成为成本标签。成本对账需要的是稳定的任务关联与用量单位,不是把客户内容复制到报表里。

一个可复现的成功任务成本模型

本地的 examples/cost_model.py 把成本行与任务结果拆开。最小模型有两个刻意的设计:

  1. CostLine 保留每个组件和尝试序号,失败的 TTS 或第二次 CRM 调用也进入总成本;
  2. successful_tasks() 将“是否把完整人工交接算成功”做成显式参数,不让它藏在 SQL 条件里。
lines = [
    CostLine("a", "asr", 0.02),
    CostLine("a", "llm", 0.03),
    CostLine("b", "human", 0.15),
]
outcomes = [TaskOutcome("a", "resolved"), TaskOutcome("b", "handoff_completed")]

cost = cost_per_success(lines, outcomes, include_handoff=True)

注意 cost_per_success() 的分子是全部成本行,不是只有 a 与 b 的最后一次调用。测试中还专门断言“没有成功任务”返回 None,而不是虚假的零成本;零分母往往意味着数据接入或任务定义出了问题。

cd "CSDN-VoiceAgent-成功任务成本(0827)"
PYTHONPATH=. python3 -m examples.run_demo
PYTHONPATH=. python3 -m unittest discover -s tests -v

7 项标准库测试覆盖失败尝试进入分子、任务过滤、人工接管政策、零分母与组件拆分。示例数字只是计算夹具,不能用于预算或供应商比较。

成本变高不一定是坏消息

成功任务成本上涨,不必然说明模型变贵或团队做错了。它至少有四种不同解释:

  • 任务结构变化:高复杂度问题占比上升;
  • 风险策略变化:更多可疑请求被转人工,短期人工成本增加;
  • 故障变化:工具超时与重试增加,失败成本被真实记出来;
  • 价值变化:更多任务真正闭环,分子上升但重复来电或人工二次处理下降。

前三种需要靠任务类型、错误类型、版本和人工接管原因继续拆分。第四种则需要和业务结果一起验证,例如重复来电率、人工补正率、问题解决率。成本模型本身不能证明这些结果,只能让团队不再把资源消耗和业务价值放在不同的账本里。

我不建议在没有任务质量指标时直接做“压缩每分钟成本”。如果错误承诺、重试和人工补救被排除在模型之外,省下来的只是报表里的成本。

代码如何固定核算口径

成本项目最容易失控的地方不是公式,而是口径在不同报表里悄悄不一样。应将以下信息和代码一起版本化:成功任务定义、共享成本分摊规则、人工接管政策、任务类型映射、排除项以及费用数据延迟的处理方法。

像 LLM 或电话线路这样的资源,账单可能按时间窗口到达;业务状态可能稍后才从 CRM 回调。不要为了日报把不完整状态强行归类。保留 cost_observed_at 与 outcome_observed_at,在状态未收敛时标记待结算,比后续大规模回填更可解释。

账单、Trace 与业务状态怎样对齐

成本数据至少需要三把键:task_id 表示一次用户任务;trace_id 关联跨服务事件;attempt 区分重试和补偿。它们并不总是一一对应:一通电话里可能有多个任务,一个任务也可能异步重试多次。

接入时先选择一个业务任务键,不要直接用电话 call_id 替代。否则同一通电话的多个问题会被汇成一个成本单位;更糟的是,电话重拨或人工回呼会被误当成同一任务的自然延续。任务键的创建、合并和关闭规则应由产品和业务共同定义。

这篇文章最终落在一条简单规则上:分钟成本用来优化资源,成功任务成本用来判断自动化是否划算;分子必须诚实地包含失败和补救,分母必须公开地定义成功。

参考资料

Logo

火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。

更多推荐