最近在帮公司重构客服系统,原来的系统一到高峰期就卡得不行,用户排队等回复,客服同学也忙得焦头烂额。正好研究了一下Coze平台,用它来搭建智能体客服工作流,效果还挺惊喜的。今天就把整个架构设计和优化过程记录下来,希望能给有类似需求的朋友一些参考。

图片

1. 我们遇到了哪些头疼的问题?

在动手之前,我们仔细盘点了老系统的几个核心痛点,这也是很多传统客服系统共有的问题:

  1. 高峰期响应延迟严重:一到促销季或者工作日午休时间,用户咨询量激增,系统QPS(每秒查询率)根本扛不住,平均响应时间从平时的2秒飙升到10秒以上,用户体验直线下降。
  2. 多轮对话状态维护困难:用户咨询一个复杂问题,比如“我想退换上周买的那个蓝色的衬衫,但是发票找不到了,怎么办?”。这个对话里包含了时间(上周)、商品(蓝色衬衫)、意图(退换货)、子问题(发票丢失)。老系统用简单的Session来存上下文,经常出现状态丢失或者不同用户对话串了的情况,非常尴尬。
  3. 意图识别准确率低:用户说的话千奇百怪,“这个怎么用不了?”、“东西坏了”、“出故障了”,其实可能都是“产品故障报修”这个意图。老系统基于关键词匹配,准确率只有70%左右,导致大量问题需要转人工,或者答非所问。
  4. 人力成本高企不下:因为上述的自动化程度低,简单重复的问题(如查订单、查物流)也需要大量人工客服介入,团队规模下不来,管理成本也高。

2. 为什么最终选择了Coze平台?

在技术选型阶段,我们重点对比了Coze、Rasa(开源)和Dialogflow(Google)。

  1. 自然语言理解(NLU)能力:

    • Rasa:非常灵活,NLU模型可以自己从头训练,但这对算法团队的要求很高,需要准备大量的标注数据,训练和调优周期长。
    • Dialogflow:背靠Google,预训练模型强大,对中文的支持也不错,但定制化能力相对较弱,一些特殊的业务意图识别不够精准。
    • Coze:它提供了一个不错的平衡点。平台内置了效果不错的通用NLU模型,同时支持我们上传自己的业务语料进行微调。对于我们这种有明确业务场景但又不具备强大NLP团队的团队来说,上手快,效果也有保障。
  2. 扩展性与集成能力:

    • Rasa:扩展性最强,可以写任何Python代码与后端系统集成,但所有东西都需要自己搭建和维护,包括对话管理、API服务等,基础设施成本高。
    • Dialogflow:主要通过Webhook与外部系统通信,集成还算方便,但深度定制业务逻辑时,有时会觉得“隔了一层”,不够直接。
    • Coze:它的“工作流”和“插件”设计深得我心。工作流可以用可视化的方式编排复杂的对话逻辑,而插件则可以非常方便地封装对内部CRM、订单系统的调用。它把复杂的对话系统抽象成了配置和简单的代码,开发效率极高。
  3. 综合成本考量:

    • Rasa:免费,但人力成本(开发、运维、算法)极高。
    • Dialogflow:按调用次数收费,量大了之后是一笔不小的开支。
    • Coze:目前有比较慷慨的免费额度,对于中小型业务场景初期来说,成本压力很小。其高开发效率也间接降低了人力成本。

最终选择Coze的核心优势:快速落地。它极大地降低了构建一个可用、好用的智能客服的门槛。我们不需要成为NLP专家或分布式系统专家,就能在几周内搭建出一个效果远超旧系统的原型。

3. 核心实现细节拆解

确定了平台,接下来就是具体的设计和实现了。

  1. 对话状态机设计(状态模式) 多轮对话的核心是状态管理。我们采用了经典的状态模式来设计对话状态机。每个对话节点(比如“问候”、“询问订单号”、“处理退货原因”、“结束”)都是一个状态。Coze的工作流节点完美对应了这些状态。

    • 状态(State):对应Coze工作流中的一个技能或插件节点。
    • 事件(Event):用户的输入或系统触发的事件(如超时),经过NLU解析后,转化为具体的“意图”和“槽位”。
    • 转换(Transition):根据当前状态和事件,决定下一个状态是什么。这个逻辑写在Coze工作流的条件分支里。

    这样设计的好处是逻辑清晰,增加新的业务场景(比如新增一个“投诉建议”流程)时,只需要增加新的状态和转换规则即可,不会影响原有流程。

  2. 意图识别模型优化 虽然Coze内置模型不错,但对于我们业务特有的术语(比如内部产品型号、活动名称)识别率还是不够。我们采用了微调方案:

    • 从历史客服聊天记录中,清洗和标注了大约5000条数据,覆盖了主要的十几个意图(咨询、下单、退货、投诉、查物流等)。
    • 利用Coze平台提供的模型微调功能,将这批数据上传进行训练。这里的关键是数据质量,标注的一致性非常重要。
    • 微调后,在我们的业务场景下,意图识别准确率从85%提升到了94%左右,效果显著。
  3. 会话上下文持久化 为了保证对话状态在用户短暂离开(比如页面刷新)后不丢失,并且支持分布式部署,我们不能把上下文只放在内存里。我们使用Redis来持久化会话上下文。

    • 每个用户会话有一个唯一的session_id。
    • 在Coze工作流的开始,通过一个“插件”从Redis中读取该session_id对应的上下文数据(包括当前对话状态、已填充的槽位信息等)。
    • 在每个对话节点处理后,再将更新后的上下文写回Redis。
    • 为Redis设置了合理的过期时间(如30分钟),避免无用数据堆积。

4. 代码与配置示例

光说不练假把式,下面贴一些核心的配置和代码片段。

Coze工作流配置核心片段(YAML格式示意): 这个片段展示了一个简单的订单查询流程,包含了状态跳转和回退(fallback)机制。

# 工作流定义 - 订单查询流程
name: order_inquiry_workflow
states:
  - name: greet
    type: skill
    content: “您好!请问有什么可以帮您?”
    transitions:
      - condition: intent == “query_order”
        target: ask_order_id
      - condition: default # Fallback 机制:当意图不匹配时,进入通用帮助
        target: general_help

  - name: ask_order_id
    type: skill
    content: “请您提供一下订单号,方便我为您查询。”
    # 这里会尝试从用户消息中提取“order_id”这个槽位
    slot_filling: order_id
    transitions:
      - condition: slots.order_id is filled
        target: process_order_query
      - condition: default
        target: clarify_order_id # 如果没提取到,进入澄清状态

  - name: process_order_query
    type: plugin
    # 调用外部插件,通过Webhook查询订单系统
    plugin_id: order_query_plugin
    transitions:
      - condition: plugin_result.success
        target: provide_order_info
      - condition: default
        target: query_failed

注释:这个配置定义了一个有三个状态的工作流。greet是问候并识别意图;ask_order_id是询问并填充订单号槽位;process_order_query是调用插件执行查询。transitions定义了状态间的跳转逻辑,default条件就是fallback路径。

与CRM系统对接的Webhook插件示例(Python): 当需要查询用户详细信息时,工作流会调用这个插件。

from flask import Flask, request, jsonify
import requests
import logging
from concurrent.futures import ThreadPoolExecutor
import json

app = Flask(__name__)
executor = ThreadPoolExecutor(max_workers=10) # 异步处理线程池
# 配置日志,记录到文件和控制台,便于排查问题
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

def async_log_to_es(session_id, action, data):
    """异步将日志记录到Elasticsearch的函数,避免阻塞主流程"""
    # 这里模拟日志记录,实际应调用ES的API
    log_entry = {"session_id": session_id, "action": action, "data": data, "timestamp": datetime.now().isoformat()}
    logger.info(f"Async Log: {json.dumps(log_entry)}")
    # 实际代码中,这里应该是 requests.post(ES_URL, json=log_entry)

@app.route('/webhook/query_user_info', methods=['POST'])
def query_user_info():
    """Webhook端点:根据用户ID从CRM查询信息"""
    data = request.json
    user_id = data.get('user_id')
    session_id = data.get('session_id', 'unknown')

    # 1. 异步记录请求日志(最佳实践:非核心逻辑异步化)
    executor.submit(async_log_to_es, session_id, 'query_user_info_request', {'user_id': user_id})

    if not user_id:
        # 2. 立即返回错误,避免无效调用下游系统
        executor.submit(async_log_to_es, session_id, 'query_user_info_error', {'error': 'missing user_id'})
        return jsonify({'success': False, 'error': 'Missing user_id'}), 400

    try:
        # 3. 调用内部CRM系统API(模拟)
        # 注意:生产环境应设置超时、重试和熔断机制
        crm_response = requests.get(f'https://internal-crm.com/api/users/{user_id}', timeout=5)
        crm_response.raise_for_status()
        user_data = crm_response.json()

        # 4. 异步记录成功日志
        executor.submit(async_log_to_es, session_id, 'query_user_info_success', {'user_id': user_id})
        
        # 5. 返回Coze插件期望的格式
        return jsonify({
            'success': True,
            'data': {
                'user_name': user_data.get('name'),
                'user_level': user_data.get('level'),
                # ... 其他需要的字段
            }
        })
    except requests.exceptions.RequestException as e:
        # 6. 异步记录失败日志
        executor.submit(async_log_to_es, session_id, 'query_user_info_failure', {'error': str(e)})
        logger.error(f"CRM query failed for user {user_id}: {e}")
        return jsonify({'success': False, 'error': 'CRM system unavailable'}), 503

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

注释:这个Webhook示例展示了几个关键点:1) 使用Flask框架提供API;2) 通过线程池实现异步日志记录,不阻塞主业务响应;3) 对输入参数进行校验;4) 调用下游服务时设置超时;5) 返回Coze插件规定的格式(包含success和data字段);6) 完善的错误处理和日志记录。

5. 性能优化实战

系统能用了,接下来就要让它好用、扛得住打。

  1. 负载测试与基准建立 我们使用Locust这个工具来模拟高并发用户场景,测试系统的瓶颈。

    • 编写Locust脚本:模拟用户从进入对话、发送不同意图消息、进行多轮交互的全过程。
    • 关键指标:关注平均响应时间、P95/P99响应时间、失败率以及在不同并发用户数下的QPS。
    • 找到瓶颈:初期测试发现,频繁读写Redis是瓶颈之一。我们通过优化数据结构(使用Hash存储会话上下文,而非整个JSON字符串)、以及使用Redis管道(pipeline)批量操作,显著提升了性能。
  2. 对话超时与重试机制

    • 超时:在Redis中为每个session_id设置TTL(生存时间)。如果用户超过一定时间(如10分钟)无交互,则会话过期,下次进入视为新会话。
    • 重试:对于调用外部插件(如CRM、订单系统)失败的情况,我们在插件代码中实现了简单的指数退避重试机制(最多重试2次),并记录失败原因,避免因临时网络抖动导致对话中断。
  3. 敏感词过滤 客服对话必须合规。我们实现了DFA(Deterministic Finite Automaton,确定有限状态自动机)算法进行敏感词过滤,它的优点是匹配效率高,一次扫描文本即可检测出所有敏感词。

    • 构建敏感词树:将敏感词库预处理成一棵多叉树。
    • 扫描过滤:遍历用户输入文本,同时在敏感词树中移动指针,一旦匹配到叶子节点,即发现敏感词,并进行替换(如替换为***)或拦截处理。
    • 这个过滤模块被做成一个独立的服务,在NLU处理之前调用,确保输入安全。

图片

6. 避坑指南:那些我们踩过的坑

  1. 对话状态泄露(隔离方案) 问题:早期将所有会话上下文都放在一个大的Redis Hash里,键名设计简单(如ctx:user_id),理论上存在键名冲突或数据覆盖的风险(虽然概率极低)。 解决方案:引入session_id作为唯一键,该ID由“用户标识+时间戳+随机数”生成,确保全局唯一。同时,在Coze工作流插件中,严格做到只读写当前会话的session_id对应的数据,从逻辑上隔离。

  2. 冷启动性能问题(预加载策略) 问题:系统重启或新实例启动后,第一次调用NLU模型或加载大型敏感词库时,响应会特别慢。 解决方案:

    • 模型预热:在服务启动时,主动用一些典型query调用一次NLU服务,让模型加载到内存。
    • 缓存预热:将高频的、静态的对话流程配置(如产品FAQ)在启动时加载到本地缓存(如Memcached)或Redis中。
    • 依赖服务健康检查:启动后,先检查所有依赖的插件服务、数据库、Redis是否连通,避免第一次用户请求时才报错。
  3. 监控指标埋点规范 没有监控的系统就是“盲人摸象”。我们制定了统一的埋点规范:

    • 业务指标:各意图的触发次数、槽位填充成功率、工作流各节点的完成/跳出率、转人工率。
    • 性能指标:每个Webhook插件的响应时间、错误码、Coze平台NLU服务的响应时间。
    • 技术指标:Redis读写延迟、服务器CPU/内存使用率。
    • 这些指标通过插件中的日志输出,由日志收集系统(如ELK)汇总,并在Grafana上制作成监控大盘和告警规则。

写在最后

通过这一套基于Coze平台的组合拳,我们的客服系统算是脱胎换骨了。最直观的感受就是,高峰期客服同学的压力小了很多,用户排队等待的时间也大幅缩短。机器能处理掉大部分简单重复的问题,人工客服可以更专注于处理那些复杂的、需要情感沟通的case,整体效率和满意度都上来了。

当然,系统没有银弹,这套架构也在持续迭代中。最后抛两个我们正在思考的开放性问题,也欢迎大家讨论:

  1. 工作流的动态更新与A/B测试:目前的工作流配置更新需要重新发布。如何实现不重启服务的热更新?更进一步,如何能对不同的用户群体灰度发布不同的对话策略(A/B测试),来验证哪种流程转化率更高?
  2. 从“任务型”到“问答型”与“主动式”的扩展:当前工作流主要处理明确的“任务”(查订单、退换货)。如何更优雅地集成基于知识库的“问答”(Q&A)能力?以及,能否基于用户的历史行为,在对话中主动推荐商品或活动,实现“主动式”客服?这其中的上下文管理和意图切换策略又该如何设计?
Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐