京东智能客服核心技术解析:从NLP到多轮对话的架构实践
技术选型
- 618、双11 零点一过,咨询量瞬间翻 20 倍,人工坐席直接被打爆。长尾问题占比高达 35%,“我买的红色 42 码什么时候发”这类口语、省略、倒装句让传统关键词机器人一脸懵。
- 规则引擎(if-contains)维护成本指数级上升,新增一个活动就要写 200+ 条正则;传统机器学习(SVM/CRF)特征工程重,跨领域迁移效果跳水。
- 京东最终采用“BERT+领域 post-train→知识图谱→多轮状态机”的深度学习链路,兼顾准确率与迭代速度,下文按模块展开。
架构设计
- 接入层:统一网关 + 消息队列(Kafka)做流量削峰,单节点 8k QPS 稳定。
- 语义层:
- 预处理:ASR 文本纠错 + 电商词典分词
- 意图识别:BERT-base 领域自适应,输出 640 维向量
- 槽位填充:Bi-LSTM+CRF,与意图共享 encoder,降低 30% 计算量
- 对话管理层:
- 有限状态机(FSM)维护 session,支持上下文继承、回退、重入
- 全局对话记忆存储在 Redis Hash(TTL 15 min),崩溃重启可恢复
- 知识层:
- 商品、活动、售后三元组写入 Neo4j,支持“<商品>-参加-><活动>-提供-><优惠券>”的 3 跳查询
- 向量检索兜底(Milvus),应对未登录 SKU
- 策略层:敏感词、广告法异步过滤,失败日志进 MQ 重试,不阻塞主流程。
代码实现
下面给出最常被问的两段代码:对话状态机与 Neo4j 写入示例,均符合 PEP8,可直接粘贴运行。
1. 轻量级多轮状态机
# -*- coding: utf-8 -*- """session.py 维护用户多轮对话状态,支持中断恢复""" import json import redis from typing import Dict, Optional REDIS = redis.Redis(host='127.0.0.1', port=6379, db=1, decode_responses=True) class DialogueState: """ 简单 FSM:state 字段取值 IDLE / AWAIT_SKU / AWAIT_SIZE / AWAIT_ADDR / END """ def __init__(self, user_id: str): self.user_id = user_id self._key = f"ds:{user_id}" def load(self) -> Dict: """崩溃重启后恢复上下文""" raw = REDIS.get(self._key) return json.loads(raw) if raw else self._default() def _default(self) -> Dict: return {"state": "IDLE", "slots": {}, "history": []} def update(self, state: str, slots: Dict, utterance: str): """每轮调用:更新状态、槽位、历史""" data = self.load() data["state"] = state data["slots"].update(slots) data["history"].append(utterance) REDIS.setex(self._key, 900, json.dumps(data)) # 15 min 过期 def clear(self): REDIS.delete(self._key)使用示例:
>>> ds = DialogueState("u_123") >>> ds.update("AWAIT_SIZE", {"sku_id": "100012043978"}, "我要红色") >>> print(ds.load()) {'state': 'AWAIT_SIZE', 'slots': {'sku_id': '100012043978'}, 'history': ['我要红色']}2. Neo4j 商品活动图谱写入
# graph.py from neo4j import GraphDatabase URI = "bolt://localhost:7687" AUTH = ("neo4j", "neo4j123") def add_sku_activity(tx, sku_id: str, activity_name: str): """建立 (sku)-[:JOIN]->(activity) 关系,存在则覆盖""" cypher = """ MERGE (s:SKU {id: $sku_id}) MERGE (a:Activity {name: $activity_name}) MERGE (s)-[r:JOIN]->(a) SET r.update_time = timestamp() """ tx.run(cypher, sku_id=sku_id, activity_name=activity_name) with GraphDatabase.driver(URI, auth=AUTH) as driver: with driver.session() as session: session.execute_write(add_sku_activity, "100012043978", "618跨店满减")性能优化
- BERT 推理:使用 TensorRT + FP16,batch=16,单卡 T4 可跑到 450 QPS,P99 延迟 58 ms;对比原始 TF 模型提升 2.3 倍。
- 意图+槽位联合训练,参数共享,整体 FLOPs 降 30%,线上 CPU 占用降 18%。
- Redis 对话状态采用 Hash + TTL,内存淘汰策略 allkeys-lfu,大促 12 h 内内存稳定 42 GB,无抖动。
- Neo4j 3 跳查询平均 18 ms,对>2 亿关系开启“索引+内存锁预热”,夜间定时执行
db.stats.full()保持执行计划新鲜。
压测结果(8 核 16 G 容器,单实例):
- 峰值 QPS:12 k
- 平均响应:42 ms
- P99 延迟:110 ms
- 意图准确率:94.7%(Top1)
- 槽位 F1:0.92
- 多轮任务完成率:87%(5 轮内解决)
生产实践
- 对话中断恢复
用户突然退出小程序,15 min 内重新进入,前端带回相同user_id,状态机load()直接拉取 Redis 缓存,实现“无缝续聊”;超过 TTL 自动兜底到“人工客服”,避免答非所问。 - 敏感词异步过滤
主流程只跑白名单模型,敏感词检测丢到 Celery 任务队列,即便异步失败,MQ 重试 3 次后报警,不阻塞用户;平均额外延迟 <10 ms。 - 领域 post-train 技巧
先拿 1.2 亿句原始日志做 MLM,再拿 200 万人工标注做意图分类微调,学习率 2e-5,warmup 10%,epoch=3,可让“价保/售后”这类电商专属意图提升 6.4 个百分点。 - 灰度发布
新模型按“城市+SKU 类目”两维灰度,AB 框架实时回传任务完成率,<85% 自动回滚;上线 3 个月零重大事故。 - 常见坑
- 槽位冲突:用户说“我要 128G 黑色”,颜色与容量同时出现,需给槽位加优先级权重
- 活动时效:Neo4j 里给关系加
end_time,查询时带上WHERE r.end_time > timestamp(),防止过期活动被误召回 - 日志漂移:多机房部署务必把
trace_id注入 Kafka header,否则链路追踪会断
写在最后
把方言、口音、情绪识别再搬进语音通道,整个状态机还要与 VAD(语音活动检测)对齐,延迟预算会被压缩到 300 ms 以内。如果换成你,会如何设计一个既听得懂“川渝塑料普通话”,又能保持多轮上下文不丢的语音客服呢?欢迎留言一起拆方案。