冲刺博客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();
    }
    }
    }

posted @ 2026-06-19 11:37  lagranSun  阅读(7)  评论(0)    收藏  举报