最近在做一个智能客服系统的升级,原来的规则引擎在面对复杂多变的用户问题时,越来越力不从心。用户经常抱怨客服机器人“答非所问”或者“一句话就结束对话”。为了解决这个问题,我们团队尝试引入了扣子平台的智能体能力,并重点围绕其问答流程图功能进行了一系列设计和优化。今天就把我们趟过的一些坑和总结的经验分享出来。
1. 背景与痛点:为什么传统的路走不通了?
我们之前的客服系统主要基于规则引擎,它的工作模式很直接:匹配关键词 -> 触发固定回复。这在处理简单、标准的问题时很快,比如“查余额”、“改密码”。但一旦遇到稍微复杂点的场景,问题就暴露无遗:
- 多轮对话僵化:用户问“我想订一张明天去北京的机票”,规则引擎可能只会回复“请问您的出发地是?”,而无法结合上下文理解用户已经提供了目的地(北京)和时间(明天)。要实现多轮,就得写一大堆
if-else嵌套的状态判断,维护起来简直是噩梦。 - 意图识别能力弱:用户说“我付不了款,提示我银行卡有问题”,这句话里可能包含“支付失败”、“银行卡异常”甚至“寻求人工帮助”多个意图。规则引擎靠关键词匹配,很容易抓错重点,或者因为词库没覆盖而直接失效。
- 异常处理全靠猜:网络超时、用户输入乱码、突然切换话题……这些边界情况在规则引擎里往往只能用一个笼统的“抱歉,我没听懂”来回应,用户体验很差,也无法收集有效数据来优化。
正是这些痛点,促使我们去寻找更灵活的解决方案,也就是AI辅助开发。扣子平台提供的智能体,其核心优势在于能理解自然语言,而流程图功能则让我们能将这种理解能力,有序地组织成一个可控、可预测的对话流程。
2. 技术方案对比:规则、模型与流程图
在选型时,我们仔细对比了三种主流方案:
传统规则引擎:
- 优点:响应极快(毫秒级),规则明确,无模型训练成本。
- 缺点:准确率严重依赖规则库的完备性,泛化能力差,维护成本随着业务复杂度呈指数级增长。
- 适用场景:业务固定、意图非常有限的场景,如密码重置、开关查询。
纯机器学习/NLP模型:
- 优点:意图识别准确率高,能处理复杂、多样的自然语言表达。
- 缺点:端到端模型时延较高(可能需要数百毫秒甚至秒级),需要大量标注数据训练,且对话流程控制逻辑需要额外开发,模型本身是个“黑盒”,调试困难。
- 适用场景:对准确率要求极高,且有充足数据和技术团队支撑的场景。
扣子平台流程图方案:
- 优点:结合了AI理解与流程控制。扣子的智能体负责理解用户意图(解决了准确率问题),开发者通过拖拽流程图来设计对话状态跳转(解决了可控性问题)。维护相对直观,可以通过调整流程图来优化业务逻辑。
- 缺点:相比纯规则引擎有额外网络调用开销(依赖平台API),流程设计的优劣直接影响最终效果。
- 适用场景:需要平衡智能与可控性的复杂业务客服系统,尤其适合快速迭代和业务人员参与调整。
对我们而言,扣子平台的方案在开发效率、维护成本和最终效果上取得了较好的平衡。它让我们能用较低的成本,获得一个“会思考、有逻辑”的客服系统。
3. 核心实现:从流程图到代码
3.1 流程图状态机设计
我们使用PlantUML来设计和文档化核心的问答流程状态机。一个好的流程图不仅是开发指南,也是团队沟通和后期维护的蓝图。
@startuml state “等待用户输入” as WaitInput state “意图识别(扣子)” as IntentRecognition state “槽位填充” as SlotFilling state “执行具体业务” as ExecuteAction state “生成回复” as GenerateResponse state “超时/异常处理” as ErrorHandle state “意图回退/澄清” as Fallback WaitInput --> IntentRecognition : 收到用户消息 IntentRecognition --> SlotFilling : 识别到明确意图\n且需补充信息 IntentRecognition --> ExecuteAction : 识别到明确意图\n且信息完整 IntentRecognition --> Fallback : 意图置信度低\n或无法识别 SlotFilling --> ExecuteAction : 所需槽位填充完毕 SlotFilling --> Fallback : 多次询问后仍无法填充 ExecuteAction --> GenerateResponse : 调用业务API成功 ExecuteAction --> ErrorHandle : 业务API调用失败\n或超时 GenerateResponse --> WaitInput : 返回答案,等待下一轮 ErrorHandle --> WaitInput : 返回友好错误提示 Fallback --> WaitInput : 提示用户重新表述\n或转人工 ' 边界条件 note right of IntentRecognition **超时重试**:调用扣子API超时, 最多重试2次,失败则跳转ErrorHandle。 end note note right of SlotFilling **会话粘性**:同一会话中, 已填充的槽位信息会被保留, 避免重复询问。 end note note right of Fallback **意图回退**:连续N次进入Fallback, 自动触发转人工策略。 end note @enduml这个状态机清晰地定义了对话的各个阶段和跳转条件,特别是包含了超时重试、意图回退等关键边界处理。
3.2 平台API集成示例
有了流程图,接下来就是用代码将其实现。以下是集成扣子平台API的核心Python示例,重点展示了异步处理和错误重试机制。
import aiohttp import asyncio from typing import Optional, Dict, Any from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type class KoziAgentClient: """扣子智能体API客户端""" def __init__(self, api_base: str, client_id: str, client_secret: str): self.api_base = api_base self.client_id = client_id self.client_secret = client_secret self._session: Optional[aiohttp.ClientSession] = None self._token: Optional[str] = None async def _get_session(self) -> aiohttp.ClientSession: """获取或创建aiohttp会话,使用连接池提升性能""" if self._session is None or self._session.closed: connector = aiohttp.TCPConnector(limit=100, limit_per_host=20) # 连接池配置 self._session = aiohttp.ClientSession(connector=connector) return self._session async def _ensure_token(self): """OAuth 2.0客户端凭证模式获取访问令牌,并缓存""" if self._token is not None: # 简单起见,这里假设token未过期。生产环境应校验过期时间。 return auth_url = f"{self.api_base}/oauth/token" data = { 'grant_type': 'client_credentials', 'client_id': self.client_id, 'client_secret': self.client_secret } session = await self._get_session() async with session.post(auth_url, data=data) as resp: result = await resp.json() self._token = result['access_token'] @retry( stop=stop_after_attempt(3), # 最大重试3次 wait=wait_exponential(multiplier=1, min=2, max=10), # 指数退避 retry=retry_if_exception_type((aiohttp.ClientError, asyncio.TimeoutError)) ) async def query_agent(self, session_id: str, user_input: str, context: Optional[Dict] = None) -> Dict[str, Any]: """ 向扣子智能体发送查询请求。 选择广度优先的意图匹配策略:扣子平台会在内部对用户输入进行多维度意图分析, 返回置信度最高的一个或多个意图,我们取最高置信度的结果,这比深度优先遍历固定规则树更灵活。 """ await self._ensure_token() query_url = f"{self.api_base}/v1/agent/query" payload = { "session_id": session_id, # 关键:用于维持对话状态 "query": user_input, "context": context or {}, # 可传递上文已填充的槽位等信息 "strategy": "broad_first" # 示意参数,代表广度优先匹配策略 } headers = {"Authorization": f"Bearer {self._token}"} session = await self._get_session() try: # 设置总超时时间为5秒 async with session.post(query_url, json=payload, headers=headers, timeout=5) as resp: resp.raise_for_status() return await resp.json() except asyncio.TimeoutError: # 记录超时日志,触发降级逻辑(如返回默认提示) raise except aiohttp.ClientError as e: # 记录网络异常日志 raise async def close(self): """清理资源""" if self._session: await self._session.close() # 使用示例 async def main(): client = KoziAgentClient(api_base="https://api.kozi.com", client_id="your_id", client_secret="your_secret") try: # 模拟一次用户咨询 response = await client.query_agent( session_id="user_123_session", user_input="我的订单为什么还没发货?" ) # 处理response中的意图、槽位和回复文本 intent = response.get('top_intent') slots = response.get('slots', {}) reply_text = response.get('reply') print(f"识别意图: {intent}, 回复: {reply_text}") finally: await client.close() if __name__ == "__main__": asyncio.run(main())4. 性能优化:让智能客服又快又稳
接入AI能力后,性能成为关键考量。外部API调用、意图识别都是潜在的耗时操作。
4.1 压测方案设计
我们使用JMeter模拟高并发用户场景,重点观察几个指标:平均响应时间、错误率、系统资源占用。
- JMeter脚本配置要点:
- 线程组:模拟并发用户数,我们设置了阶梯加压(50, 100, 200, 500用户)。
- HTTP请求:指向我们封装好的客服接口,Body中携带不同的用户问题样本(需覆盖简单、复杂、模糊意图)。
- 定时器:在请求间添加随机等待时间(如高斯随机定时器),模拟真实用户间隔。
- 断言:检查响应中是否包含关键字段(如
reply),并验证状态码。 - 监听器:使用聚合报告、响应时间图等查看结果。
4.2 连接池与缓存策略
- 连接池:如上文代码所示,使用
aiohttp.TCPConnector建立HTTP连接池,避免频繁建立和断开连接的开销。根据压测结果调整limit和limit_per_host参数。 - 缓存应用:
- 意图缓存:对于高频、标准的用户问题(如“你好”、“谢谢”),其意图识别结果在短时间内是稳定的。我们使用Redis缓存
用户问题MD5 -> 意图&标准回复,设置一个较短的TTL(如5分钟),能显著减少对扣子平台的调用。 - 会话状态缓存:将
session_id对应的对话上下文(如已填充的槽位)存储在Redis中,避免每次请求都从数据库读取,保证会话粘性的同时也降低了延迟。
- 意图缓存:对于高频、标准的用户问题(如“你好”、“谢谢”),其意图识别结果在短时间内是稳定的。我们使用Redis缓存
通过以上优化,我们成功将客服系统的平均响应速度提升了30%以上,在高峰期也能保持稳定。
5. 避坑指南:前人踩过的坑
5.1 对话状态持久化的常见错误
- 错误1:状态存储过大或过小。把整个对话历史全存下来,浪费空间且效率低;或者只存最后一个意图,导致上下文丢失。正确做法:只存储必要的上下文信息,如当前流程节点、已收集的槽位键值对、用户身份标识等结构化数据。
- 错误2:状态丢失与并发冲突。用户快速连续发送消息,可能导致后一个请求读取到的还是未更新的旧状态。解决方案:使用支持原子操作的存储(如Redis的
HMSET+EXPIRE),或采用乐观锁机制更新会话状态。
5.2 敏感词过滤的异步处理陷阱
为了合规,我们需要在返回给用户前进行敏感词过滤。最初我们采用同步过滤,即拿到扣子的回复后,立即调用本地过滤服务,这增加了响应时间。
- 陷阱:后来我们改为“先返回,后异步过滤并记录日志”,却发现如果异步过滤失败,这条潜在的敏感内容就失去了追溯能力。
- 优化方案:采用并行处理。在调用扣子API的同时,也发起一个异步的敏感词预检任务(对用户输入)。当拿到扣子回复后,预检任务如果已完成或有高风险结果,则同步进行最终过滤;如果预检未完成且风险未知,则先返回一个中性提示(如“正在处理您的请求”),待异步过滤完成后再通过推送等方式补发安全回复。这需要在流程图中增加一个判断节点。
6. 实践建议:流程图的长期运营
- 流程图版本控制:将扣子平台导出的流程图JSON文件纳入Git仓库管理。每次修改都对应一个提交,方便回滚和追溯。可以建立
dev、test、prod分支,对应不同环境。 - A/B测试实施:对于关键流程的改动(比如修改意图澄清的话术),可以通过在会话
session_id或user_id上哈希取模的方式,将流量切分到不同版本的流程图上。然后对比两组用户的任务完成率、平均对话轮次和满意度评分,用数据驱动决策。
总结与思考
通过将扣子平台的智能体与精心设计的流程图相结合,我们构建的客服系统在灵活性和可控性上取得了不错的平衡。AI负责理解用户的“言外之意”,流程图则确保对话朝着解决问题的方向有序推进。优化是一个持续的过程,需要不断监控、压测和调整。
最后,留一个我们在实践中遇到的开放式问题,供大家思考:
当用户的一句话同时触发了多个流程分支时(例如,“帮我改签机票并且开发票”包含了“改签”和“开票”两个独立意图),如何设计优先级仲裁机制?是根据业务规则硬编码优先级,还是设计一个更智能的流程合并与调度机制?