"意图明确"是直跳的必要条件,但不是充分条件。 在本方案里,只有同时满足三条,才会跳过 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接管。