gogo-agent

快速路由到上次执行的子智能体

多智能体系统里,一句完整的用户请求要先后经过"问题改写 → 意图识别 → MasterAgent 协调 → 子智能体执行"多级 LLM 推理。但真实对话中,用户在一个子智能体的处理过程里往往会连续追问或确认,比如:

TL;DR

多智能体系统里,一句完整的用户请求要先后经过"问题改写 → 意图识别 → MasterAgent 协调 → 子智能体执行"多级 LLM 推理。但真实对话中,用户在一个子智能体的处理过程里往往会连续追问或确认,比如:

多智能体系统里,一句完整的用户请求要先后经过"问题改写 → 意图识别 → 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

版本提示

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

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

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