背景与痛点
把“能跑”的智能客服搬到线上,往往比“能跑”本身更折磨人。 一个常见场景:业务线已经有一套成熟 App / 小程序 / Web 站点,需要在 1-2 周内把自研客服能力嵌入,结果一动手就踩坑:
- 接口协议不兼容:客服返回的是 JSON,老系统只认 XML;或者字段大小写、时区、编码方式不一致。
- 上下文丢失:用户刷新页面或 App 切后台,再回来时对话 ID 失效,机器人从头开始“您好,请问有什么可以帮您?”
- 性能瓶颈:高峰期并发 3 k,客服服务 200 ms 响应直接飙到 2 s,连带把主站 CPU 打满。
这些痛点本质上是“集成边界”问题——自研服务与存量系统在网络、数据、状态三个维度没有对齐。
技术方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Webhook(事件推送) | 实时性好,客服侧主动推送 | 需要公网可回调地址,对防火墙不友好 | 客服事件需及时同步到 CRM |
| REST API(轮询/同步) | 实现简单,调试直观 | 高频轮询浪费带宽,长轮询仍受限于 HTTP 超时 | 移动端、H5 嵌入,接入周期短 |
| WebSocket | 真正的全双工,低延迟 | 需要额外网关支持,断线重连逻辑复杂 | 网页在线客服、IM 场景 |
经验:80% 产品先用 REST API 兜底,后续按渠道逐步切 WebSocket;Webhook 仅做“事件通知”补充,不承载主对话流。
核心实现
1. 设计可扩展的 RESTful API 接口
- 统一前缀
/api/v1/bot,版本号放路径,方便灰度。 - 资源导向:
POST /sessions创建会话,GET /sessions/{sid}/messages拉取消息。 - 采用 JSON:API 风格,返回包一层
data/meta,预留included做分页或推荐商品。 - 全部接口带
X-Request-ID,方便链路追踪。
2. 实现对话上下文保持机制
- 会话 ID(sid)由服务端雪花算法生成,落 Redis 带 24 h TTL;App 端持久化到 localStorage / 轻量数据库。
- 上下文 = 最近 k 轮(如 10 轮)用户与机器人消息,按 “role: user / Bot” 数组存储;每次请求带
last_message_id做增量拉取,避免全量传输。 - 若用户匿名,用设备号 + 临时 token;登录后调用
PATCH /sessions/{sid}绑定 UID,实现匿名→实名迁移。
3. 处理异步消息队列
- 机器人回答需要调用 NLU、知识图谱、搜索等多服务,耗时 200-800 ms,必须解耦。
- 采用“请求-回执”模式:用户提问 → 网关立即返回 202 Accepted + 空响应 → 后端把任务写入 Kafka topic
bot.query→ 消费者计算完成后写 Redis 并 pub 通知 → 网关通过长轮询或 WebSocket 把答案推回客户端。 - 好处:失败可重试,削峰填谷;客户端感知不到后端抖动。
代码示例
以下示例基于 Python 3.11 + FastAPI,展示“创建会话 + 发送消息”最小闭环,含日志与异常映射。Node.js 版本同理,可照搬到 Express。
# main.py import uuid import time import logging from typing import Dict from fastapi import FastAPI, HTTPException, Header, Depends from redis import Redis from pydantic import BaseModel, Field # -------------------- 配置 -------------------- LOG = logging.getLogger(__name__) redis = Redis(host='127.0.0.1', port=6379, decode_responses=True) app = FastAPI(title="Bot Gateway", version="1.0.0") # -------------------- 模型 -------------------- class CreateSessionRsp(BaseModel): sid: str ttl: int = Field(description="会话有效期,秒") class MessageReq(BaseModel): text: str = Field(..., min_length=1, max_length=500) class MessageRsp(BaseModel): answer: str elapsed_ms: int # -------------------- 依赖 -------------------- def get_request_id(x_request_id: str = Header(default_factory=lambda: str(uuid.uuid4()))): return x_request_id # -------------------- 接口 -------------------- @app.post("/api/v1/bot/sessions", response_model=CreateSessionRsp) def create_session(request_id: str = Depends(get_request_id)): sid = str(uuid.uuid4()) ttl = 86400 redis.setex(f"session:{sid}", ttl, "{}") # 占位 LOG.info("[rid:%s] session created sid=%s", request_id, sid) return CreateSessionRsp(sid=sid, ttl=ttl) @app.post("/api/v1/bot/sessions/{sid}/messages", response_model=MessageRsp) def chat(sid: str, req: MessageReq, request_id: str = Depends(get_request_id)): if not redis.exists(f"session:{sid}"): raise HTTPException(status_code=404, detail="session not found") start = time.time() # TODO: 写入消息队列,等待消费者处理;此处简化成立即返回 answer = f"Echo: {req.text}" elapsed = int((time.time() - start)*1000) LOG.info("[tid:%s] sid=%s, Q=%s, A=%s, elapsed=%dms", request_id, sid, req.text, answer, elapsed) return MessageRsp(answer=answer, elapsed_ms=elapsed) # -------------------- 统一异常 -------------------- @app.exception_handler(Exception) def all_exception_handler(request, exc): LOG.exception("Unhandled error") return {"code": 500, "message": "Internal Server Error", "request_id": request.headers.get("x-request-id")}要点:
- 日志统一带
request_id,方便 ELK 聚合。 - 业务异常与未知异常分层返回,前端可按
code做 Toast。 - Redis 键加前缀,方便后续分库。
性能优化
- 缓存策略
- 热点问答直接走 Redis 缓存,key 为
faq:hash(问题),TTL 5 min;命中率 30%+ 即可把平均 RT 降到 60 ms。
- 热点问答直接走 Redis 缓存,key 为
- 连接池管理
- 网关到客服服务使用 aiohttp 的 TCPConnector,limit=200,limit_per_host=50;同时把
keepalive_timeout调到 30 s,减少握手开销。
- 网关到客服服务使用 aiohttp 的 TCPConnector,limit=200,limit_per_host=50;同时把
- 负载均衡
- 客服服务无状态,用 Nginx + consul-template 动态 upstream;高峰时基于 CPU 做自动扩容,HPA 策略:CPU>55% 持续 60 s 即加 2 个 Pod。
避坑指南
- 认证:千万别把
appSecret硬编码到前端。推荐 OAuth2 客户端模式,网关用 Client Credentials 换 JWT,缓存 5 min。 - 超时:反向代理→客服服务 read_timeout 设 3 s,超过即熔断返回“正在思考中”,后台继续算,用队列结果回补。
- 限流:单 UID 维度 60 次/分钟,IP 维度 300 次/分钟;超出返回 429,前端弹“提问过快”提示,避免被刷。
- 日志脱敏:手机号、身份证用正则打码,否则 GDPR/网安法等着请喝茶。
总结与延伸
把自研客服搬进产品线,本质是“边开飞机边换引擎”:先让老系统无感过渡,再逐步替换核心链路。建议读者按本文示例,先跑通“创建会话→发送消息→接收回答”最小闭环,压测 500 并发,观察 RT 与错误率;接着把 WebSocket 推流加上,实现“机器人正在输入…”的实时体验;最后思考多轮对话的槽点——例如用户说“我要退掉昨天的订单”,机器人需要追问“哪一笔”、展示订单卡片,这背后就要把“对话状态机”与“业务实体”打通,用 DSL 描述槽位填充与函数调用,才能真正做到“无缝”。
下一步,不妨尝试:
- 引入强化学习做动态话术排序;
- 把 FAQ 召回与生成式模型做混合推理;
- 用 GraphQL 聚合订单、物流、优惠券接口,让机器人在一次响应里返回结构化卡片,而非纯文本。
把客服做成“隐形”功能,用户只觉方便,不觉存在,就是集成成功的标志。