gogo-agent

用户意图很明确时,直接路由给子智能体还是主智能体?

"意图明确"是直跳的必要条件,但不是充分条件。 在本方案里,只有同时满足三条,才会跳过 MasterAgent、把请求直接交给某个子智能体: 1. 单意图(multi intent = false 且 intents 恰好 1 项)…

TL;DR

"意图明确"是直跳的必要条件,但不是充分条件。 在本方案里,只有同时满足三条,才会跳过 MasterAgent、把请求直接交给某个子智能体: 1. 单意图(multi intent = false 且 intents 恰好 1 项)…

"意图明确"是直跳的必要条件,但不是充分条件。 在本方案里,只有同时满足三条,才会跳过 MasterAgent、把请求直接交给某个子智能体: 1. 单意图(multi_intent = false 且 intents 恰好 1 项); 2. 高置信(该意图 confidence = high); 3. 目标子智能体在"可直跳白名单"里(当前白名单只有 infoAgent)。 三条缺任意一条,即使意图再明确,也会退回 MasterAgent 走完整路由。换句话说:明确 ≠ 直跳;明确 + 安全(无副作用)才直跳。 估计很多人会问:意图都高置信、单一了,为什么还要加个白名单拦着不让直跳、非要多花一次 MasterAgent 的推理? 主要因为,用户可能在一个对话中一直聊。如果第一次用户要规划行程,意图识别结果是规划,直接路由给行程规划Agent了,他自己确实可以把活都干了。这个流程没问题。 但是,他返回之后,用户说,那你帮我做预订吧。这时候意图识别到预定,会直接路由给预定Agent,但是,预定Agent是没有任何上下文的。他根本不知道之前你和行程规划Agent说了啥,行程规划的结果是啥。 解决这个问题,有两个做法: 1、预定Agent先去读一遍前面的行程规划Agent的历史Message或者Session 2、对于这种有先后依赖需要上下文感知的,交由Master做路由,由Master做上下文传递。 第一个方案处理起来比较麻烦,并且容易因为上下文不足而导致丢失信息,影响最终的输出结果。而且我们很难控制到底要啥时候去加载这个历史消息。。。 所以,我们选择的是第二个方案。但是,对于一些特殊的agent,比如像InfoAgent这种比较独立的,我们可以把他加到白名单,如果是意图识别的结果是这个的话,那么可以考虑直接跳。

两条路由路径

用户消息经过"改写 + 意图识别"后,AgentPipelineService.dispatchByIntent 面临一个二选一:

意图识别结果 JSON
        │
        ▼
 tryPlanDirectDispatch(intentJson)   ← 判定能否直跳
        │
   ┌────┴─────────────┐
   │ 满足三条件         │ 不满足(任意一条)
   ▼                   ▼
直跳子智能体          走 MasterAgent
dispatchSubAgentDirectly   masterAgent.call(...)
(跳过 MasterAgent)    (统一路由 / 编排 / 整合)

对应代码:

private Mono<Msg> dispatchByIntent(String intentJson, List<Msg> inputMessages,String contextQuestion, String sessionId, String userId) {
    Optional<DirectDispatchPlan> planOpt = tryPlanDirectDispatch(intentJson);
    if (planOpt.isPresent()) {
        // 路径 A:单意图 + 高置信 + 命中白名单 → 直连子智能体,跳过 MasterAgent
        DirectDispatchPlan plan = planOpt.get();
        logger.info("[PIPELINE] 单意图高置信直跳:{} (intent={}, confidence={}),跳过 MasterAgent",
                plan.beanName(), plan.intentCode(), plan.confidence());
        return dispatchSubAgentDirectly(plan, inputMessages, contextQuestion, sessionId, userId);
    }

    // 路径 B:其余一律走 MasterAgent 路由
    ReActAgent masterAgent = agentRegistry.getAgent(MASTER_AGENT_NAME);
    return masterAgent.call(buildMasterInput(inputMessages, contextQuestion, intentJson))
            .doOnSubscribe(s -> executionRegistry.register(sessionId, masterAgent))
            .flatMap(masterResult -> handleMasterResult(masterResult, sessionId));
}

这是整套决策的核心。逐条看它如何"层层设卡":

private Optional<DirectDispatchPlan> tryPlanDirectDispatch(String intentJson) {
    if (intentJson == null || intentJson.isBlank()) return Optional.empty();
    try {
        JSONObject obj = JSON.parseObject(extractJsonBlock(intentJson));
        if (obj == null) return Optional.empty();

        // —— 门槛①:必须是单意图 ——
        // 多意图含跨子智能体编排与依赖,没有单个子智能体能独立完成,交给 MasterAgent
        if (Boolean.TRUE.equals(obj.getBoolean("multi_intent"))) {
            logger.debug("[PIPELINE] multi_intent=true,不走直跳");
            return Optional.empty();
        }
        JSONArray intents = obj.getJSONArray("intents");
        if (intents == null || intents.size() != 1) return Optional.empty(); // 冗余保险

        JSONObject primary = intents.getJSONObject(0);
        if (primary == null) return Optional.empty();
        String targetAgent = primary.getString("target_agent");
        String confidence  = primary.getString("confidence");
        String intentCode  = primary.getString("intent");

        // —— 门槛②:必须高置信 ——
        // 中/低置信度即便直跳也可能召错 Agent(如把行程规划误判成机票查询),
        // 多一次 MasterAgent 的 LLM 推理,远比一次错误路由便宜
        if (!IntentRecognitionResult.Confidence.HIGH.name().equalsIgnoreCase(confidence)) {
            logger.info("[PIPELINE] 单意图 confidence={} 非 high,保守起见仍走 MasterAgent (intent={})",
                    confidence, intentCode);
            return Optional.empty();
        }

        // —— 门槛③:target_agent 必须命中"可直跳白名单" ——
        // 用白名单而非 containsBean:masterAgent 本身也是 ReActAgent bean,
        // 单纯 containsBean 会把"跳过 Master 又路由回 Master"的退化路径放行
        String bean = Character.toLowerCase(targetAgent.charAt(0)) + targetAgent.substring(1);
        if (targetAgent == null || !DISPATCHABLE_BEAN_NAMES.contains(bean)) {
            logger.info("[PIPELINE] target_agent={} 不可直跳(不在子智能体白名单中),走 MasterAgent", targetAgent);
            return Optional.empty();
        }

        return Optional.of(new DirectDispatchPlan(targetAgent, intentCode, confidence, targetAgent));
    } catch (Exception e) {
        // 解析失败同样降级 MasterAgent —— 防御式兜底
        logger.warn("[PIPELINE] 解析意图 JSON 失败,降级到 MasterAgent: {}", e.getMessage());
        return Optional.empty();
    }
}

而白名单当前只放了一个:

/** 可直跳的子智能体 */
private static final Set<String> DISPATCHABLE_BEAN_NAMES = Set.of("infoAgent");

这意味着:当前只有信息查询类意图(policy_query / attractions_query / general_info → infoAgent)在"单意图 + 高置信"时才会直跳;其余所有子智能体(行程管理、行程规划、预订等)即使意图明确,也一律经 MasterAgent。

infoAgent为啥可以直接跳

为了避免上下文丢失,我们会尽可能走MasterAgent,为啥InfoAgent特殊呢? 主要有以下几种情况: 情况1: 第一次对话 ----> L1/L2意图识别命中 ----> InfoAgent执行 第二次对话 ----> L1/L2意图识别未命中 ----> 问题改写 ----> L1/L2意图识别命中 ----> InfoAgent执行 这种情况没问题,因为InfoAgent有记忆。 情况2: 第一次对话 ----> L1/L2意图识别命中 ----> InfoAgent执行 第二次对话 ----> L1/L2意图识别未命中 ----> 问题改写 ----> L3意图识别命中 ----> InfoAgent执行 这种情况没问题,因为InfoAgent有记忆。 情况3: 第一次对话 ----> L1/L2意图识别命中 ----> InfoAgent执行 第二次对话 ----> L1/L2意图识别未命中 ----> 问题改写 ----> L3意图识别命中 ----> MasterAgent执行 这种情况没问题,因为有问题改写,问题改写会做多轮对话的融合及指代消除 情况4: 第一次对话 ----> L1/L2意图识别命中 ----> InfoAgent执行 第二次对话 ----> L1/L2意图识别未命中 ----> 问题改写 ----> L1/L2意图识别命中 ----> MasterAgent执行 这种情况没问题,因为有问题改写,问题改写会做多轮对话的融合及指代消除 情况5: 第一次对话 ----> L1/L2意图识别命中 ----> InfoAgent执行 第二次对话 ----> L1/L2意图识别命中 ----> MasterAgent执行 这是唯一可能有问题的情况,这时候Master直接用用户当前轮对话做回答,可能会丢失历史上下文信息。 InfoAgent要处理的一般都是信息查询,职责比较单一明确。查个天气,查个新闻,查询一下政策啥的(如果是查询酒店、机票相关则不是InfoAgent的职责)。 这种情况其实在实际的业务中发生的概率是比较低的,一般不太有其他的Agent会依赖InfoAgent做前置处理。 一般来说我们都是先确定行程,然后再查询相关信息的。而先查询信息,在做行程管理、规划等等概率比较低。一旦真的遇到了,那就让用户补充下上下文吧。(手动狗头) 综上内容,再结合我们认为InfoAgent的查询是个高频操作,应该缩短链路,所以我们针对InfoAgent这个比较独立的Agent做了个特殊的直接路由。当然也根据业务实际情况,如果你的Agent中没有这种比较独立的,那么就不要做直接路由,全都让Master接管。

版本提示

模型、框架与接口会持续变化。涉及版本号、参数与生产配置时,请在实践前对照对应官方文档。

LLMentor系统化学习大模型应用工程

内容来自个人课程知识库备份,并经过结构化整理。技术版本持续演进,生产使用前请结合官方文档验证。