HarmonyOS开发:智能客服与对话机器人

HarmonyOS开发:智能客服与对话机器人

核心要点:基于HarmonyOS AI能力构建多轮对话、意图识别、知识库驱动的智能客服系统


一、背景与动机

你一定有过这样的体验:深夜遇到产品问题,拨打客服电话,先是听5分钟语音菜单,然后排队等待,好不容易接通了,客服说"这个问题我需要转接",然后又是等待......整个过程可能花了30分钟,问题还没解决。

传统客服的痛点很明显------人力成本高、响应速度慢、7×24小时难以保证。而智能客服可以同时服务成千上万的用户,秒级响应,永不休息。但市面上的智能客服也有问题:要么太"智障"(只会匹配关键词),要么太"云端"(数据安全没保障),要么太"割裂"(不能无缝转人工)。

HarmonyOS的端侧AI能力给了我们一个新选择:在设备本地运行对话引擎,敏感数据不出设备;同时支持云端大模型增强,复杂问题无缝转人工。本文将带你构建一个完整的智能客服系统,从对话管理、意图识别、知识库检索到多轮对话、人工转接,全栈实现。


二、核心原理

2.1 智能客服系统架构

智能客服的核心是一个"理解→决策→执行→学习"的闭环。用户发送消息→NLU理解意图→对话管理器决策下一步→执行对应动作→更新对话状态。
#mermaid-svg-NPjdb40vNUl8zj2L{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-NPjdb40vNUl8zj2L .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-NPjdb40vNUl8zj2L .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-NPjdb40vNUl8zj2L .error-icon{fill:#552222;}#mermaid-svg-NPjdb40vNUl8zj2L .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-NPjdb40vNUl8zj2L .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-NPjdb40vNUl8zj2L .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-NPjdb40vNUl8zj2L .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-NPjdb40vNUl8zj2L .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-NPjdb40vNUl8zj2L .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-NPjdb40vNUl8zj2L .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-NPjdb40vNUl8zj2L .marker{fill:#333333;stroke:#333333;}#mermaid-svg-NPjdb40vNUl8zj2L .marker.cross{stroke:#333333;}#mermaid-svg-NPjdb40vNUl8zj2L svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-NPjdb40vNUl8zj2L p{margin:0;}#mermaid-svg-NPjdb40vNUl8zj2L .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-NPjdb40vNUl8zj2L .cluster-label text{fill:#333;}#mermaid-svg-NPjdb40vNUl8zj2L .cluster-label span{color:#333;}#mermaid-svg-NPjdb40vNUl8zj2L .cluster-label span p{background-color:transparent;}#mermaid-svg-NPjdb40vNUl8zj2L .label text,#mermaid-svg-NPjdb40vNUl8zj2L span{fill:#333;color:#333;}#mermaid-svg-NPjdb40vNUl8zj2L .node rect,#mermaid-svg-NPjdb40vNUl8zj2L .node circle,#mermaid-svg-NPjdb40vNUl8zj2L .node ellipse,#mermaid-svg-NPjdb40vNUl8zj2L .node polygon,#mermaid-svg-NPjdb40vNUl8zj2L .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-NPjdb40vNUl8zj2L .rough-node .label text,#mermaid-svg-NPjdb40vNUl8zj2L .node .label text,#mermaid-svg-NPjdb40vNUl8zj2L .image-shape .label,#mermaid-svg-NPjdb40vNUl8zj2L .icon-shape .label{text-anchor:middle;}#mermaid-svg-NPjdb40vNUl8zj2L .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-NPjdb40vNUl8zj2L .rough-node .label,#mermaid-svg-NPjdb40vNUl8zj2L .node .label,#mermaid-svg-NPjdb40vNUl8zj2L .image-shape .label,#mermaid-svg-NPjdb40vNUl8zj2L .icon-shape .label{text-align:center;}#mermaid-svg-NPjdb40vNUl8zj2L .node.clickable{cursor:pointer;}#mermaid-svg-NPjdb40vNUl8zj2L .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-NPjdb40vNUl8zj2L .arrowheadPath{fill:#333333;}#mermaid-svg-NPjdb40vNUl8zj2L .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-NPjdb40vNUl8zj2L .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-NPjdb40vNUl8zj2L .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NPjdb40vNUl8zj2L .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-NPjdb40vNUl8zj2L .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NPjdb40vNUl8zj2L .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-NPjdb40vNUl8zj2L .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-NPjdb40vNUl8zj2L .cluster text{fill:#333;}#mermaid-svg-NPjdb40vNUl8zj2L .cluster span{color:#333;}#mermaid-svg-NPjdb40vNUl8zj2L 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-NPjdb40vNUl8zj2L .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-NPjdb40vNUl8zj2L rect.text{fill:none;stroke-width:0;}#mermaid-svg-NPjdb40vNUl8zj2L .icon-shape,#mermaid-svg-NPjdb40vNUl8zj2L .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NPjdb40vNUl8zj2L .icon-shape p,#mermaid-svg-NPjdb40vNUl8zj2L .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-NPjdb40vNUl8zj2L .icon-shape .label rect,#mermaid-svg-NPjdb40vNUl8zj2L .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NPjdb40vNUl8zj2L .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-NPjdb40vNUl8zj2L .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-NPjdb40vNUl8zj2L :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-NPjdb40vNUl8zj2L .primary>*{fill:#4F46E5!important;stroke:#3730A3!important;color:#fff!important;}#mermaid-svg-NPjdb40vNUl8zj2L .primary span{fill:#4F46E5!important;stroke:#3730A3!important;color:#fff!important;}#mermaid-svg-NPjdb40vNUl8zj2L .primary tspan{fill:#fff!important;}#mermaid-svg-NPjdb40vNUl8zj2L .warning>*{fill:#F59E0B!important;stroke:#D97706!important;color:#fff!important;}#mermaid-svg-NPjdb40vNUl8zj2L .warning span{fill:#F59E0B!important;stroke:#D97706!important;color:#fff!important;}#mermaid-svg-NPjdb40vNUl8zj2L .warning tspan{fill:#fff!important;}#mermaid-svg-NPjdb40vNUl8zj2L .error>*{fill:#EF4444!important;stroke:#DC2626!important;color:#fff!important;}#mermaid-svg-NPjdb40vNUl8zj2L .error span{fill:#EF4444!important;stroke:#DC2626!important;color:#fff!important;}#mermaid-svg-NPjdb40vNUl8zj2L .error tspan{fill:#fff!important;}#mermaid-svg-NPjdb40vNUl8zj2L .info>*{fill:#06B6D4!important;stroke:#0891B2!important;color:#fff!important;}#mermaid-svg-NPjdb40vNUl8zj2L .info span{fill:#06B6D4!important;stroke:#0891B2!important;color:#fff!important;}#mermaid-svg-NPjdb40vNUl8zj2L .info tspan{fill:#fff!important;}#mermaid-svg-NPjdb40vNUl8zj2L .purple>*{fill:#8B5CF6!important;stroke:#7C3AED!important;color:#fff!important;}#mermaid-svg-NPjdb40vNUl8zj2L .purple span{fill:#8B5CF6!important;stroke:#7C3AED!important;color:#fff!important;}#mermaid-svg-NPjdb40vNUl8zj2L .purple tspan{fill:#fff!important;} 知识库回答
多轮追问
转人工
功能执行
用户消息
消息预处理
自然语言理解 NLU
意图识别
实体提取
情感分析
对话状态追踪 DST
对话策略决策 DP
决策类型
知识库检索
槽位填充
人工客服转接
技能调用
回复生成
转接通知
回复用户
对话记录存储
知识库更新

2.2 对话管理核心概念

对话管理是智能客服的大脑,包含三个核心模块:

对话状态追踪(DST):维护当前对话的完整状态,包括已收集的槽位、用户意图历史、对话轮次等。

对话策略(DP):根据当前状态决定下一步动作------是直接回答、追问缺失信息、还是转人工。

回复生成(NLG):将决策转化为自然语言回复,支持模板填充和动态生成。

2.3 知识库检索原理

知识库采用"问题-答案对"的结构化存储,检索时使用语义相似度匹配:

  1. 将用户问题转为向量表示
  2. 在知识库中找到最相似的问题
  3. 返回对应的答案

在端侧实现时,可以使用TF-IDF或轻量级BERT模型进行语义匹配。


三、代码实战

3.1 对话状态管理与意图识别

typescript 复制代码
// DialogManager.ets - 对话管理引擎

// 对话意图枚举
export enum DialogIntent {
  PRODUCT_INQUIRY = 'product_inquiry',     // 产品咨询
  ORDER_QUERY = 'order_query',             // 订单查询
  REFUND_REQUEST = 'refund_request',       // 退款申请
  COMPLAINT = 'complaint',                 // 投诉建议
  TECH_SUPPORT = 'tech_support',           // 技术支持
  GENERAL_CHAT = 'general_chat',           // 闲聊
  TRANSFER_HUMAN = 'transfer_human',       // 转人工
  UNKNOWN = 'unknown'                      // 未识别
}

// 槽位定义
export interface SlotDefinition {
  name: string;           // 槽位名称
  required: boolean;      // 是否必填
  prompt: string;         // 追问话术
  type: 'string' | 'number' | 'date'; // 值类型
}

// 对话状态
export interface DialogState {
  intent: DialogIntent;               // 当前意图
  slots: Record<string, string>;      // 已填充的槽位
  history: DialogTurn[];              // 对话历史
  turnCount: number;                  // 当前轮次
  isComplete: boolean;                // 是否完成
  needsHumanTransfer: boolean;        // 是否需要转人工
  sentiment: 'positive' | 'neutral' | 'negative'; // 情感状态
}

// 对话轮次
export interface DialogTurn {
  role: 'user' | 'bot';
  content: string;
  timestamp: number;
  intent?: DialogIntent;
}

// 意图对应的槽位定义
const INTENT_SLOTS: Map<DialogIntent, SlotDefinition[]> = new Map([
  [DialogIntent.ORDER_QUERY, [
    { name: 'order_id', required: true, prompt: '请提供您的订单号', type: 'string' },
    { name: 'query_type', required: false, prompt: '您想查询订单的哪方面信息?', type: 'string' }
  ]],
  [DialogIntent.REFUND_REQUEST, [
    { name: 'order_id', required: true, prompt: '请提供需要退款的订单号', type: 'string' },
    { name: 'reason', required: true, prompt: '请说明退款原因', type: 'string' },
    { name: 'amount', required: false, prompt: '您希望退款多少金额?', type: 'number' }
  ]],
  [DialogIntent.TECH_SUPPORT, [
    { name: 'product', required: true, prompt: '请告诉我您使用的是哪款产品?', type: 'string' },
    { name: 'issue', required: true, prompt: '请描述您遇到的问题', type: 'string' },
    { name: 'error_code', required: false, prompt: '如果有错误代码,请提供', type: 'string' }
  ]],
  [DialogIntent.PRODUCT_INQUIRY, [
    { name: 'product_name', required: true, prompt: '您想了解哪款产品?', type: 'string' },
    { name: 'aspect', required: false, prompt: '您想了解产品的哪方面?', type: 'string' }
  ]],
  [DialogIntent.COMPLAINT, [
    { name: 'issue', required: true, prompt: '请描述您遇到的问题', type: 'string' },
    { name: 'contact', required: false, prompt: '请留下您的联系方式,方便我们跟进', type: 'string' }
  ]]
]);

export class DialogManager {
  private state: DialogState;
  private knowledgeBase: KnowledgeBase;
  
  constructor() {
    this.state = this.createInitialState();
    this.knowledgeBase = new KnowledgeBase();
  }
  
  // 创建初始对话状态
  private createInitialState(): DialogState {
    return {
      intent: DialogIntent.UNKNOWN,
      slots: {},
      history: [],
      turnCount: 0,
      isComplete: false,
      needsHumanTransfer: false,
      sentiment: 'neutral'
    };
  }
  
  // 处理用户消息
  async processMessage(userMessage: string): Promise<string> {
    // 1. 记录用户消息
    this.state.history.push({
      role: 'user',
      content: userMessage,
      timestamp: Date.now()
    });
    this.state.turnCount++;
    
    // 2. 情感分析
    this.state.sentiment = this.analyzeSentiment(userMessage);
    
    // 3. 检查是否需要转人工
    if (this.shouldTransferToHuman(userMessage)) {
      this.state.needsHumanTransfer = true;
      const response = '我理解您的情况,正在为您转接人工客服,请稍候...';
      this.state.history.push({
        role: 'bot',
        content: response,
        timestamp: Date.now()
      });
      return response;
    }
    
    // 4. 意图识别(首轮或意图变更时)
    if (this.state.intent === DialogIntent.UNKNOWN || this.turnCount <= 1) {
      this.state.intent = this.recognizeIntent(userMessage);
    }
    
    // 5. 槽位填充
    this.extractSlots(userMessage);
    
    // 6. 检查槽位完整性
    const missingSlot = this.getMissingSlot();
    if (missingSlot) {
      const response = missingSlot.prompt;
      this.state.history.push({
        role: 'bot',
        content: response,
        timestamp: Date.now()
      });
      return response;
    }
    
    // 7. 生成回复
    const response = await this.generateResponse();
    
    // 8. 记录回复
    this.state.history.push({
      role: 'bot',
      content: response,
      timestamp: Date.now(),
      intent: this.state.intent
    });
    
    return response;
  }
  
  // 意图识别
  private recognizeIntent(text: string): DialogIntent {
    const intentKeywords: Map<DialogIntent, string[]> = new Map([
      [DialogIntent.PRODUCT_INQUIRY, ['产品', '价格', '功能', '介绍', '多少钱', '有什么', '推荐']],
      [DialogIntent.ORDER_QUERY, ['订单', '物流', '快递', '到哪了', '发货', '配送']],
      [DialogIntent.REFUND_REQUEST, ['退款', '退货', '退钱', '不想要', '质量差', '发错']],
      [DialogIntent.COMPLAINT, ['投诉', '不满意', '差评', '举报', '欺骗', '虚假']],
      [DialogIntent.TECH_SUPPORT, ['故障', '报错', '不能用', '打不开', '崩溃', '卡顿', '黑屏']],
      [DialogIntent.TRANSFER_HUMAN, ['人工', '客服', '真人', '转接', '不要机器人']],
      [DialogIntent.GENERAL_CHAT, ['你好', '在吗', '你是谁', '聊天']]
    ]);
    
    let bestIntent = DialogIntent.UNKNOWN;
    let bestScore = 0;
    
    for (const [intent, keywords] of intentKeywords) {
      let score = 0;
      for (const keyword of keywords) {
        if (text.includes(keyword)) {
          score += keyword.length;
        }
      }
      if (score > bestScore) {
        bestScore = score;
        bestIntent = intent;
      }
    }
    
    return bestIntent;
  }
  
  // 槽位提取
  private extractSlots(text: string): void {
    // 订单号提取(格式:DD开头+8位数字)
    const orderMatch = text.match(/DD\d{8,}/i);
    if (orderMatch) {
      this.state.slots['order_id'] = orderMatch[0];
    }
    
    // 金额提取
    const amountMatch = text.match(/(\d+(?:\.\d+)?)\s*元/);
    if (amountMatch) {
      this.state.slots['amount'] = amountMatch[1];
    }
    
    // 产品名提取(简化版)
    const products = ['MatePad', 'MateBook', 'FreeBuds', 'Watch GT', 'Mate 60', 'P60'];
    for (const product of products) {
      if (text.toLowerCase().includes(product.toLowerCase())) {
        this.state.slots['product'] = product;
        this.state.slots['product_name'] = product;
        break;
      }
    }
    
    // 错误码提取
    const errorMatch = text.match(/错误[代码号]?\s*[::]?\s*([A-Z0-9-]+)/i);
    if (errorMatch) {
      this.state.slots['error_code'] = errorMatch[1];
    }
    
    // 通用提取:将整段文本作为issue/reason槽位
    if (this.state.intent === DialogIntent.TECH_SUPPORT && !this.state.slots['issue']) {
      this.state.slots['issue'] = text;
    }
    if (this.state.intent === DialogIntent.REFUND_REQUEST && !this.state.slots['reason']) {
      this.state.slots['reason'] = text;
    }
  }
  
  // 获取缺失的必填槽位
  private getMissingSlot(): SlotDefinition | null {
    const slotDefs = INTENT_SLOTS.get(this.state.intent);
    if (!slotDefs) return null;
    
    for (const slotDef of slotDefs) {
      if (slotDef.required && !this.state.slots[slotDef.name]) {
        return slotDef;
      }
    }
    return null;
  }
  
  // 情感分析(简化版)
  private analyzeSentiment(text: string): 'positive' | 'neutral' | 'negative' {
    const negativeWords = ['差', '烂', '垃圾', '骗', '投诉', '生气', '不满', '失望', '愤怒', '坑'];
    const positiveWords = ['好', '棒', '满意', '感谢', '不错', '喜欢', '赞'];
    
    let negCount = 0;
    let posCount = 0;
    
    for (const word of negativeWords) {
      if (text.includes(word)) negCount++;
    }
    for (const word of positiveWords) {
      if (text.includes(word)) posCount++;
    }
    
    if (negCount > posCount + 1) return 'negative';
    if (posCount > negCount + 1) return 'positive';
    return 'neutral';
  }
  
  // 判断是否需要转人工
  private shouldTransferToHuman(text: string): boolean {
    // 显式请求转人工
    if (this.state.intent === DialogIntent.TRANSFER_HUMAN || text.includes('转人工')) {
      return true;
    }
    
    // 负面情绪 + 多轮未解决
    if (this.state.sentiment === 'negative' && this.state.turnCount > 3) {
      return true;
    }
    
    // 超过8轮仍未完成
    if (this.state.turnCount > 8) {
      return true;
    }
    
    return false;
  }
  
  // 生成回复
  private async generateResponse(): Promise<string> {
    switch (this.state.intent) {
      case DialogIntent.ORDER_QUERY:
        return this.handleOrderQuery();
      case DialogIntent.REFUND_REQUEST:
        return this.handleRefundRequest();
      case DialogIntent.PRODUCT_INQUIRY:
        return this.handleProductInquiry();
      case DialogIntent.TECH_SUPPORT:
        return this.handleTechSupport();
      case DialogIntent.COMPLAINT:
        return this.handleComplaint();
      case DialogIntent.GENERAL_CHAT:
        return this.handleGeneralChat();
      default:
        return '抱歉,我没有理解您的意思。您可以描述一下您的具体需求吗?比如查询订单、产品咨询、退款申请等。';
    }
  }
  
  // 处理订单查询
  private handleOrderQuery(): string {
    const orderId = this.state.slots['order_id'];
    if (!orderId) {
      return '请提供您的订单号,我来帮您查询。';
    }
    
    // 模拟查询结果
    return `订单 ${orderId} 的信息如下:\n📦 状态:已发货,运输中\n🚚 快递:顺丰速运 SF1234567890\n📅 预计送达:明天下午\n\n如需了解更多信息,请告诉我。`;
  }
  
  // 处理退款申请
  private handleRefundRequest(): string {
    const orderId = this.state.slots['order_id'];
    const reason = this.state.slots['reason'];
    
    return `已收到您的退款申请:\n📋 订单号:${orderId}\n📝 退款原因:${reason}\n\n我们将在1-3个工作日内审核您的申请,审核通过后退款将原路返回。请注意查收。`;
  }
  
  // 处理产品咨询
  private handleProductInquiry(): string {
    const productName = this.state.slots['product_name'];
    
    // 查询知识库
    const answer = this.knowledgeBase.search(productName || '');
    if (answer) {
      return answer;
    }
    
    return `${productName || '该产品'}是我们非常受欢迎的产品。主要特点包括:\n✨ 高性能处理器\n📷 超清摄像头\n🔋 长续航\n\n您还想了解哪方面的详细信息?`;
  }
  
  // 处理技术支持
  private handleTechSupport(): string {
    const product = this.state.slots['product'];
    const issue = this.state.slots['issue'];
    const errorCode = this.state.slots['error_code'];
    
    // 查询知识库
    const searchKey = `${product} ${errorCode || ''} ${issue || ''}`;
    const answer = this.knowledgeBase.search(searchKey);
    if (answer) {
      return answer;
    }
    
    return `关于您反馈的${product ? product + ' ' : ''}问题:\n"${issue}"\n\n建议您尝试以下步骤:\n1️⃣ 重启设备\n2️⃣ 更新到最新系统版本\n3️⃣ 清除应用缓存\n\n如果问题仍然存在,我可以为您转接技术支持工程师。`;
  }
  
  // 处理投诉
  private handleComplaint(): string {
    const issue = this.state.slots['issue'];
    
    return `非常抱歉给您带来了不好的体验!🙏\n\n您反馈的问题"${issue}"我们已经记录,会在24小时内由专人联系您处理。\n\n您的满意是我们最大的追求,我们会认真对待每一个反馈。`;
  }
  
  // 处理闲聊
  private handleGeneralChat(): string {
    const greetings = [
      '您好!我是智能客服小华,很高兴为您服务!😊',
      '你好呀!有什么我可以帮您的吗?',
      '嗨!欢迎咨询,我随时为您解答问题~'
    ];
    
    return greetings[Math.floor(Math.random() * greetings.length)];
  }
  
  // 获取当前对话状态
  getState(): DialogState {
    return { ...this.state };
  }
  
  // 获取对话历史
  getHistory(): DialogTurn[] {
    return [...this.state.history];
  }
  
  // 重置对话
  reset(): void {
    this.state = this.createInitialState();
  }
  
  // 获取当前轮次
  get turnCount(): number {
    return this.state.turnCount;
  }
}

3.2 知识库模块

typescript 复制代码
// KnowledgeBase.ets - 知识库检索模块

// 知识条目
interface KnowledgeEntry {
  id: string;
  question: string;       // 标准问题
  keywords: string[];     // 关键词
  answer: string;         // 标准答案
  category: string;       // 分类
  priority: number;       // 优先级(1-10)
}

export class KnowledgeBase {
  private entries: KnowledgeEntry[] = [];
  
  constructor() {
    this.loadDefaultKnowledge();
  }
  
  // 加载默认知识库
  private loadDefaultKnowledge(): void {
    this.entries = [
      {
        id: 'kb_001',
        question: 'MatePad Pro的价格是多少',
        keywords: ['MatePad', 'Pro', '价格', '多少钱'],
        answer: 'MatePad Pro 目标售价:\n• 8+256GB:3299元\n• 12+256GB:3999元\n• 12+512GB:4499元\n\n活动期间可能有优惠,建议关注官方商城。',
        category: '产品价格',
        priority: 8
      },
      {
        id: 'kb_002',
        question: '如何申请退货退款',
        keywords: ['退货', '退款', '申请', '流程'],
        answer: '退货退款流程:\n1. 进入"我的订单"找到对应订单\n2. 点击"申请退款/退货"\n3. 选择退款原因并提交\n4. 等待审核(1-3个工作日)\n5. 审核通过后退款原路返回\n\n⚠️ 注意:商品需保持原包装完好,7天内可无理由退货。',
        category: '售后服务',
        priority: 9
      },
      {
        id: 'kb_003',
        question: 'FreeBuds Pro无法连接怎么办',
        keywords: ['FreeBuds', '连接', '蓝牙', '无法', '配对'],
        answer: 'FreeBuds Pro连接问题解决方案:\n1. 将耳机放回充电盒,关盖5秒后重新打开\n2. 手机蓝牙关闭后重新开启\n3. 长按充电盒按钮10秒恢复出厂设置\n4. 重新搜索并配对\n\n如果以上步骤无法解决,可能是硬件问题,建议联系售后。',
        category: '技术支持',
        priority: 7
      },
      {
        id: 'kb_004',
        question: '保修政策是什么',
        keywords: ['保修', '质保', '维修', '政策'],
        answer: '保修政策:\n• 手机/平板:整机1年,电池6个月\n• 耳机/手表:整机1年\n• 配件:3个月\n\n保修期内非人为损坏免费维修。需提供购买凭证。\n\n📍 维修点查询:设置 → 服务与支持 → 服务中心',
        category: '售后服务',
        priority: 8
      },
      {
        id: 'kb_005',
        question: '如何查询物流信息',
        keywords: ['物流', '快递', '查询', '到哪了', '配送'],
        answer: '物流查询方式:\n1. 官方商城 → 我的订单 → 查看物流\n2. 短信中的快递单号到快递公司官网查询\n3. 拨打客服热线查询\n\n📦 发货后一般2-5天送达(偏远地区可能延迟)。',
        category: '订单服务',
        priority: 7
      },
      {
        id: 'kb_006',
        question: 'Watch GT续航多久',
        keywords: ['Watch', 'GT', '续航', '电池', '待机'],
        answer: 'Watch GT 续航表现:\n• 典型使用:14天\n• 重度使用:8天\n• GPS连续运动:40小时\n\n💡 省电技巧:\n• 降低屏幕亮度\n• 关闭常亮显示\n• 减少心率监测频率',
        category: '产品咨询',
        priority: 6
      }
    ];
  }
  
  // 搜索知识库
  search(query: string): string | null {
    if (!query.trim()) return null;
    
    const queryLower = query.toLowerCase();
    let bestMatch: KnowledgeEntry | null = null;
    let bestScore = 0;
    
    for (const entry of this.entries) {
      let score = 0;
      
      // 关键词匹配
      for (const keyword of entry.keywords) {
        if (queryLower.includes(keyword.toLowerCase())) {
          score += keyword.length * 2;
        }
      }
      
      // 问题文本匹配
      for (const word of queryLower.split(/\s+/)) {
        if (word.length >= 2 && entry.question.toLowerCase().includes(word)) {
          score += word.length;
        }
      }
      
      // 优先级加权
      score += entry.priority * 0.5;
      
      if (score > bestScore && score >= 3) {
        bestScore = score;
        bestMatch = entry;
      }
    }
    
    return bestMatch?.answer || null;
  }
  
  // 添加知识条目
  addEntry(entry: KnowledgeEntry): void {
    this.entries.push(entry);
  }
  
  // 按分类获取
  getByCategory(category: string): KnowledgeEntry[] {
    return this.entries.filter(e => e.category === category);
  }
  
  // 获取所有分类
  getCategories(): string[] {
    return [...new Set(this.entries.map(e => e.category))];
  }
}

3.3 完整客服聊天界面

typescript 复制代码
// CustomerServicePage.ets - 智能客服聊天界面
import { promptAction } from '@kit.ArkUI';

@Entry
@Component
struct CustomerServicePage {
  @State messages: ChatMessage[] = [];
  @State inputText: string = '';
  @State isTyping: boolean = false;
  @State showQuickActions: boolean = true;
  @State isHumanMode: boolean = false;
  @State satisfactionRating: number = 0;
  
  private dialogManager: DialogManager = new DialogManager();
  private scroller: Scroller = new Scroller();
  
  aboutToAppear() {
    // 欢迎消息
    this.messages.push({
      role: 'bot',
      content: '您好!我是智能客服小华 🤖\n\n我可以帮您:\n📦 查询订单状态\n💰 办理退款退货\n📱 产品咨询与推荐\n🔧 技术问题支持\n\n请告诉我您需要什么帮助?',
      timestamp: Date.now(),
      type: 'text'
    });
  }
  
  build() {
    Column() {
      // 顶部状态栏
      this.HeaderBar();
      
      // 聊天消息区
      this.MessageArea();
      
      // 快捷操作
      if (this.showQuickActions && this.messages.length <= 2) {
        this.QuickActions();
      }
      
      // 底部输入区
      this.InputArea();
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#0A0A1A')
  }
  
  @Builder HeaderBar() {
    Row() {
      Column() {
        Text(this.isHumanMode ? '👤 人工客服' : '🤖 智能客服小华')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
        Text(this.isHumanMode ? '客服正在为您服务' : '在线 · 平均响应 < 1秒')
          .fontSize(11)
          .fontColor('#9CA3AF')
          .margin({ top: 2 })
      }
      .alignItems(HorizontalAlign.Start)
      
      Blank()
      
      // 转人工按钮
      if (!this.isHumanMode) {
        Button('转人工')
          .height(30)
          .fontSize(12)
          .fontColor('#E0E7FF')
          .backgroundColor('#312E81')
          .borderRadius(15)
          .onClick(() => this.transferToHuman())
      }
    }
    .width('100%')
    .padding({ left: 16, right: 16, top: 12, bottom: 8 })
    .backgroundColor('#0F0F2A')
  }
  
  @Builder MessageArea() {
    List({ space: 12, scroller: this.scroller }) {
      ForEach(this.messages, (msg: ChatMessage, index: number) => {
        ListItem() {
          this.MessageBubble(msg, index);
        }
      })
      
      // 正在输入提示
      if (this.isTyping) {
        ListItem() {
          Row() {
            Text('小华正在输入')
              .fontSize(13)
              .fontColor('#9CA3AF')
            Text('...')
              .fontSize(13)
              .fontColor('#9CA3AF')
          }
          .padding({ left: 16 })
        }
      }
    }
    .width('100%')
    .layoutWeight(1)
    .padding({ left: 12, right: 12, top: 8 })
  }
  
  @Builder MessageBubble(msg: ChatMessage, index: number) {
    if (msg.role === 'bot') {
      // 机器人消息 - 左对齐
      Row() {
        Column() {
          // 头像
          Text('🤖')
            .fontSize(24)
            .margin({ bottom: 4 })
          
          // 消息内容
          Column() {
            Text(msg.content)
              .fontSize(14)
              .fontColor('#E0E7FF')
              .lineHeight(22)
          }
          .padding(12)
          .backgroundColor('#1E1B4B')
          .borderRadius({ topLeft: 4, topRight: 16, bottomLeft: 16, bottomRight: 16 })
          .constraintSize({ maxWidth: '80%' })
          
          // 时间
          Text(this.formatTime(msg.timestamp))
            .fontSize(10)
            .fontColor('#6B7280')
            .margin({ top: 4 })
        }
        .alignItems(HorizontalAlign.Start)
      }
      .width('100%')
      .justifyContent(FlexAlign.Start)
    } else {
      // 用户消息 - 右对齐
      Row() {
        Column() {
          Column() {
            Text(msg.content)
              .fontSize(14)
              .fontColor('#FFFFFF')
              .lineHeight(22)
          }
          .padding(12)
          .backgroundColor('#4F46E5')
          .borderRadius({ topLeft: 16, topRight: 4, bottomLeft: 16, bottomRight: 16 })
          .constraintSize({ maxWidth: '80%' })
          
          Text(this.formatTime(msg.timestamp))
            .fontSize(10)
            .fontColor('#6B7280')
            .margin({ top: 4 })
        }
        .alignItems(HorizontalAlign.End)
      }
      .width('100%')
      .justifyContent(FlexAlign.End)
    }
  }
  
  @Builder QuickActions() {
    Column() {
      Text('快捷操作')
        .fontSize(13)
        .fontColor('#9CA3AF')
        .width('100%')
        .padding({ left: 16, bottom: 8 })
      
      Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Start }) {
        ForEach([
          { icon: '📦', label: '查询订单', message: '我想查询我的订单' },
          { icon: '💰', label: '申请退款', message: '我要申请退款' },
          { icon: '📱', label: '产品咨询', message: '我想了解产品信息' },
          { icon: '🔧', label: '技术支持', message: '我遇到了技术问题' },
          { icon: '📝', label: '投诉建议', message: '我要投诉' },
          { icon: '👤', label: '转人工', message: '请帮我转接人工客服' }
        ], (action: { icon: string; label: string; message: string }) => {
          Row() {
            Text(action.icon)
              .fontSize(16)
            Text(action.label)
              .fontSize(13)
              .fontColor('#E0E7FF')
              .margin({ left: 4 })
          }
          .padding({ left: 12, right: 12, top: 8, bottom: 8 })
          .backgroundColor('#1E1B4B')
          .borderRadius(20)
          .margin({ right: 8, bottom: 8 })
          .onClick(() => this.sendMessage(action.message))
        })
      }
      .width('100%')
      .padding({ left: 16, right: 16 })
    }
    .width('100%')
    .padding({ top: 8, bottom: 8 })
  }
  
  @Builder InputArea() {
    Row() {
      TextInput({ text: this.inputText, placeholder: '输入您的问题...' })
        .layoutWeight(1)
        .height(44)
        .fontSize(15)
        .fontColor('#FFFFFF')
        .backgroundColor('#1A1A2E')
        .borderRadius(22)
        .placeholderColor('#6B7280')
        .padding({ left: 16, right: 16 })
        .onChange((value: string) => {
          this.inputText = value;
        })
        .onSubmit(() => this.sendMessage(this.inputText))
      
      // 发送按钮
      Column() {
        Text('➤')
          .fontSize(20)
          .fontColor('#FFFFFF')
      }
      .width(44)
      .height(44)
      .justifyContent(FlexAlign.Center)
      .backgroundColor(this.inputText.trim() ? '#4F46E5' : '#312E81')
      .borderRadius(22)
      .margin({ left: 8 })
      .onClick(() => this.sendMessage(this.inputText))
    }
    .width('100%')
    .padding({ left: 12, right: 12, top: 8, bottom: 16 })
    .backgroundColor('#0F0F2A')
  }
  
  // 发送消息
  private async sendMessage(text: string) {
    if (!text.trim()) return;
    
    // 隐藏快捷操作
    this.showQuickActions = false;
    
    // 添加用户消息
    this.messages.push({
      role: 'user',
      content: text.trim(),
      timestamp: Date.now(),
      type: 'text'
    });
    
    this.inputText = '';
    
    // 滚动到底部
    setTimeout(() => this.scroller.scrollEdge(Edge.Bottom), 100);
    
    // 显示正在输入
    this.isTyping = true;
    
    // 处理消息
    try {
      const response = await this.dialogManager.processMessage(text.trim());
      
      // 模拟打字延迟
      await this.simulateTypingDelay(response);
      
      // 添加机器人回复
      this.messages.push({
        role: 'bot',
        content: response,
        timestamp: Date.now(),
        type: 'text'
      });
      
      // 检查是否需要转人工
      const state = this.dialogManager.getState();
      if (state.needsHumanTransfer) {
        this.isHumanMode = true;
      }
      
    } catch (error) {
      this.messages.push({
        role: 'bot',
        content: '抱歉,处理您的消息时出现了问题,请稍后重试。',
        timestamp: Date.now(),
        type: 'text'
      });
    }
    
    this.isTyping = false;
    setTimeout(() => this.scroller.scrollEdge(Edge.Bottom), 100);
  }
  
  // 模拟打字延迟
  private simulateTypingDelay(text: string): Promise<void> {
    const delay = Math.min(text.length * 30, 2000); // 最多2秒
    return new Promise(resolve => setTimeout(resolve, delay));
  }
  
  // 转人工
  private transferToHuman() {
    this.isHumanMode = true;
    this.messages.push({
      role: 'bot',
      content: '正在为您转接人工客服,请稍候...\n\n⏳ 预计等待时间:2分钟',
      timestamp: Date.now(),
      type: 'text'
    });
    
    // 模拟人工客服接入
    setTimeout(() => {
      this.messages.push({
        role: 'bot',
        content: '您好,我是客服专员小王,很高兴为您服务!请问有什么可以帮您的?',
        timestamp: Date.now(),
        type: 'text'
      });
      setTimeout(() => this.scroller.scrollEdge(Edge.Bottom), 100);
    }, 3000);
  }
  
  // 格式化时间
  private formatTime(timestamp: number): string {
    const date = new Date(timestamp);
    return `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
  }
  
  aboutToDisappear() {
    this.dialogManager.reset();
  }
}

// 聊天消息数据结构
interface ChatMessage {
  role: 'user' | 'bot';
  content: string;
  timestamp: number;
  type: 'text' | 'image' | 'card';
}

四、踩坑与注意事项

4.1 对话上下文丢失

多轮对话最大的挑战是上下文管理。用户说"那个订单呢",机器人必须知道"那个"指代什么。解决方案是在DST中维护完整的对话状态:

typescript 复制代码
// 错误做法:每轮独立处理
const response = nlu.process(currentMessage); // 丢失上下文

// 正确做法:传入完整对话状态
const response = dialogManager.processMessage(currentMessage); // 保持上下文

4.2 意图切换处理

用户可能在对话中途切换意图,比如正在查订单时突然问产品价格。对话管理器需要支持意图切换:

typescript 复制代码
// 检测意图切换
if (newIntent !== currentIntent && newIntent !== DialogIntent.UNKNOWN) {
  // 保存当前对话状态(以便回来)
  this.savedStates.push(this.state);
  // 开始新意图的对话
  this.state.intent = newIntent;
  this.state.slots = {};
}

4.3 知识库的冷启动问题

新部署的客服系统知识库往往是空的,需要从零建设。建议:

  1. 导入FAQ:将现有FAQ文档转为知识条目
  2. 对话日志挖掘:从历史对话中提取高频问题
  3. 渐进式补充:上线后根据用户实际提问持续补充

4.4 敏感信息处理

客服对话中可能包含订单号、手机号、身份证号等敏感信息。必须在存储和展示时做脱敏处理:

typescript 复制代码
// 脱敏处理
function desensitize(text: string): string {
  // 手机号脱敏
  text = text.replace(/1[3-9]\d{9}/g, match => 
    match.substring(0, 3) + '****' + match.substring(7)
  );
  // 身份证脱敏
  text = text.replace(/\d{17}[\dXx]/g, match => 
    match.substring(0, 6) + '********' + match.substring(14)
  );
  return text;
}

4.5 并发与性能

智能客服需要支持多用户同时在线。对话管理器应该是无状态的(或使用外部存储状态),避免多用户共享状态导致混乱:

typescript 复制代码
// 错误:全局共享一个DialogManager
const globalDialogManager = new DialogManager(); // 所有用户共享

// 正确:每个用户会话独立
const sessions: Map<string, DialogManager> = new Map();
function getOrCreateSession(userId: string): DialogManager {
  if (!sessions.has(userId)) {
    sessions.set(userId, new DialogManager());
  }
  return sessions.get(userId)!;
}

五、HarmonyOS 6适配

5.1 API变更

变更项 HarmonyOS 5 HarmonyOS 6
NLU能力 基础关键词匹配 新增naturalLanguageUnderstanding API
情感分析 无原生支持 新增sentimentAnalysis API
对话管理 需自实现 新增dialogManagement框架
知识库 无原生支持 新增knowledgeBase检索API

5.2 原生NLU集成

HarmonyOS 6提供了原生NLU API,可以替代自实现的关键词匹配方案:

typescript 复制代码
// HarmonyOS 6 新增
import { naturalLanguageUnderstanding } from '@kit.AIServiceKit';

const nluEngine = naturalLanguageUnderstanding.createEngine({
  modelType: naturalLanguageUnderstanding.ModelType.LOCAL
});

const result = await nluEngine.analyze('我想查一下订单DD20240101的物流信息');
// result.intent = 'order_query'
// result.entities = [{ type: 'order_id', value: 'DD20240101' }]
// result.sentiment = 'neutral'

5.3 原生情感分析

typescript 复制代码
// HarmonyOS 6 新增
import { sentimentAnalysis } from '@kit.AIServiceKit';

const sentimentEngine = sentimentAnalysis.createEngine();
const sentiment = await sentimentEngine.analyze('这个产品太差了,我要投诉!');
// sentiment.label = 'negative'
// sentiment.score = 0.92

5.4 迁移要点

  1. 替换关键词匹配:使用原生NLU API替代自实现的意图识别
  2. 接入情感分析:利用原生API实现更精准的情感检测
  3. 知识库API:使用原生知识库检索API替代自实现的搜索

六、总结

本文从零构建了一个完整的HarmonyOS智能客服系统,核心知识点如下:

复制代码
智能客服系统
├── 对话管理
│   ├── 对话状态追踪(DST)
│   ├── 意图识别与路由
│   ├── 槽位填充与追问
│   ├── 对话策略决策
│   └── 情感分析与转人工
├── 知识库
│   ├── 结构化知识条目
│   ├── 关键词匹配检索
│   ├── 优先级加权排序
│   └── 分类管理
├── 多轮对话
│   ├── 上下文状态维护
│   ├── 意图切换处理
│   ├── 必填槽位追问
│   └── 对话历史管理
├── UI交互
│   ├── 聊天气泡界面
│   ├── 快捷操作面板
│   ├── 打字延迟模拟
│   └── 人工转接流程
└── 工程化
    ├── 敏感信息脱敏
    ├── 会话隔离管理
    ├── 知识库冷启动
    └── HarmonyOS 6原生NLU

关键收获

  • 对话状态追踪是多轮对话的基石,没有上下文就没有智能
  • 槽位填充+追问是处理复杂意图的标准范式
  • 情感分析+自动转人工是提升用户体验的关键
  • 知识库是客服系统的灵魂,需要持续建设和维护
  • HarmonyOS 6的原生NLU和情感分析API将大幅简化开发

智能客服是AI最成熟的商业落地场景之一。从关键词匹配到深度理解,从单轮问答到多轮对话,从纯机器人到人机协同------智能客服的进化之路,也是AI技术落地的缩影。HarmonyOS的端侧AI能力让这一切更安全、更快速、更可控。