从声音特征到 CRM 回流:闪电智能 Voice Agent 沟通策略自适应系统 v1 实战

一条事件日志看起来很普通:用户说"直接告诉我物流到哪了",系统给了简短回答,CRM 里也有一个工单编号。真正排查时却发现,日志没有说明这次简短回答来自用户明确要求、模型推测,还是某个旧策略残留;CRM 编号也只是被写进了上下文,并不能证明外部工单真的创建成功。

这正是把 VASI 从演示代码接进 Voice Agent 时最容易漏掉的地方。链路不是"声音进来,模型判断人格,话术切换,然后写 CRM",而是:实时语音和对话事件先形成可观察证据,证据经过置信度与风险校验,再选择表达策略;业务工具返回确定状态后,系统才把有限的事实回流到下一轮。

我的判断是,VASI v1 的第一目标不是让话术看起来更聪明,而是让每次策略切换都能解释、能撤销、能回放。用户明确说"说短一点"可以改变答案顺序和解释密度,但不能跳过金额、身份、地址等确认;低置信度要回到澄清;高风险或多轮失败要转人工。系统记录策略和依据,不记录 MBTI 或人格标签。

本文给出一个只依赖 Python 标准库的整合示例。它不连接真实 ASR、LLM、TTS 或 CRM,不提供线上效果数字。示例的价值在于把模块接口、失败边界、事件日志和测试先固定下来,便于替换成真实服务。

目录

从一条错误回流记录看系统缺口

先看一个合成事件。它不是线上日志,只用于说明排查方法:

json 复制代码
{
  "conversation_id": "conv-017",
  "transcript": "直接告诉我退款什么时候到账",
  "strategy": "answer_first:brief",
  "crm_case_id": "CRM-17"
}

这条记录有三个问题:

  1. "直接告诉我"是用户明确说的,还是语音特征模型推测的,没有来源字段;
  2. 退款涉及权益,为什么没有记录风险校验结果;
  3. 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

测试覆盖:

  1. 显式偏好改变表达细节;
  2. 低置信度回到澄清;
  3. 高风险覆盖表达偏好;
  4. 反馈快照保留策略和 CRM 引用;
  5. trace 记录证据来源且不生成 MBTI 标签;
  6. CRM 意图不会伪装成外部写入成功;
  7. 用户纠正只改变当前轮对象;
  8. 默认策略仍然可执行。

run_demo.py 会打印三条合成链路:低风险直接回答、低置信度澄清和高风险转人工。输出可重复,用来检查字段和状态,不用于证明识别准确率、转人工率或 CRM 性能。

如果替换成真实服务,我会先保留这套纯函数测试,再为 ASR、LLM、TTS、CRM 分别增加契约测试和故障注入测试。不要一上来用端到端满意度掩盖某个模块的状态错误。

中文客服上线前的验收样本和指标

上线前至少准备以下样本:

样本 预期策略 关键检查
"直接告诉我快递到哪了" answer_first:brief 先给已确认物流事实,不省略时间范围
"我说的是 12 号楼,不是 20 号楼" 澄清并重新确认 不把旧地址写回 CRM
"直接告诉我退款什么时候到账" handoff 或风险确认 不因简短偏好跳过权益核验
"我要找主管,别再解释了" handoff 交接载荷包含原话与失败原因
CRM 请求超时 clarifyhandoff 不能把 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 状态是真的"放在"话术听起来更自然"之前。对于中文客服,先把这两个底座搭稳,再讨论更细的声音沟通适配,成本更低,也更容易排障。

参考资料

相关推荐
Python私教1 小时前
AI 并行编码的 Worktree 生命周期:创建、隔离与安全回收
人工智能·git
jufeng13071 小时前
【系列:手搓自主 AI Agent:Hermes 架构原理剖析 · 第 6 篇】
python·ai agent·记忆系统
ZhengEnCi1 小时前
什么是 SFT 监督微调:大模型从只会续写到会听话的关键一步
人工智能
桃西西呀1 小时前
你的 AI Agent 真的会做题吗?Harbor评测框架帮你测评
人工智能
Python私教1 小时前
AI Agent 可观测性实战:从 correlationId 到失败时间线
人工智能·后端
Dave1205462 小时前
AI Agent学习 Day14:LangGraph进阶 —— 条件分支、Tool Node与Memory持久化
人工智能·学习
乱世刀疤2 小时前
深度 | 美国AI开始攻击真人
人工智能
qq_25294131682 小时前
列车车轮缺陷智能检测数据集:800张图像、4大类别,助力铁路安全运维
运维·人工智能·安全·yolo·目标检测·计算机视觉·视觉检测
kevinnett2 小时前
别再把模型地址写死了:用 Python 设计一个可切换的 LLM 调用层
python