一条事件日志看起来很普通:用户说"直接告诉我物流到哪了",系统给了简短回答,CRM 里也有一个工单编号。真正排查时却发现,日志没有说明这次简短回答来自用户明确要求、模型推测,还是某个旧策略残留;CRM 编号也只是被写进了上下文,并不能证明外部工单真的创建成功。
这正是把 VASI 从演示代码接进 Voice Agent 时最容易漏掉的地方。链路不是"声音进来,模型判断人格,话术切换,然后写 CRM",而是:实时语音和对话事件先形成可观察证据,证据经过置信度与风险校验,再选择表达策略;业务工具返回确定状态后,系统才把有限的事实回流到下一轮。
我的判断是,VASI v1 的第一目标不是让话术看起来更聪明,而是让每次策略切换都能解释、能撤销、能回放。用户明确说"说短一点"可以改变答案顺序和解释密度,但不能跳过金额、身份、地址等确认;低置信度要回到澄清;高风险或多轮失败要转人工。系统记录策略和依据,不记录 MBTI 或人格标签。
本文给出一个只依赖 Python 标准库的整合示例。它不连接真实 ASR、LLM、TTS 或 CRM,不提供线上效果数字。示例的价值在于把模块接口、失败边界、事件日志和测试先固定下来,便于替换成真实服务。
目录
- 从一条错误回流记录看系统缺口
- [先定义业务结果,再画 VASI 链路](#先定义业务结果,再画 VASI 链路)
- 每一层的输入、输出和禁止事项
- [事件契约:partial、final、策略和 CRM 状态怎么连](#事件契约:partial、final、策略和 CRM 状态怎么连)
- 特征只是证据,不是人格结论
- 策略决策的优先级:风险、置信度、显式偏好
- 从一个大函数演进到可审计对象
- [LLM、TTS 与人工交接的边界](#LLM、TTS 与人工交接的边界)
- [CRM 回流不能把"意图"说成"成功"](#CRM 回流不能把“意图”说成“成功”)
- 运行项目与测试失败路径
- 中文客服上线前的验收样本和指标
- 接入真实系统前仍然要补的工作
- 结论与限制
- 参考资料
从一条错误回流记录看系统缺口
先看一个合成事件。它不是线上日志,只用于说明排查方法:
json
{
"conversation_id": "conv-017",
"transcript": "直接告诉我退款什么时候到账",
"strategy": "answer_first:brief",
"crm_case_id": "CRM-17"
}
这条记录有三个问题:
- "直接告诉我"是用户明确说的,还是语音特征模型推测的,没有来源字段;
- 退款涉及权益,为什么没有记录风险校验结果;
crm_case_id只说明上下文里出现了编号,不说明外部 CRM 的写入状态、请求 ID 或重试结果。
如果只看最终文本,工程师很容易把问题归给 LLM。实际上,缺口发生在系统边界:输入证据没有分层,策略决定没有版本,工具状态没有区分"已确认"和"待处理"。所以本文先搭接口,再谈话术。
先定义业务结果,再画 VASI 链路
VASI v1 要交付的不是"识别出某种人",而是三件可以验收的事情:
- 用户明确要求直接回答时,低风险查询可以先给结果,再补必要说明;
- 系统没有足够证据时,不把推测包装成事实,而是澄清或回到中性策略;
- 工具和人工状态真实可追踪,下一轮只能读取已经确认的业务事实。
围绕这三个结果,链路可以画成:
text
实时语音
↓
ASR partial / final + 轮次事件
↓
可观察特征与证据来源
↓
置信度计算 + 风险与权限校验
↓
表达策略:answer_first / clarify / handoff
↓
LLM 组织文本 → TTS 播放或人工交接
↓
业务工具:CRM 查询、创建、更新
↓
确认状态与脱敏反馈回到下一轮
每个箭头都应有事件时间、请求 ID 和版本。没有这些字段,系统只能给出最后一句话,不能解释策略为什么改变。
每一层的输入、输出和禁止事项
把模块职责写成表格后,很多"模型能力问题"会变成接口问题:
| 层 | 输入 | 输出 | 禁止事项 |
|---|---|---|---|
| ASR/对话事件 | partial、final、静音、用户话语 | 文本、时间、轮次状态 | 直接生成 MBTI 或人格标签 |
| 特征/证据层 | 语速、停顿、追问、显式表达、任务上下文 | 证据、来源、置信度 | 把线索当成确定事实 |
| 策略层 | 风险状态、置信度、显式偏好 | 表达动作、原因、版本 | 绕过权限和关键字段确认 |
| LLM/TTS 层 | 已确认事实、策略约束 | 文本片段、播放动作 | 自行扩展业务承诺 |
| 工具层 | 已确认参数、鉴权上下文 | 业务状态、外部 ID | 把自由文本直接写 CRM |
| 反馈层 | case_id、状态、下一步 | 有限事实快照 | 把 CRM 全量档案塞给模型 |
用户明确说"直接说结果"属于 dialogue 来源的显式偏好;"最近说话比较快"最多是一个 asr 或音频分析线索。两者在数据结构里不能共用一个 preference 字段,否则审计时无法判断系统听的是用户,还是猜测。
事件契约:partial、final、策略和 CRM 状态怎么连
实时链路里至少有四类事件:
text
asr.partial 用户还没说完,不能触发不可逆工具动作
asr.final 本轮文本完成,可进入意图和字段校验
policy.decided 记录策略、原因、版本和证据来源
crm.updated 只有外部服务确认后才记录成功状态
partial 可以用于更新候选线索,但不能因为半句话里出现"退款"就直接创建工单。final 也不代表字段已经可信,金额、地址、订单号仍需要业务校验。
一个最小策略事件可以长这样:
json
{
"conversation_id": "conv-001",
"task_id": "query-01",
"policy_version": "vasi-v1",
"decision": {
"action": "answer_first",
"detail": "brief",
"reason": "explicit_direct"
},
"evidence": [
{"name": "explicit_request", "value": "direct", "source": "dialogue", "confidence": 1.0}
]
}
生产服务还要加 schema 版本、脱敏规则、写入失败告警和保留期限。示例先把字段结构固定,避免后续接入消息队列时把"策略结果"和"模型原始输出"混在同一条日志里。
特征只是证据,不是人格结论
VASI 的特征层可以收集:
- 电话窄带下的语速、停顿和用户打断位置;
- ASR final 文本中"直接一点""讲详细一点"等明确表达;
- 用户重复询问、纠正数字或地址的行为;
- 当前任务是普通查询、权益争议还是投诉升级。
这些信息的可靠程度不同。显式话语通常比语速线索更直接,但即使显式偏好也只有表达层作用域。用户说"快点告诉我退款时间",不等于允许系统跳过退款规则核验。
示例用不可变的 FeatureEvidence 保存线索:
python
@dataclass(frozen=True)
class FeatureEvidence:
name: str
value: str
source: Literal["asr", "dialogue", "business"]
confidence: float
这里没有 mbti_type 字段。不是为了少存一列,而是为了让系统的数据模型不鼓励人格化结论。后续如果需要研究沟通偏好,应保存可观察事件、来源和用户纠正,不保存不可验证的稳定人格标签。
策略决策的优先级:风险、置信度、显式偏好
决策顺序比阈值本身更重要。我会固定成下面的优先级:
text
高风险或明确要求人工? -> handoff
证据置信度过低? -> clarify
用户明确要求直接? -> answer_first + brief
用户明确要求详细? -> answer_first + expanded
其他 -> answer_first + normal
对应的纯函数很短,但每个分支都有明确理由:
python
def choose_strategy(turn: TurnInput) -> StrategyDecision:
if turn.high_risk:
return StrategyDecision("handoff", "normal", "high_risk")
if turn.confidence < 0.7:
return StrategyDecision("clarify", "normal", "low_confidence")
if turn.explicit_preference == "direct":
return StrategyDecision("answer_first", "brief", "explicit_direct")
if turn.explicit_preference == "detailed":
return StrategyDecision("answer_first", "expanded", "explicit_detailed")
return StrategyDecision("answer_first", "normal", "default_strategy")
0.7 只是测试夹具里的边界值,不是识别准确率,也不是线上建议。真实系统需要用标注集和人工质检确定回退策略。没有数据时,宁可把低置信度送去澄清,也不要用一条看似精确的小数替代风险判断。
从一个大函数演进到可审计对象
最初的原型可能只有一段:
python
if direct_request:
answer_style = "brief"
它能展示效果,却不能表达策略来源、版本和业务状态。项目把它拆成四个不可变对象:
TurnInput:一轮会话的事实和外部引用;FeatureEvidence:线索、来源与置信度;StrategyDecision:动作、细节、原因和策略版本;FeedbackSnapshot:下一轮允许读取的有限业务快照。
用户纠正也不应该原地修改全局状态。示例用 dataclasses.replace() 只生成当前轮的新对象:
python
def apply_user_correction(turn, preference):
return replace(turn, explicit_preference=preference)
这样可以测试"讲详细一点"只影响当前任务,而不会把偏好泄漏到下一次退款、身份核验或其他会话。生产系统还需要给状态增加 TTL、任务作用域和撤销事件。
LLM、TTS 与人工交接的边界
策略层返回的是约束,不是最终话术。一个安全的运行顺序是:
text
策略决定
-> 取已确认事实
-> LLM 按 detail 组织表达
-> 检查业务承诺和关键字段
-> TTS 分句、播放或取消
brief 只能减少解释,不应删除必须确认的字段。TTS 播放时如果用户打断,播放队列要取消未播片段,LLM 也要停止继续生成;这属于运行时控制,不应由沟通偏好函数直接处理。
高风险或多轮失败时,策略动作是 handoff。交给人工的载荷至少包括:已确认的任务、用户原话、已调用工具、失败原因和需要人工确认的字段。不要只传一句"用户不满意",那样座席仍要重新问一遍。
CRM 回流不能把"意图"说成"成功"
CRM 连接至少要区分四种状态:
| 状态 | Voice Agent 可以说什么 | 是否可当作成功 |
|---|---|---|
not_requested |
当前没有创建或更新请求 | 否 |
pending |
请求已提交,等待外部确认 | 否 |
confirmed |
外部返回已确认 ID 和状态 | 是,仍需展示范围 |
failed |
外部失败或超时,需要重试/转人工 | 否 |
项目里的 crm_write_intent() 只返回 reference_only,故意不声称写入成功:
python
def crm_write_intent(turn, decision):
if not turn.crm_case_id:
return {"status": "not_requested", "case_id": None, "strategy": None}
return {
"status": "reference_only",
"case_id": turn.crm_case_id,
"strategy": f"{decision.action}:{decision.detail}",
}
真实接入要在 CRM 客户端增加请求幂等键、外部请求 ID、重试上限、超时分类和对账任务。case_id 必须来自已鉴权的服务响应,不能从 LLM 输出或用户口述中直接写入。
下一轮只读取脱敏后的 FeedbackSnapshot:
json
{
"conversation_id": "conv-001",
"task_id": "query-01",
"strategy": "answer_first:brief",
"confidence": 0.86,
"crm_case_id": "CRM-0002"
}
原始音频、无关 CRM 历史和人格标签不应默认进入模型上下文。上下文越大不等于事实越多,反而会增加把旧状态当成当前状态的机会。
运行项目与测试失败路径
项目结构:
text
CSDN-VASI-系统整合/
├── article.md
├── README.md
├── examples/
│ ├── vasi_system.py
│ └── run_demo.py
└── tests/
└── test_vasi_system.py
项目只依赖 Python 标准库,在项目根目录运行:
bash
python3 -m unittest discover -v
python3 -m examples.run_demo
测试覆盖:
- 显式偏好改变表达细节;
- 低置信度回到澄清;
- 高风险覆盖表达偏好;
- 反馈快照保留策略和 CRM 引用;
- trace 记录证据来源且不生成 MBTI 标签;
- CRM 意图不会伪装成外部写入成功;
- 用户纠正只改变当前轮对象;
- 默认策略仍然可执行。
run_demo.py 会打印三条合成链路:低风险直接回答、低置信度澄清和高风险转人工。输出可重复,用来检查字段和状态,不用于证明识别准确率、转人工率或 CRM 性能。
如果替换成真实服务,我会先保留这套纯函数测试,再为 ASR、LLM、TTS、CRM 分别增加契约测试和故障注入测试。不要一上来用端到端满意度掩盖某个模块的状态错误。
中文客服上线前的验收样本和指标
上线前至少准备以下样本:
| 样本 | 预期策略 | 关键检查 |
|---|---|---|
| "直接告诉我快递到哪了" | answer_first:brief |
先给已确认物流事实,不省略时间范围 |
| "我说的是 12 号楼,不是 20 号楼" | 澄清并重新确认 | 不把旧地址写回 CRM |
| "直接告诉我退款什么时候到账" | handoff 或风险确认 |
不因简短偏好跳过权益核验 |
| "我要找主管,别再解释了" | handoff |
交接载荷包含原话与失败原因 |
| CRM 请求超时 | clarify 或 handoff |
不能把 pending 说成 confirmed |
指标也要按层拆开:
- 输入层:ASR final 到达率、关键字段确认率、用户纠正率;
- 策略层:显式偏好触发率、低置信度回退率、风险覆盖率;
- 运行层:TTS 首播等待、打断后停播、工具超时分类;
- 业务层:任务完成率、转人工完整率、CRM 状态对账率。
这些是指标口径,不是本文的线上结果。没有真实流量和人工标注,不能写"策略使完成率提升了多少"。尤其要单独看 crm_status_mismatch:模型回答正确但 CRM 状态错误,仍然是生产事故。
接入真实系统前仍然要补的工作
第一,输入事件必须来自可信服务。模型生成的 confidence、权限和关键字段确认不能直接当作事实;它们要经过服务端校验。
第二,策略切换要可撤销、可审计。用户说"讲详细一点"后,旧的 brief 策略应该在任务作用域内被覆盖,并记录触发事件和版本;任务切换时不能把上一任务的表达偏好带过去。
第三,CRM 回流要和幂等、重试、权限一起设计。只有本地策略决定并不能证明工单已写入;需要保留外部请求 ID、状态和对账路径。
第四,电话环境需要单独测偏差。窄带、噪声、方言和 ASR partial/final 的差异会影响线索提取,不能把文本 Demo 的结果直接推到电话生产环境。
第五,要规定数据最小化和保留期限。VASI 只需要当前任务相关的事实和表达偏好,不需要把整段录音和完整 CRM 档案复制到每一轮 LLM 上下文。
结论与限制
VASI v1 的整合重点不是多接几个模型,而是把每层的责任划清:ASR 提供事件,特征层保留证据来源,策略层执行风险优先的表达选择,LLM/TTS 负责受约束的呈现,CRM 客户端只报告真实外部状态,反馈层只回流下一轮需要的事实。
这套标准库示例能证明对象边界、状态优先级、日志结构和失败路径可测试。它不能证明 ASR、LLM、TTS 或 CRM 的真实性能,也不能据此声称系统能够识别人格。真正上线前,还需要真实电话样本、业务规则、权限审查、人工质检、数据脱敏和故障演练。
我会把"能解释为什么切换策略"和"能证明 CRM 状态是真的"放在"话术听起来更自然"之前。对于中文客服,先把这两个底座搭稳,再讨论更细的声音沟通适配,成本更低,也更容易排障。