后端实战|AI客服核心实现:Redis状态机+AI意图识别双引擎
大家好,今天给大家分享一个近期落地的AI客服模块核心实现——用Redis做状态机持久化,结合AI Flow做意图识别,搞定客服会话的全流程管控和用户需求精准判断。
做后端的朋友应该都知道,AI客服看似简单,实则核心痛点在于「会话状态管控」和「用户意图识别」:比如用户要查订单,怎么引导用户输入订单号?用户想转人工,怎么终止AI自动回复?如果会话中途中断,再次进入怎么恢复之前的状态?
今天就把这套企业级落地的双引擎方案拆解开,从核心设计到代码实现,干货拉满,代码可直接复用,新手也能快速上手,建议收藏备用~
先说明下:本次分享选取了原项目中「状态机引擎(Redis实现)」「意图识别引擎(Jeecg AI Flow实现)」两大核心模块,这也是AI客服的底层支撑,也是后端开发中最具复用价值的部分,其余测试、优化细节就不冗余赘述了。
一、核心架构:双引擎搞定AI客服底层逻辑
先上整体架构,不用搞复杂,核心就两个引擎,分工明确,避免耦合:
- 状态机引擎:基于Redis实现,负责会话状态的持久化、流转控制,全程追踪对话生命周期(比如初始状态→查单状态→转人工状态);
- 意图识别引擎:基于AI Flow实现,负责识别用户输入的真实需求(比如“查订单”“转人工”“随便聊聊”),输出结构化结果驱动状态机流转。
这里要避个坑:很多人做AI客服会把「状态管控」和「意图识别」混在一起写,后期维护起来巨麻烦,建议按模块拆分,状态机管流程,意图识别管决策,清晰又好扩展。
二、实战实现:Redis状态机引擎(核心代码可直接复用)
状态机是整个客服会话的“大脑”,核心是用Redis的Hash结构持久化会话状态和变量,以conversationId作为会话唯一标识,支持状态读取、初始化、流转和过期管理。
1. 先定义核心常量(KfConsts)
统一管理Redis Key前缀、字段名、过期时间,避免硬编码,后期修改更方便:
public class KfConsts {
// Redis状态Key前缀(格式:kf:state:会话ID)
public static final String KF_STATE_PREFIX = "kf:state:";
// Hash字段:当前会话状态
public static final String FIELD_CURRENT_STATE = "current_state";
// Hash字段:会话变量(存储订单号、意图等,JSON格式)
public static final String FIELD_VARIABLES = "variables";
// 会话过期时间(30分钟,闲置自动过期,读写刷新TTL)
public static final long STATE_TTL_MINUTES = 30L;
// 意图识别AI Flow ID(Jeecg AI Flow的唯一标识)
public static final String INTENT_FLOW_ID = "2042592514488737794";
}
2. 定义会话状态枚举(KfStateNode)
覆盖客服全流程的4种核心状态,避免状态值混乱,同时提供状态匹配方法:
@AllArgsConstructor
@Getter
public enum KfStateNode {
// 初始状态:新会话必须先执行意图识别
INIT,
// 查单状态:等待用户输入订单号
ASK_ORDER,
// 普通对话状态:直接执行AI回复
CHAT,
// 转人工状态:终止AI自动回复
HUMAN_TRANSFER;
// 根据状态名称匹配枚举(忽略大小写,避免前端传值大小写问题)
public static KfStateNode findByType(String type){
for (KfStateNode node : KfStateNode.values()) {
if(node.name().equalsIgnoreCase(type)){
return node;
}
}
return null;
}
}
3. 封装会话状态实体(KfSessionState)
作为状态机的核心数据载体,封装当前状态和会话变量,方便Redis读写和业务逻辑调用:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class KfSessionState {
// 当前会话状态(枚举类型,避免非法状态)
private KfStateNode currentState;
// 会话变量(存储订单号、用户意图、技能标签等,JSON格式)
private JSONObject variables;
}
4. 状态机核心服务(IntentDecisionManager)
最核心的实现,负责会话状态的加载/初始化、持久化、TTL刷新,也是业务层调用的入口,重点看两个方法:loadOrInitState(加载/初始化状态)和persistState(持久化状态):
@Service
@Slf4j
public class IntentDecisionManager {
@Autowired
private RedisTemplate redisTemplate;
// 拼接Redis状态Key(前缀+会话ID)
private String stateKey(String sessionId) {
return KfConsts.KF_STATE_PREFIX + sessionId;
}
// 加载或初始化会话状态(核心方法)
public KfSessionState loadOrInitState(String sessionId) {
String key = stateKey(sessionId);
// 从Redis Hash中获取当前状态和变量
Object currentState = redisTemplate.opsForHash().get(key, KfConsts.FIELD_CURRENT_STATE);
Object variables = redisTemplate.opsForHash().get(key, KfConsts.FIELD_VARIABLES);
// 无状态(新会话),初始化状态为INIT
if (currentState == null) {
KfSessionState initState = new KfSessionState(KfStateNode.INIT, new JSONObject());
// 持久化初始状态
persistState(sessionId, initState);
return initState;
}
// 解析已有状态,刷新TTL(避免会话过期)
KfStateNode state = KfStateNode.valueOf(String.valueOf(currentState));
JSONObject varJson = variables == null ? new JSONObject() : JSONObject.parseObject(String.valueOf(variables));
redisTemplate.expire(key, KfConsts.STATE_TTL_MINUTES, TimeUnit.MINUTES);
return new KfSessionState(state, varJson);
}
// 持久化会话状态到Redis(核心方法)
public void persistState(String sessionId, KfSessionState state) {
String key = stateKey(sessionId);
// 存储当前状态(枚举名称)和变量(JSON字符串)
redisTemplate.opsForHash().put(key, KfConsts.FIELD_CURRENT_STATE, state.getCurrentState().name());
redisTemplate.opsForHash().put(key, KfConsts.FIELD_VARIABLES, state.getVariables().toJSONString());
// 刷新过期时间
redisTemplate.expire(key, KfConsts.STATE_TTL_MINUTES, TimeUnit.MINUTES);
}
}
这里有个实战技巧:每次读写Redis状态时,一定要刷新TTL,避免会话在用户交互过程中过期;另外,新会话默认初始化为INIT状态,确保每次新对话都先执行意图识别,避免流程混乱。
三、实战实现:Jeecg AI Flow意图识别引擎
意图识别是驱动状态机流转的关键,核心是通过Jeecg内置的AI Flow编排意图分类流程,输入用户消息,输出结构化的意图结果(意图、技能标签、置信度)。
1. 封装意图识别结果实体(IntentDecision)
统一封装AI Flow的返回结果,方便业务层使用,避免直接操作JSON:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class IntentDecision {
// 用户意图(transfer_human=转人工,ask_order=查订单,chat=普通对话)
private String intent;
// 业务技能标签(售后/售前/技术支持,用于后续业务分流)
private String skill;
// 识别置信度(0-1,可根据实际场景调整阈值)
private BigDecimal confidence;
}
2. 意图识别核心方法(延续IntentDecisionManager)
调用Jeecg AI Flow接口,执行意图分类,解析返回结果,同时处理异常情况(比如AI Flow返回为空、格式异常):
@Service
@Slf4j
public class IntentDecisionManager {
// 省略上面的Redis状态机相关方法...
@Autowired
private ISysBaseAPI sysBaseApi;
// 执行AI Flow意图识别(核心方法)
public IntentDecision runIntentFlow(String userMessage) {
// 1. 封装AI Flow入参(用户消息作为question传入)
Map<String, Object> inputs = new HashMap<>();
inputs.put("question", userMessage);
// 2. 构建AI Flow调用参数(指定Flow ID,非流式调用)
AiragFlowDTO dto = AiragFlowDTO.builder()
.flowId(KfConsts.INTENT_FLOW_ID)
.inputParams(inputs)
.isStream(false)
.build();
// 3. 调用Jeecg AI Flow接口,获取识别结果
Object result = sysBaseApi.runAiragFlow(dto);
JSONObject json = normalizeFlowResult(result);
// 4. 解析意图结果,校验必填字段
String intent = json.getString("intent");
String skill = json.getString("skill");
if (oConvertUtils.isEmpty(intent)) {
throw new JeecgBootException("意图识别结果缺少intent字段");
}
// 5. 返回结构化结果(置信度可根据AI Flow实际返回调整,这里默认0.92)
return new IntentDecision(intent, skill, new BigDecimal(0.92));
}
// 标准化AI Flow返回结果(处理不同格式的返回,避免空指针)
private JSONObject normalizeFlowResult(Object result) {
if (result == null) {
throw new JeecgBootException("意图识别返回为空");
}
// 处理JSON对象、字符串、Map三种常见返回格式
if (result instanceof JSONObject) {
return (JSONObject) result;
}
if (result instanceof String) {
return JSONObject.parseObject((String) result);
}
if (result instanceof Map) {
JSONObject json = new JSONObject();
json.putAll((Map<? extends String, ?>) result);
Object output = json.get("output");
if (output instanceof String) {
return JSONObject.parseObject((String) output);
}
if (output instanceof Map) {
JSONObject out = new JSONObject();
out.putAll((Map<? extends String, ?>) output);
return out;
}
return json;
}
// 兜底处理:转为JSON字符串再解析
return JSONObject.parseObject(JSONObject.toJSONString(result));
}
// 额外补充:查单状态专用——提取用户消息中的订单号(正则匹配)
public String extractOrderNo(String text) {
if (oConvertUtils.isEmpty(text)) {
return null;
}
// 匹配8-32位字母、数字、横线组成的订单号(可根据实际订单格式调整正则)
Matcher matcher = Pattern.compile("([A-Za-z0-9\-]{8,32})").matcher(text);
return matcher.find() ? matcher.group(1) : null;
}
}
实战避坑:AI Flow的返回格式可能不统一(比如有时是JSON对象,有时是字符串),一定要做标准化处理,否则容易出现空指针;另外,订单号提取的正则的可以根据自己的业务场景调整,避免漏提或误提。
四、核心流程串联(极简版)
最后给大家串一下核心流程,让大家明白两个引擎是怎么配合工作的:
- 用户发送消息,请求进入/airag/chat/send接口;
- 接口调用loadOrInitState方法,加载或初始化会话状态(新会话为INIT);
- 如果是INIT状态,调用runIntentFlow方法识别用户意图;
- 根据意图更新会话状态(比如“ask_order”→ASK_ORDER状态,“transfer_human”→HUMAN_TRANSFER状态);
- 如果是ASK_ORDER状态,调用extractOrderNo提取订单号,存储到会话变量;
- 如果是HUMAN_TRANSFER状态,终止AI回复,提示用户等待人工客服。
五、实战心得&后续优化建议
这套方案已经在项目中落地,总结两个核心心得,帮大家少走弯路:
- 状态机一定要持久化:用Redis比本地缓存更可靠,避免服务重启后会话状态丢失;
- 意图识别只执行一次:仅在INIT状态执行,避免重复调用AI Flow,降低成本。
后续可以优化的点(留给大家拓展):
- AI Flow超时兜底:超时或返回异常时,默认降级为CHAT状态,不中断会话;
- 转人工联动:状态转为HUMAN_TRANSFER后,通过WebSocket通知人工坐席;
- 日志审计:记录会话ID、意图、状态,方便问题排查。
今天的分享就到这里,核心代码都已经贴出来了,大家可以直接复制到项目中修改使用。这套双引擎方案不仅适用于AI客服,还可以迁移到其他需要会话状态管控的场景(比如智能问答、对话机器人)。
如果大家有疑问,或者有更好的实现方式,欢迎在评论区交流~ 关注我,后续持续分享后端AI应用开发干货,一起提升技术,少走弯路!