冲刺博客10
昨天的成就
飞书机器人的核心推送能力已经就绪:token 缓存机制工作正常(token 过期前不会重复请求),ENGINEER_OPEN_ID 映射表正确完成姓名 ↔ open_id 的双向解析,sendInteractiveCard 可以向指定工程师推送格式化的富文本卡片,且 WebSocket 长连接服务能正常接收用户消息。
遇到的困难
困难一:自然语言意图识别的歧义问题。用户说"帮我整个工单呗"到底是创建工单还是查询工单?"3 楼空调坏了"既像报修也像陈述事实。需要建立一套优先级规则:先检查故障关键词 → 再检查"工单/计划"的明确指令 → 最后回退为聊天。
困难二:同义词和错别字的归一化。用户可能会说"修一下空调"、"维修空调"、"整个维修单",甚至错别字"维收"、"寻检"。如果不做归一化,规则匹配会漏掉大量请求。
困难三:槽位抽取的冲突。一句话中可能同时包含位置信息("3 楼")、设备名("空调")、优先级("紧急")、指派人("赵工")、日期("明天")等信息,如何把这些字段准确分桶到对应的 slot 而不互相覆盖?
困难四:多轮对话的状态维护。NLParser 解析后可能发现缺字段(比如用户只说"3 楼空调坏了"没说派给谁),这时需要引导用户补充缺失信息。要在多轮对话间保持 Draft 状态。
今天的任务
实现 FeishuNLParser 自然语言解析器:意图识别 + 槽位抽取 + 同义词归一化
实现 FeishuDraftSession:维护多轮对话中的工单草稿状态
接入 KimiAIService:当 NLParser 判定为闲聊时,调用大模型生成回复
实现 applyModification 方法:支持用户在多轮对话中修改任意槽位
代码嵌入
FeishuNLParser.java — 核心意图识别 detectType:
/** 识别一句话属于什么意图:
-
- CREATE_ORDER / CREATE_PLAN(创建工单/计划)
-
- 其他(纯对话) */
public static String detectType(String text) {
if (text == null) return null;
String lower = normalize(text); // 同义词/错别字归一化
// ① 明确出现「XX计划」字样 → 一定是计划
if (containsAny(lower, "巡检计划", "巡查计划")) return TYPE_INSPECTION_PLAN;
if (containsAny(lower, "保养计划", "维护计划")) return TYPE_MAINTENANCE_PLAN;
if (containsAny(lower, "检测计划", "检查计划")) return TYPE_DETECTION_PLAN;
if (lower.contains("计划")) {
if (containsAny(lower, "巡检")) return TYPE_INSPECTION_PLAN;
if (containsAny(lower, "保养")) return TYPE_MAINTENANCE_PLAN;
if (containsAny(lower, "检测")) return TYPE_DETECTION_PLAN;
return TYPE_MAINTENANCE_PLAN; // 默认保养计划
}// ② 明确出现「XX工单」字样
if (containsAny(lower, "维修工单", "修工单")) return TYPE_REPAIR_ORDER;
if (containsAny(lower, "巡检工单")) return TYPE_INSPECTION_ORDER;
if (containsAny(lower, "故障工单", "故障报修")) return TYPE_FAULT;// ③ 出现"维修/巡检/保养/检测"字样 → 工单
if (containsAny(lower, "维修", "修理")) return TYPE_REPAIR_ORDER;
if (containsAny(lower, "巡检", "巡查")) return TYPE_INSPECTION_ORDER;// ④ 故障关键词(如"坏了"、"漏水"、"跳闸")→ 故障工单
for (String kw : FAULT_KEYWORDS) if (lower.contains(kw)) return TYPE_FAULT;// ⑤ 出现"创建/开 工单" → 默认维修工单
if (containsAny(lower, "创建", "新增", "开", "生成")) {
if (lower.contains("工单")) return TYPE_REPAIR_ORDER;
}
return null; // 未命中 → 当作普通聊天
}
同义词归一化 normalize(占位符法避免循环替换污染):
private static String normalize(String text) {
if (text == null) return "";
String s = text.toLowerCase();// 非空目标替换:修→维修,寻检→巡检,空条→空调...
int placeholderIdx = 0;
for (String[] pair : SYNONYMS) {
if (pair == null || pair.length < 2) continue;
String target = pair[pair.length - 1];
if (target == null || target.isEmpty()) continue; // 语气词组跳过List
sources = new ArrayList<>();
for (int i = 0; i < pair.length - 1; i++) {
if (pair[i] != null && !pair[i].isEmpty() && !pair[i].equals(target)) {
sources.add(pair[i]);
}
}
if (sources.isEmpty()) continue;// 占位符法:保护原 target → 替换 source → 还原 placeholder
String placeholder = "\u0001T" + (placeholderIdx++) + "\u0001";
s = s.replace(target, placeholder);
for (String src : sources) s = s.replace(src, target);
s = s.replace(placeholder, target);
}// 语气词清理:"请/麻烦/谢谢"等
for (String tone : TONE_WORDS) {
s = s.replaceAll("(^|\s|[,。!?])" + Pattern.quote(tone) + "($|\s|[,。!?])", "$1$2");
}
s = s.replaceAll("\s+", " ").trim();
return s.trim();
}
槽位抽取 — parseDraftFromText(同时抽取多个字段):
public static Draft parseDraftFromText(String rawText) {
if (rawText == null || rawText.isEmpty()) return null;
Draft draft = new Draft();
String text = rawText.trim();
String normalized = normalize(text);// 1) 意图/类型
String type = detectType(normalized);
if (type != null) draft.type = type;// 2) 多槽位并行抽取(顺序无关)
extractPriority(text, draft); // 紧急/高/中/低
extractFrequency(text, draft); // 日/周/月/季度/半年/年(仅计划用)
extractAssignTo(text, draft); // 派给XXX
extractLocation(text, draft); // 位置
extractDeviceType(text, draft); // 设备类型(消防/空调/电梯...)
extractDeviceName(text, draft); // 设备名(空调/水泵/烟感...)
extractDates(text, draft); // 日期(2026-06-15 / 明天)
extractDescription(text, draft); // 故障描述(漏水、异响...)return draft;
}
多轮修改 — applyModification(支持"位置改成5楼"这种增量补充):
public static boolean applyModification(String text, Draft draft) {
if (text == null || draft == null) return false;
String raw = text.trim();
boolean modified = false;// 优先级模式:"优先级改成高" / "级别设为紧急"
Pattern[] priPatterns = {
Pattern.compile("(?:优先级|级别|紧急程度)\s(?:改成|改为|是|设为|::)\s(紧急|高|中|低)"),
Pattern.compile("(?:改|修改|调整)?\s(?:优先级|级别)\s(?:为|::)?\s*(紧急|高|中|低)")
};
for (Pattern p : priPatterns) {
Matcher m = p.matcher(raw);
if (m.find()) { draft.priority = m.group(1); modified = true; }
}// 指派人模式:"派给张三" / "负责人是李四"
Pattern assignP = Pattern.compile(
"(?:派给|分配给|交给|指派给|指派人|负责人|由)\s(?:是|::|为)?\s([\u4e00-\u9fa5A-Za-z]{2,8})");
Matcher m = assignP.matcher(raw);
if (m.find()) { draft.assignTo = m.group(1).trim(); modified = true; }// ...位置、设备类型、设备名称、频率、日期等槽位同理
return modified;
}
KimiAIService.java — 当 NLParser 未识别为工单指令时,调用大模型回复:
public class KimiAIService {
private static final String API_KEY = "sk-hnYZ0Su2qjhZDz4hGNxcr2ukqP51dCGXU7E4CuBblAA87iuH";
private static final String API_URL = "https://api.moonshot.cn/v1/chat/completions";public static String chat(String systemPrompt, String userMessage) {
// 自动注入当前日期,让 AI 知道"今天"是哪一天
String today = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy年MM月dd日"));
String now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
String fullSystem = (systemPrompt == null ? "" : systemPrompt)
+ "\n【当前系统时间】" + now
+ "\n【重要】当前真实日期是 " + today;return rawChat(fullSystem, userMessage);
}private static String rawChat(String systemPrompt, String userMessage) {
try {
URL url = new URL(API_URL);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/json");
conn.setRequestProperty("Authorization", "Bearer " + API_KEY);
conn.setDoOutput(true);// 构造 OpenAI 兼容的 Chat Completions 请求体
String requestJson = "{"model":"moonshot-v1-8k","messages":["
+ "{"role":"system","content":"" + escapeJson(systemPrompt) + ""},"
+ "{"role":"user","content":"" + escapeJson(userMessage) + ""}"
+ "],"temperature":0.3}";try (OutputStream os = conn.getOutputStream()) {
os.write(requestJson.getBytes(StandardCharsets.UTF_8));
}BufferedReader reader = new BufferedReader(
new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8));
StringBuilder response = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) response.append(line);
reader.close();return extractContent(response.toString());
} catch (Exception e) {
e.printStackTrace();
return "AI助手暂时不可用:" + e.getMessage();
}
}
} - 其他(纯对话) */

浙公网安备 33010602011771号