一段电话刚结束,系统拿到一组看似很有用的事件:用户在 12 秒内连续追问两次、说话速度偏快、上一轮回答被打断。最容易写出的规则是:fast_speech -> 简短回答。
我不建议这样做。语速快可能只是赶时间,也可能是线路卡顿后用户在重复;一次打断可能意味着对方没耐心,也可能意味着客服正把金额、日期和地址揉在一句话里。把这些信号直接变成"用户偏好",很快就会把服务策略做成另一种人格贴标签。
VASI 3 要解决的不是"如何从声音猜人",而是把本轮会话中可观察的信号,收敛为一次可撤销的话术动作:少说一点、先确认、放慢步骤,或继续探索。每次切换都要能回答三个问题:依据是什么?为什么这次没有切?用户下一句话反对时怎么撤回?
本文使用一个无第三方依赖的策略引擎示例。它验证决策顺序和回退路径,不代表闪电智能 Voice Agent 的线上阈值或识别效果。
目录
先别让语速直接决定话术
下面这段是合成的事件快照,不是生产日志。它的价值在于说明:同一批声学和对话信号,不能绕过业务状态直接进入话术模板。
json
{
"speech_rate_ratio": 1.24,
"pause_ms": 180,
"clarification_count": 2,
"interruption_count": 1,
"asr_confidence": 0.63,
"business_state": "address_change",
"explicit_preference": null
}
如果只看前四项,系统可能选"高效推进":一句话给步骤,减少铺垫。但地址变更是关键字段操作,且 ASR 置信度偏低。这时更合理的动作是"短句 + 字段确认",而不是把回答压得更短。
所以,语速、停顿、追问和打断只能做弱证据。它们能影响候选策略的分数,却不能单独决定用户属于哪类人,更不能压过金额、身份、地址、投诉或多轮失败这些风险状态。
四类输入,四类话术动作
策略引擎不需要把每一种信号都送给大模型解释。先把输入分层,后面的排障会轻松得多。
| 输入层 | 可用信号 | 能影响什么 | 不能推出什么 |
|---|---|---|---|
| 显式偏好 | "直接说结果""请慢一点""别重复" | 回答长度、确认频率、步骤颗粒度 | 长期人格或跨场景偏好 |
| 对话行为 | 追问次数、打断、轮次长度、停顿 | 候选策略的弱加分 | 用户意图已经完全确定 |
| 识别质量 | ASR 置信度、关键字段缺失、端点不稳 | 是否需要复述、澄清或回退 | 用户表达能力、态度或情绪标签 |
| 业务状态 | 金额、地址、身份、投诉、多轮失败 | 是否禁止强切、是否给人工入口 | 用户"喜欢"何种话术 |
这篇示例只输出四种服务动作:
efficient: 先给结论,再给可选细节;clear_confirm: 逐步说明,关键字段复述确认;care: 先承接当前问题,再给下一步,不把安抚写成长篇套话;explore: 多问一个澄清问题,避免过早假定任务目标。
它们不是人格类别,也不应该被保存为客户标签。它们只是一轮对话里可被下一轮推翻的编排选择。
策略引擎先看什么:一条不能越过的优先级
我会把策略路由写成"先禁止错误动作,再选择更合适动作"的顺序:
#mermaid-svg-9LreHR8T6FsQWZsO{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-9LreHR8T6FsQWZsO .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-9LreHR8T6FsQWZsO .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-9LreHR8T6FsQWZsO .error-icon{fill:#552222;}#mermaid-svg-9LreHR8T6FsQWZsO .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-9LreHR8T6FsQWZsO .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-9LreHR8T6FsQWZsO .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-9LreHR8T6FsQWZsO .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-9LreHR8T6FsQWZsO .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-9LreHR8T6FsQWZsO .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-9LreHR8T6FsQWZsO .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-9LreHR8T6FsQWZsO .marker{fill:#333333;stroke:#333333;}#mermaid-svg-9LreHR8T6FsQWZsO .marker.cross{stroke:#333333;}#mermaid-svg-9LreHR8T6FsQWZsO svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-9LreHR8T6FsQWZsO p{margin:0;}#mermaid-svg-9LreHR8T6FsQWZsO .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-9LreHR8T6FsQWZsO .cluster-label text{fill:#333;}#mermaid-svg-9LreHR8T6FsQWZsO .cluster-label span{color:#333;}#mermaid-svg-9LreHR8T6FsQWZsO .cluster-label span p{background-color:transparent;}#mermaid-svg-9LreHR8T6FsQWZsO .label text,#mermaid-svg-9LreHR8T6FsQWZsO span{fill:#333;color:#333;}#mermaid-svg-9LreHR8T6FsQWZsO .node rect,#mermaid-svg-9LreHR8T6FsQWZsO .node circle,#mermaid-svg-9LreHR8T6FsQWZsO .node ellipse,#mermaid-svg-9LreHR8T6FsQWZsO .node polygon,#mermaid-svg-9LreHR8T6FsQWZsO .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-9LreHR8T6FsQWZsO .rough-node .label text,#mermaid-svg-9LreHR8T6FsQWZsO .node .label text,#mermaid-svg-9LreHR8T6FsQWZsO .image-shape .label,#mermaid-svg-9LreHR8T6FsQWZsO .icon-shape .label{text-anchor:middle;}#mermaid-svg-9LreHR8T6FsQWZsO .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-9LreHR8T6FsQWZsO .rough-node .label,#mermaid-svg-9LreHR8T6FsQWZsO .node .label,#mermaid-svg-9LreHR8T6FsQWZsO .image-shape .label,#mermaid-svg-9LreHR8T6FsQWZsO .icon-shape .label{text-align:center;}#mermaid-svg-9LreHR8T6FsQWZsO .node.clickable{cursor:pointer;}#mermaid-svg-9LreHR8T6FsQWZsO .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-9LreHR8T6FsQWZsO .arrowheadPath{fill:#333333;}#mermaid-svg-9LreHR8T6FsQWZsO .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-9LreHR8T6FsQWZsO .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-9LreHR8T6FsQWZsO .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-9LreHR8T6FsQWZsO .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-9LreHR8T6FsQWZsO .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-9LreHR8T6FsQWZsO .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-9LreHR8T6FsQWZsO .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-9LreHR8T6FsQWZsO .cluster text{fill:#333;}#mermaid-svg-9LreHR8T6FsQWZsO .cluster span{color:#333;}#mermaid-svg-9LreHR8T6FsQWZsO div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-9LreHR8T6FsQWZsO .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-9LreHR8T6FsQWZsO rect.text{fill:none;stroke-width:0;}#mermaid-svg-9LreHR8T6FsQWZsO .icon-shape,#mermaid-svg-9LreHR8T6FsQWZsO .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-9LreHR8T6FsQWZsO .icon-shape p,#mermaid-svg-9LreHR8T6FsQWZsO .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-9LreHR8T6FsQWZsO .icon-shape .label rect,#mermaid-svg-9LreHR8T6FsQWZsO .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-9LreHR8T6FsQWZsO .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-9LreHR8T6FsQWZsO .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-9LreHR8T6FsQWZsO :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
是
否
是
否
会话事件
用户明确说了偏好?
直接采用显式偏好
是否金额、地址、身份、投诉或多轮失败?
中性确认 + 人工入口
ASR 或证据是否不足?
澄清问题 / 保守回退
对话行为为候选策略打分
限幅后的话术动作
记录依据与可撤销状态
这个顺序比"先算一个偏好分数"更重要。NIST 的 AI RMF 把风险管理拆为 Govern、Map、Measure、Manage,并强调持续测量和响应;拿到电话场景里,可以把它理解为:先标出不该被优化掉的风险,再评估策略是否值得切换,而不是让一个分数统治整轮服务。NIST AI RMF Playbook 是可自愿采用的实践参考,不是客服话术的固定模板。
可运行的状态与路由示例
本地项目中的 dialogue_policy_engine.py 用两个对象区分"观察到什么"和"决定做什么"。关键点不在于这些分值多精确,而在于没有一条声学信号能越过风险回退。
python
def choose_policy(snapshot: TurnSnapshot) -> PolicyDecision:
if snapshot.explicit_preference:
return PolicyDecision(snapshot.explicit_preference, "explicit_preference", 1.0)
if snapshot.business_state in RISK_STATES:
return PolicyDecision("clear_confirm", "risk_fallback", 1.0, handoff_available=True)
if snapshot.asr_confidence < 0.72:
return PolicyDecision("explore", "asr_low_confidence", 1.0)
scores = score_candidates(snapshot)
strategy, score = max(scores.items(), key=lambda item: item[1])
if score < 2:
return PolicyDecision("explore", "insufficient_evidence", score)
return PolicyDecision(strategy, "session_evidence", score)
score_candidates() 只接受可解释的会话证据:明确催办、连续追问、较长停顿和用户主动要求细讲。示例故意不用音高、口音、性别、年龄或地域推测;它们容易受电话窄带、设备、方言和模型误差影响,也没有必要进入服务策略。
运行示例与测试:
bash
cd examples
python3 dialogue_policy_engine.py
cd ../tests
python3 -m unittest -v test_dialogue_policy_engine.py
切换不是一次性决定:状态机和撤销
真正容易出错的地方在下一轮。系统上一轮选了 efficient,用户却说:"不是,我想把原因也讲清楚。"如果还沿用原策略,只因为刚才的语速很快,那就不是自适应,而是固执。
因此状态不要存"用户是高效型",而应存一条短生命周期的策略记录:
json
{
"active_policy": "efficient",
"source": "session_evidence",
"expires_after_turn": 1,
"override_on": ["explicit_preference", "risk_state", "asr_low_confidence"],
"evidence_codes": ["user_hurry", "two_short_turns"]
}
expires_after_turn: 1 是本例的保守设计:推测只默认影响下一轮,之后必须重新被证据支持。显式偏好、风险状态和识别不确定性都可以立即覆盖它。生产实现还应为每次策略变化写入会话日志,至少包含来源、依据编码、风险状态、是否被撤销;不要记录 MBTI 或人格结论。
测试什么,才知道切换没有越界
策略引擎的测试不应只断言"快速说话会得到高效回答"。那只是证明规则会跑。更值得锁住的是下面四条不会回归的边界:
| 测试 | 构造条件 | 期望结果 |
|---|---|---|
| 显式偏好覆盖 | 用户说"请慢一点",同时有催办和打断 | 采用 clear_confirm,来源是显式偏好 |
| 风险状态收敛 | 用户表现出催办,但正在改地址或处理金额 | clear_confirm 且人工入口可用 |
| 识别低置信度回退 | 语速和打断信号存在,关键字段 ASR 低置信度 | 进入 explore,先澄清 |
| 证据不足不硬切 | 单一短时信号,没有明确偏好 | 保持探索式问法 |
test_dialogue_policy_engine.py 已把这四条写成可运行测试。它们不测"用户满意度提升了多少",因为这里没有真实流量和对照组;上线前应另行定义完成率、重复解释率、关键字段确认失败率、转人工率和投诉率,并按业务风险分层评估。NIST AI RMF 的 Measure 部分也强调选择、应用和更新与已识别风险相匹配的方法和指标;这更适合做持续评估的框架,而不是拿一个离线准确率直接宣布策略有效。NIST AI RMF Measure
把策略当成可测、可回退的服务动作
语速、停顿和追问当然有价值,但它们的价值是让系统更早发现"这轮话术可能不合适",不是给用户定性。
一个靠谱的 Voice Agent 策略引擎,应该先尊重用户明确表达,再守住高风险字段和人工入口;只有在证据足够、没有风险冲突时,才用会话行为轻微调整回答节奏。下一轮证据变了,就允许撤回。
这比给每个人贴一个看似聪明的标签麻烦一些,但在中文客服里,麻烦通常正是系统不至于越界的那部分。
本文说明:文中的事件、分值、阈值、JSON 和 Python 代码均为机制演示与测试夹具,不代表闪电智能 Voice Agent 的线上指标、训练数据、用户画像或客户效果。生产接入应结合业务风险、真实电话样本、数据合规要求与人工服务流程单独校准。