多智能体系统里,一句完整的用户请求要先后经过"问题改写 → 意图识别 → MasterAgent 协调 → 子智能体执行"多级 LLM 推理。但真实对话中,用户在一个子智能体的处理过程里往往会连续追问或确认,比如:
用户:帮我查一下杭州的差旅政策 → InfoAgent 处理
用户:那上海呢 → 明显还是问政策,却又要重新走一遍改写+意图+Master
用户:确认 → 只是一个确认信号,没必要再全量路由
如果每一句都从头跑完整链路,既浪费 token 和延迟,还可能因为意图识别把"确认""那上海呢"这种碎片句误判到别的 Agent,导致对话上下文断裂。
快速路由机制就是为此设计:记住"上一轮是哪个子智能体在干活",当这一轮的用户消息是一个"继续信号"时,直接把消息喂给那个子智能体,跳过问题改写、意图识别和 MasterAgent 三层。
整体方案如下:

记录侧:活跃 Agent 如何被记住
时机——每轮推理前的 PreReasoning Hook
ActiveAgentPersistenceHook 监听 PreReasoningEvent(每个 Agent 每次 LLM 推理之前触发),把当前正在推理的 Agent 名字写入 Session:
public <T extends HookEvent> Mono<T> onEvent(T event) {
if (!(event instanceof PreReasoningEvent)) {
return Mono.just(event);
}
return Mono.deferContextual(ctx -> {
String sessionId = ctx.getOrDefault("sessionId", null); // 从 Reactor Context 取 sessionId
if (sessionId == null) { return Mono.just(event); }
String agentName = event.getAgent() != null ? event.getAgent().getName() : null;
activeAgentSessionStore.setActiveAgent(sessionId, agentName);
return Mono.just(event);
});
}
这个 Hook 被挂载到几乎所有有状态的 Agent 上(BaseSubAgent 统一注入,MasterAgent / IntentRecognition / QueryRewriting 等也显式列入)。 一轮对话里 QueryRewriting → IntentRecognition → Master → 某子智能体会依次触发 PreReasoning,每次都覆盖存储值。所以最后一个真正推理的 Agent 会"胜出"——这恰好就是"处理了上一轮的那个子智能体",无需任何额外逻辑去判断"谁是最后一个"。
存储——Session 持久化,集群安全
ActiveAgentSessionStore 封装读写:
private static final String ROUTER_SESSION_KEY_SUFFIX = ":router";
private static final String ACTIVE_AGENT_FIELD = "activeAgent";
public String getActiveAgent(String sessionId) {
try {
return session.get(buildSessionKey(sessionId), ACTIVE_AGENT_FIELD, ActiveAgentState.class)
.map(ActiveAgentState::agentName).orElse(null);
} catch (Exception e) { // 任何异常都降级为 null
return null;
}
}
private SimpleSessionKey buildSessionKey(String sessionId) {
return SimpleSessionKey.of(sessionId + ROUTER_SESSION_KEY_SUFFIX);
}
存储要点: - Key 格式:{sessionId}:router。刻意加 :router 后缀,与各 Agent 自己的记忆 key({sessionId}:{AgentName})隔离,互不干扰。 - Value:ActiveAgentState record,只含一个 agentName(PascalCase 的 Agent 名,如 "InfoAgent")。 - 底层:走 AgentScope 的 Session 抽象,实际实现是 MysqlSession(库 gogo_travel、表 agentscope_session)。用持久化而非本地内存,是为了集群环境下任意节点都能读到最新活跃 Agent。 - 无 TTL:不设过期,靠"每轮被覆盖"来自然更新。
读取侧:下一轮如何决定抄近路
入口判断——两个与条件
ChatController.chat() 在处理新消息的最前面就做决策:
String activeAgent = activeAgentSessionStore.getActiveAgent(sessionId);
if (activeAgent != null && isContinuation(message)) {
ReActAgent targetAgent = agentRegistry.getAgent(activeAgent);
agentExecutor.executeAgent(targetAgent, inputMessages, message, emitter, sessionId, userId);
return emitter;
}
// 新会话、切换话题或未命中 continuation:由 MasterAgent 协调
agentExecutor.executeAgent(null, inputMessages, message, emitter, sessionId, userId);
命中快速路由需同时满足两个条件:存在活跃 Agent(activeAgent != null),且这句话是"继续信号"(isContinuation)。
继续信号判定——精确匹配 + 话题切换否决
private boolean isContinuation(String message) {
if (message == null || message.isBlank()) return false;
String lower = message.toLowerCase();
// 话题切换词一票否决,强制走 MasterAgent
if (TOPIC_SWITCH_SIGNALS.stream().anyMatch(lower::equals)) return false;
// 精确匹配继续信号词表
return ContinuationSignals.ALL.stream().anyMatch(lower::equals);
}
- ContinuationSignals.ALL:确定/确认/提交/继续/是的/好的/对/好/修改/补充/取消/重新/再/ok/yes/confirm/continue…
- TOPIC_SWITCH_SIGNALS:另外/换个/换一个/不想/不要这个/重新申请/重新提交/另外一件事 这里用的是 equals 精确匹配而非 contains——这是一个刻意的保守设计。只有当用户明确输入这些简短的继续/确认词时才抄近路;一旦是带实质内容的新句子(可能是新意图),就老老实实回到 MasterAgent 全量协调,宁可多花一次推理也不误路由。话题切换词则直接一票否决。
真正的续接调用——区分"子智能体"与"流水线 Agent"
ChatAgentExecutor.executeAgent() 拿到目标 Agent 后还有一层分流:
if (agent == null) {
execution = agentPipelineService.executeFullPipeline(...); // 全量流水线
} else {
if (PIPELINE_AGENT_NAMES.contains(agent.getName())) { // 活跃的是流水线 Agent
execution = agentPipelineService.executePipeline(..., agent.getName());
} else {
executionRegistry.register(sessionId, agent);
execution = agent.call(inputMessages) // 真·子智能体:直接 call
.doFinally(signal -> executionRegistry.remove(sessionId));
}
}
- PIPELINE_AGENT_NAMES = {QueryRewritingAgent, IntentRecognitionAgent, MasterAgent}。
- 如果上一轮活跃的是真正的业务子智能体(InfoAgent、BookingAgent…),走 agent.call(inputMessages) 直接续接,彻底跳过三层。这就是本功能的核心快路径。
- 如果活跃的恰是流水线里的某个 Agent,则交给 executePipeline 按 Agent 类型再分流(Master → 直接续跑不重识别;IntentRecognition → 重跑意图再分派;QueryRewriting → 回退全量)。
提高命中率
前面提到快路径的命中条件之一是 isContinuation(message),而它采用的是精确匹配固定词表——只有用户输入恰好等于"确定/继续/修改/取消…"这类词才会命中。用户自由打字很难正好落在词表里,命中率天然受限。 这里做了一个关键的产品级设计闭环:每轮助手回复后,由"问题推荐"生成 1~4 个可点击的追问,并刻意让其中的"快速操作项"从继续信号词表里挑选。用户不再靠打字,而是点击推荐的问题,就能精确的匹配上,从而稳定命中快路径。 以下是问题推荐的提示词中关于这段的内容:
### 何时应该用快速操作关键词
- 助手刚生成一个方案、报表、结论,用户接下来可能想**整体确认/否定/修改**时 → 优先用"确定/修改/重新"等
- 用户上一轮在追问补充信息,助手刚给出答复后用户想表示"收到" → 用"好的/继续"
- 助手提供了多个并列选项让用户选其一,但用户其实想"全都不要/都同意" → 用"都不对/都行"
快速操作关键词(节选):
- 确认/继续类:确定、确认、提交、继续、是的、好的...
- 修改/补充类:修改、补充...
- 取消/重新类:不对、取消、重新...
✅如何基于回答更好的问题推荐?
推荐问题不是「凑几个相关问题」,一条好的推荐要同时满足四点: gogo-agent 的实现恰好是围绕这四点设计的。下面逐层拆。 触发时机:主回复完成后,异步补推 推荐问题不在主回复链路里同步生成,而是在主答案推送完、且满足条件时才异步补一 LLMentor
HIL
除了上面这个实现方式,我们还有一种情况也会直接路由到某个智能体执行,那就是某个智能体触发了Human In the Loop的时候,这个我们单独讲。
Human-in-the-Loop — Agent 与用户的结构化交互
... LLMentor