MethodRouterService.java 3.79 KB
package com.xly.service;

import cn.hutool.core.util.ObjectUtil;
import com.alibaba.fastjson2.JSON;
import com.xly.agent.SecMethodAiAgent;
import com.xly.agent.SystemPromptGenerator;
import com.xly.entity.MethodChoiceResult;
import com.xly.entity.ToolMeta;
import com.xly.entity.UserSceneSession;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Optional;

@Slf4j
@Service
@RequiredArgsConstructor
public class MethodRouterService {

    private final SecMethodAiAgent aiAgent;

    /** confidence 低于此值视为未命中,走澄清兜底 */
    private static final double CONFIDENCE_THRESHOLD = 0.5;

    /**
     * 根据用户输入路由到具体方法,命中则写入 session.currentTool 并返回该方法。
     *
     * @return 命中的 ToolMeta;未命中返回 null(由上层决定澄清或走通用 chat)
     */
    public ToolMeta route(UserSceneSession session, String userInput,
                          List<ToolMeta> toolList, String sKnowledgeBase) {

        if (ObjectUtil.isEmpty(toolList)) {
            log.warn("方法路由:可选方法列表为空, userId={}", session.getUserId());
            return null;
        }

        // ① 生成方法选择 prompt
        String sSystemPrompt = SystemPromptGenerator
                .generateChoiceMethodPrompt(toolList, sKnowledgeBase);

        // ② 调大模型
        String raw = aiAgent.choiceMethod(session.getUserId(), userInput, sSystemPrompt).content();
        log.debug("方法路由原始输出: {}", raw);

        // ③ 解析(含 ```json 容错)
        MethodChoiceResult result = parse(raw);
        if (result == null || ObjectUtil.isEmpty(result.getSMethodNo())) {
            return null;
        }

        String methodNo = result.getSMethodNo();
        Double confidence = result.getConfidence();

        // ④ none / 阈值 / 白名单三重校验
        if ("none".equalsIgnoreCase(methodNo)) {
            log.info("方法路由:无匹配方法, input={}", userInput);
            return null;
        }
        if (confidence == null || confidence < CONFIDENCE_THRESHOLD) {
            log.info("方法路由:置信度不足({}), input={}", confidence, userInput);
            return null;
        }
        Optional<ToolMeta> hit = toolList.stream()
                .filter(t -> methodNo.equals(t.getSMethodNo()))
                .findFirst();
        if (hit.isEmpty()) {
            log.warn("方法路由:模型返回了不存在的编号 {}", methodNo);  // 防编造
            return null;
        }

        // ⑤ 命中,写入当前工具,交给下游参数提取
        ToolMeta chosen = hit.get();
        session.setCurrentTool(chosen);
        log.info("方法路由命中: {} - {}, confidence={}",
                chosen.getSMethodNo(), chosen.getSMethodName(), confidence);
        return chosen;
    }

    /** 解析模型输出,兼容被 ```json ... ``` 包裹或前后带杂字符的情况 */
    private MethodChoiceResult parse(String raw) {
        if (ObjectUtil.isEmpty(raw)) return null;
        try {
            String json = stripCodeFence(raw);
            return JSON.parseObject(json, MethodChoiceResult.class);
        } catch (Exception e) {
            log.error("方法路由结果解析失败, raw={}", raw, e);
            return null;
        }
    }

    /** 去掉 markdown 代码块,并截取第一个 { 到最后一个 } */
    private String stripCodeFence(String raw) {
        String s = raw.trim()
                .replaceAll("(?s)```json", "")
                .replaceAll("(?s)```", "")
                .trim();
        int start = s.indexOf('{');
        int end = s.lastIndexOf('}');
        return (start >= 0 && end > start) ? s.substring(start, end + 1) : s;
    }
}