最近在负责公司智能客服系统的质量保障工作,传统测试方法带来的高成本和低覆盖率问题让我头疼不已。经过一段时间的探索和实践,我们落地了一套基于AI的智能客服测试方案,效果显著。今天就来分享一下我们的实践思路和具体实现,希望能给有类似需求的同学一些参考。

智能客服测试示意图

1. 传统测试的痛点:为什么我们需要改变?

我们最初的测试方案是基于规则的脚本测试。简单来说,就是测试工程师根据产品文档,编写大量的“用户提问-客服回答”的测试用例脚本。这套方案在初期看似有效,但随着业务发展,问题逐渐暴露:

  • 脚本维护成本极高:客服知识库每周都在更新,每次更新都意味着大量测试脚本需要同步修改。一个简单的业务逻辑变动,可能需要修改几十个甚至上百个测试用例,人力投入巨大。
  • 长尾场景覆盖不全:用户的实际提问千奇百怪,充满了口语化、错别字和省略。我们预先编写的脚本只能覆盖主流场景,大量边缘案例(长尾问题)无法被测试到,导致线上经常出现“答非所问”的bad case。
  • 多轮对话测试困难:真实的客服对话往往是多轮的,用户会根据上一轮的回答继续追问。基于静态脚本的测试很难模拟这种动态的、有上下文的对话流,测试深度不足。
  • 异常检测依赖人工:判断一个回答是否“异常”(比如答错、答偏、态度不好),主要靠测试人员肉眼检查,效率低且主观性强,难以规模化。

正是这些痛点,迫使我们思考如何利用技术手段,让测试变得更“智能”、更高效。

2. 技术选型:为什么是“BERT + 规则”的混合方案?

在方案设计初期,我们调研了几种主流的技术路径:

  • 纯规则引擎:这是我们原来的方案。优点是规则明确、可控性强、解释性好。缺点就是上面说的,维护成本高、泛化能力差,无法理解语义。
  • 纯NLP模型(如BERT):利用预训练语言模型直接对用户query进行意图分类或生成回复。优点是泛化能力强,能处理未见过的话术变体。缺点是模型存在“幻觉”,可能生成不符合业务规则的答案,且训练需要大量标注数据。
  • 强化学习:让AI智能体通过与模拟环境互动来学习测试策略。理论上非常强大,能探索出人意料的测试路径。但实践下来,训练不稳定、收敛慢,且对仿真环境要求极高,短期内难以落地。

综合评估后,我们选择了 “BERT微调 + 轻量级业务规则”的混合方案。这个方案的核心思想是:

  1. 让BERT做它擅长的事:理解用户query的意图和提取关键实体。这是NLP模型的强项。
  2. 让规则做它该做的事:基于识别出的意图和实体,去匹配和校验预设的、结构化的业务知识库(FAQ、流程节点等)。这保证了答案的准确性和合规性。
  3. 两者结合:模型负责“听懂人话”,规则负责“按章办事”。既利用了AI的泛化能力,又保留了规则的可控性。

3. 核心实现:三步构建智能测试引擎

我们的智能测试引擎主要包含三个核心模块:意图识别、对话流自动生成和智能异常检测。

3.1 意图识别模块:让机器“听懂”用户问题

我们使用BERT进行意图分类。首先需要收集和标注数据。冷启动阶段,我们利用现有的客服日志,通过聚类和人工标注的方式,初步构建了十几个意图类别(如“查询物流”、“产品咨询”、“投诉建议”等)的样本。

import torch
from transformers import BertTokenizer, BertForSequenceClassification, Trainer, TrainingArguments
from torch.utils.data import Dataset
import pandas as pd

class IntentDataset(Dataset):
    """自定义意图分类数据集"""
    def __init__(self, texts, labels, tokenizer, max_len=128):
        self.texts = texts
        self.labels = labels
        self.tokenizer = tokenizer
        self.max_len = max_len

    def __len__(self):
        return len(self.texts)

    def __getitem__(self, idx):
        text = str(self.texts[idx])
        label = self.labels[idx]
        encoding = self.tokenizer.encode_plus(
            text,
            add_special_tokens=True,
            max_length=self.max_len,
            padding='max_length',
            truncation=True,
            return_attention_mask=True,
            return_tensors='pt',
        )
        return {
            'input_ids': encoding['input_ids'].flatten(),
            'attention_mask': encoding['attention_mask'].flatten(),
            'labels': torch.tensor(label, dtype=torch.long)
        }

def train_intent_model(train_df, val_df, model_name='bert-base-chinese', num_labels=10):
    """
    微调BERT意图分类模型
    Args:
        train_df: 训练集DataFrame,包含‘text’和‘label’列
        val_df: 验证集DataFrame
        model_name: 预训练模型名称
        num_labels: 意图类别数量
    Returns:
        trainer: 训练好的Trainer对象
    """
    tokenizer = BertTokenizer.from_pretrained(model_name)
    model = BertForSequenceClassification.from_pretrained(model_name, num_labels=num_labels)

    train_dataset = IntentDataset(train_df['text'].tolist(), train_df['label'].tolist(), tokenizer)
    val_dataset = IntentDataset(val_df['text'].tolist(), val_df['label'].tolist(), tokenizer)

    training_args = TrainingArguments(
        output_dir='./results',
        num_train_epochs=3,
        per_device_train_batch_size=16,
        per_device_eval_batch_size=64,
        warmup_steps=500,
        weight_decay=0.01,
        logging_dir='./logs',
        logging_steps=50,
        evaluation_strategy="epoch",
        save_strategy="epoch",
        load_best_model_at_end=True,
    )

    trainer = Trainer(
        model=model,
        args=training_args,
        train_dataset=train_dataset,
        eval_dataset=val_dataset,
    )
    trainer.train()
    return trainer, tokenizer

# 示例:加载数据并训练
# train_data = pd.read_csv('train_intent.csv')
# val_data = pd.read_csv('val_intent.csv')
# trainer, tokenizer = train_intent_model(train_data, val_data, num_labels=15)
3.2 对话流自动生成:模拟真实多轮对话

测试单轮问答容易,测试多轮对话难。我们的方案是将客服对话流程抽象为一个有向无环图(DAG)状态机。每个节点代表一个对话状态(如“确认订单号”、“询问问题类型”、“提供解决方案”),边代表状态间的转移条件(如“用户提供了有效订单号”)。

对话状态机示意图

基于这个DAG,我们可以使用图遍历算法(如随机游走、深度优先搜索)自动生成成千上万条不同的、符合业务逻辑的对话路径。这极大地扩展了测试场景的覆盖度。

import networkx as nx
import random
from typing import List, Dict

class DialogueGraph:
    """基于DAG的对话流生成器"""
    def __init__(self):
        self.graph = nx.DiGraph()

    def add_state(self, state_id: str, state_info: Dict):
        """添加一个对话状态节点"""
        self.graph.add_node(state_id, **state_info)

    def add_transition(self, from_state: str, to_state: str, condition: str):
        """添加状态转移边及条件"""
        self.graph.add_edge(from_state, to_state, condition=condition)

    def generate_dialogue_path(self, start_state: str, max_turns: int = 10) -> List[Dict]:
        """
        从起始状态随机生成一条对话路径
        Args:
            start_state: 起始状态ID
            max_turns: 最大对话轮次
        Returns:
            path: 对话路径列表,每个元素包含状态ID和转移条件
        """
        path = []
        current_state = start_state
        for _ in range(max_turns):
            path.append({'state': current_state, 'info': self.graph.nodes[current_state]})
            # 获取当前节点的所有出边(可能的下一个状态)
            successors = list(self.graph.successors(current_state))
            if not successors:
                break
            # 随机选择一个后继状态
            next_state = random.choice(successors)
            edge_data = self.graph.get_edge_data(current_state, next_state)
            path[-1]['transition'] = edge_data['condition']
            current_state = next_state
        return path

# 示例:构建一个简单的退货流程DAG
# dg = DialogueGraph()
# dg.add_state('start', {'prompt': '您好,有什么可以帮您?'})
# dg.add_state('ask_order', {'prompt': '请提供您的订单号。'})
# dg.add_state('ask_reason', {'prompt': '请问是什么原因需要退货呢?'})
# dg.add_state('provide_solution', {'prompt': '根据您的情况,我们为您提供以下解决方案...'})
# dg.add_state('end', {'prompt': '感谢您的咨询,再见。'})
# dg.add_transition('start', 'ask_order', '用户表达退货意向')
# dg.add_transition('ask_order', 'ask_reason', '用户提供有效订单号')
# dg.add_transition('ask_reason', 'provide_solution', '用户说明退货原因')
# dg.add_transition('provide_solution', 'end', '用户接受方案')
# 生成10条不同的测试路径
# for i in range(10):
#     path = dg.generate_dialogue_path('start')
#     print(f"Path {i+1}: {[p['state'] for p in path]}")
3.3 智能异常检测:动态阈值与综合评分

判断客服回答是否异常,不能只靠一个简单的规则。我们设计了一个综合评分器,从多个维度评估回答质量,并为每个维度设置动态阈值。

  • 意图匹配度:用户query的意图与客服回答的预期意图是否一致?使用BERT计算语义相似度。
  • 实体准确性:回答中提到的订单号、金额、日期等关键实体是否与用户query中的一致?
  • 业务规则符合度:回答是否符合预设的业务逻辑和话术规范?(这部分用规则校验)
  • 情绪极性:客服回答的情绪是否积极、中性?避免出现负面情绪。(使用情感分析模型)

每个维度都有一个基础分数和阈值。我们根据历史测试数据,动态调整这些阈值。例如,如果发现近期“实体准确性”维度的误报很多,就适当调高其阈值,减少误判。

class AnomalyDetector:
    """智能异常检测器"""
    def __init__(self):
        # 初始化各维度检测器
        self.intent_matcher = IntentMatcher()
        self.entity_checker = EntityChecker()
        self.sentiment_analyzer = SentimentAnalyzer()
        # 动态阈值,初始值基于经验设定
        self.thresholds = {
            'intent_score': 0.7,
            'entity_score': 0.9,
            'sentiment_score': 0.3  # 情感负向分数阈值
        }
        self.history_scores = []  # 用于记录历史分数,动态调整阈值

    def detect(self, user_query: str, bot_response: str, expected_intent: str) -> Dict:
        """
        检测单轮对话是否存在异常
        Args:
            user_query: 用户问题
            bot_response: 客服回答
            expected_intent: 当前对话节点预期的意图
        Returns:
            result: 包含各维度分数和最终判断的字典
        """
        scores = {}
        # 1. 意图匹配度检测
        intent_score = self.intent_matcher.compare(expected_intent, bot_response)
        scores['intent_score'] = intent_score
        # 2. 实体一致性检测
        entity_score = self.entity_checker.check(user_query, bot_response)
        scores['entity_score'] = entity_score
        # 3. 情感分析
        sentiment_score = self.sentiment_analyzer.analyze(bot_response)
        scores['sentiment_score'] = sentiment_score

        # 综合判断
        is_anomaly = False
        anomaly_reasons = []
        if intent_score < self.thresholds['intent_score']:
            is_anomaly = True
            anomaly_reasons.append(f"意图不匹配(得分: {intent_score:.2f})")
        if entity_score < self.thresholds['entity_score']:
            is_anomaly = True
            anomaly_reasons.append(f"实体错误(得分: {entity_score:.2f})")
        if sentiment_score > self.thresholds['sentiment_score']: # 情感负向分数高
            is_anomaly = True
            anomaly_reasons.append(f"情感负面(得分: {sentiment_score:.2f})")

        # 记录本次分数用于后续阈值调整
        self._update_history(scores)

        return {
            'is_anomaly': is_anomaly,
            'scores': scores,
            'reasons': anomaly_reasons
        }

    def _update_history(self, scores: Dict):
        """更新历史分数记录,并定期调整阈值(简化示例)"""
        self.history_scores.append(scores)
        if len(self.history_scores) > 1000:  # 积累一定数据后调整
            # 简单的阈值调整策略:取历史分数的某个百分位(如5%)
            import numpy as np
            intent_scores = [s['intent_score'] for s in self.history_scores[-500:]]
            self.thresholds['intent_score'] = np.percentile(intent_scores, 5)  # 动态调整为下5%分位数
            # 清空旧历史,防止内存无限增长
            self.history_scores = self.history_scores[-500:]

4. 性能优化:让智能测试跑得更快更稳

模型上线,性能是关键。我们主要做了两方面的优化:

  • 模型量化与加速部署:使用 ONNX Runtime 对微调后的BERT模型进行量化(int8)和推理加速。相比原生PyTorch,推理速度提升了3-5倍,内存占用减少约60%,完全满足高并发测试的需求。
  • 压力测试:使用 Locust 编写压测脚本,模拟海量用户同时发起咨询。我们重点关注两个指标:QPS(每秒查询率)P99响应延迟。通过压测,我们确定了单台服务器的承载能力,并为弹性伸缩提供了数据依据。
# Locust 压测脚本示例 (locustfile.py)
from locust import HttpUser, task, between
import json

class ChatbotUser(HttpUser):
    wait_time = between(0.5, 2)  # 用户等待时间

    @task
    def test_intent(self):
        # 模拟发送用户query进行意图识别
        headers = {'Content-Type': 'application/json'}
        test_queries = [
            "我的快递到哪里了",
            "我要退货怎么操作",
            "这个商品有优惠吗"
        ]
        for query in test_queries:
            payload = json.dumps({"text": query})
            self.client.post("/api/v1/predict/intent", data=payload, headers=headers)

    @task(3)  # 权重为3,更频繁地执行
    test_dialogue(self):
        # 模拟发起一轮完整对话
        payload = json.dumps({
            "session_id": "test_123",
            "query": "订单123456怎么还没发货?"
        })
        self.client.post("/api/v1/chat", data=payload)

5. 实践中的避坑指南

在落地过程中,我们踩过不少坑,这里分享三个关键问题的解决方案:

  1. 冷启动语料收集:一开始没有标注数据怎么办?我们的策略是“爬取+生成+众包”。先爬取公开的客服对话语料,再用数据增强技术(如回译、同义词替换)生成更多变体,最后请业务人员对关键样本进行精标。快速构建了一个可用的初版训练集。
  2. 多轮对话上下文保持:AI测试机器人也需要“记忆力”。我们为每一条测试对话会话(session)维护一个上下文缓存,存储历史对话的意图、实体和关键信息。每次生成新的用户query时,会结合上下文信息,使其更符合真实对话逻辑。
  3. 敏感词过滤的误判:直接使用关键词过滤会误伤很多正常表述(如“打击盗版”中的“打击”)。我们升级为“敏感词+上下文判断”的模式。先检测敏感词,再使用一个轻量级模型判断当前语境是否真的涉及违规。误判率大幅下降。

6. 延伸思考:未来还能怎么优化?

这套方案上线后,我们的测试效率提升了3倍以上,但技术探索永无止境。这里抛出三个开放式问题,也是我们团队正在思考的方向:

  1. 如何实现测试用例的“自生长”? 能否让AI在测试过程中,自动发现新的、未覆盖的用户问法或对话路径,并自动将其转化为新的测试用例,形成闭环?
  2. 如何评估测试的“充分性”? 除了代码覆盖率和场景覆盖率,对于对话系统,有没有更科学的指标来衡量我们的测试是否真的触达了系统的能力边界和脆弱点?
  3. “模糊测试”在NLP领域的应用? 传统的模糊测试通过随机变异输入来发现程序漏洞。能否借鉴其思想,对用户query进行有导向的语义变异(如插入噪音、变换句式),来更暴力地发现客服模型的盲区?

写在最后

从基于规则的脚本测试,到引入AI的智能测试,对我们团队来说确实是一场效率革命。最大的感受是,测试工程师的角色正在从“脚本编写者”向“质量策略设计师”和“AI训练师”转变。我们不再需要耗费大量时间维护脆弱的脚本,而是可以更专注于设计测试场景、分析bad case、优化AI模型和策略。

当然,这套方案也不是银弹,它需要算法、工程、测试同学的紧密协作,初期在数据标注和模型调优上也有一定投入。但长远来看,这笔投资是值得的。如果你也在为客服系统或类似对话系统的测试效率发愁,不妨尝试一下AI驱动的思路,或许会有意想不到的收获。

Logo

这里是“一人公司”的成长家园。我们提供从产品曝光、技术变现到法律财税的全栈内容,并连接云服务、办公空间等稀缺资源,助你专注创造,无忧运营。

更多推荐