限时福利领取


开篇:AI 客服开发的三大拦路虎

第一次做 AI 客服,我踩的坑比写的代码还多。总结下来,最痛的其实就三件事:

  1. Intent Recognition/意图识别准确率忽高忽低,用户一句“我要退货”能被拆成“我要”“退货”两个意图,结果直接走售后流程。
  2. 对话状态管理复杂,用户中途去微信回个消息,再回来问“那刚才那个订单呢?”系统已经忘了上下文。
  3. 第三方 API 集成困难,微信、钉钉、飞书各家字段不统一,每对接一次就要重写一套解析逻辑。

如果你也卡在以上三点,下面的实战笔记或许能帮你少走 3 天弯路。

技术选型:TensorFlow 与 PyTorch 的 Java 生态对比

Java 圈做深度学习,最尴尬的就是“Python 训练完,Java 只能调 REST”。先把主流方案拉个表:

维度 TensorFlow Serving PyTorch Java API
模型热更新 支持版本标签 需重启 JVM
Java 客户端 官方 tensorflow-java 社区 pytorch-java
序列化格式 SavedModel(Protobuf) TorchScript
线程安全 开箱即用 需手动加锁
文档&示例 丰富 稀少

结论:Java 技术栈直接上 TensorFlow Serving,配合 Spring Boot + WebFlux,响应式线程模型可以把 P99 延迟再降 20 ms。

核心实现:三步跑通“识别-管理-回复”闭环

1. 用 TensorFlow Java API 加载 BERT 意图模型

把 Python 训练好的 intent_bert 导出 SavedModel,目录结构:

saved_model.pb
variables/
assets/

Spring Boot 侧只干两件事:预热 Session,封装预测。

@Configuration
public class TfConfig {
  @Bean
  public SavedModelBundle bundle() {
    return SavedModelBundle.load("/models/intent_bert", "serve");
  }
}

@Service
public class IntentService {
  private final SavedModelBundle bundle;

  public IntentService(SavedModelBundle bundle) {
    this.bundle = bundle;
  }

  public String predict(@NonNull String text) {
    try (TString tensor = TString.scalarOf(Shape.of(1), NdArrays.ofObjects(String.class, Shape.of(1)))) {
      tensor.set(NdArrays.vectorOfObject(text, Shape.of(1)));
      try (TString result = bundle.session().runner()
          .feed("serving_default_input_ids", tensor)
          .fetch("StatefulPartitionedCall")
          .run().get(0).expect(TString.class)) {
        return result.getObject(0);
      }
    }
  }
}

要点:

  • 用 try-with-resources 自动关 tensor,防止 native 内存泄漏。
  • 输入节点名、输出节点名用 saved_model_cli show 查,别硬编码。

2. Redis 对话上下文管理(含 TTL)

用户维度 key 设计:chat:${openId},value 用 JSON 存多轮实体。

@Component
@RequiredArgsConstructor
public class ChatContextManager {
  private final StringRedisTemplate redis;

  public void save(@NonNull String openId, @NonNull ChatTurn turn) {
    String key = "chat:" + openId;
    List<ChatTurn> list = Optional.ofNullable(redis.opsForValue().get(key))
        .map(s -> JSON.parseArray(s, new TypeReference<List<ChatTurn>>() {}))
        .orElse(new ArrayList<>());
    list.add(turn);
    redis.opsForValue().set(key, JSON.toJSONString(list), Duration.ofMinutes(30));
  }

  public List<ChatTurn> list(@NonNull String openId) {
    String s = redis.opsForValue().get("chat:" + openId);
    return s == null ? Collections.emptyList()
        : JSON.parseArray(s, ChatTurn.class);
  }
}

TTL 30 min 足够普通客服场景,既省内存又符合 GDPR“数据不过夜”。

3. 微信/钉钉消息适配器(适配器模式)

统一入口接口:

public interface MessageAdapter {
  boolean support(String platform);
  ChatTurn extract(HttpServletRequest request) throws IOException;
}

微信实现:

@Component
public class WxAdapter implements MessageAdapter {
  public boolean support(String platform) { return "wechat".equals(platform); }

  public ChatTurn extract(HttpServletRequest request) {
    WxMessage wx = XmlUtils.parse(request.getInputStream(), WxMessage.class);
    return new ChatTurn(wx.getFromUser(), wx.getContent(), Instant.now());
  }
}

钉钉实现同理,只换 XML 字段名。上层 Service 按 support 顺序链式调用,后期加飞书、Teams 都不用改核心逻辑。

性能调优:把 200 TPS 翻成 600 TPS

JMeter 压测关键指标

  • 样本:20 并发线程,每个线程循环 500 次,Payload 为 200 字中文。
  • 结果:
    • 平均 RT 42 ms → 28 ms(开启 WebFlux + Netty 工作线程=CPU 核数×2)
    • 95% RT 110 ms → 55 ms
    • 错误率 0.2% → 0%(补齐连接池 maxIdle=0 的坑)

线程池与 GC 建议

  1. 业务线程池:
    core=CPU*2, max=CPU*4, queue=1024, RejectedExecutionHandler=CallerRuns
    防止突发流量把内存打爆。
  2. GC:
    用 G1,-XX:+UseStringDeduplication 对 JSON 极友好;MaxGCPauseMillis 设 100 ms,实测 4C8G 机器 Young GC 平均 35 ms。

避坑指南:中文场景的小细节

1. 中文分词歧义

“长春药店” 还是 “长/春药店”?BERT 本身自带子词,但实体词典仍需人工维护。
解决:

  • 训练数据里加城市+药店组合 5000 条。
  • 预测后处理用词典再校正,准确率能再提 3%。

2. 异步日志对延迟的影响

logback 的 AsyncQueueAppender 默认 buffer=256,高并发下打满会阻塞业务线程。
解决:

  • buffer 提到 2048,discardingThreshold=0(不丢日志)。
  • 关键日志用同步写,其余走异步,RT 抖动从 15 ms 降到 2 ms。

3. OAuth2.0 令牌缓存

微信客服接口 access_token 有效期 2 h,官方限 2000 次/天。
解决:

  • 全局缓存到 Redis,key=wechat:access_token,TTL=7000 s,提前 5 min 刷新。
  • 用 Redisson 的 RLock 防并发刷新,避免多实例同时请求。

上线效果截图

压测报告

小结:从 0 到 200 TPS 的踩坑路线

  1. 选 TensorFlow Serving 而不是自己写 JNI,能省 30% 调试时间。
  2. Redis 对话上下文 + TTL 是性价比最高的状态管理方案,比 JWT 自解析省 40% 网络开销。
  3. 适配器模式让多平台消息字段差异对业务代码透明,后期扩展飞书、Teams 只需新增一个类。
  4. 性能优化先调线程池再调 GC,日志别小看,异步参数不对直接拖慢 RT。
  5. 中文场景别迷信算法,词典+后处理往往比再叠一层模型见效快。

开放讨论

多轮对话如果跨天、跨设备,用户说“接着昨天的聊”,你该怎么设计断点恢复机制?

  • 把上下文快照存数据库还是对象存储?
  • 快照版本如何与模型迭代兼容?
  • 用户隐私合规要求 30 天删除,断点数据是否单独生命周期?

欢迎留言聊聊你的思路,一起把 AI 客服做成真正“记得住”的伙伴。

限时福利领取


Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐