MasterAgent 是整个智能差旅系统的总协调者(Orchestrator)。它本身是一个 ReActAgent,但不直接干"查政策、订机票、提审批"这类脏活,而是把各个专业子智能体(ItineraryManageAgent / ItineraryPlanAgent / InfoAgent / BookingAgent)注册成自己的工具,通过 LLM 的推理决定"当前这句话该交给谁处理",再把子智能体的产出整合成统一、连贯的回复返回给用户。
一句话概括它的定位:对上是用户的入口,对下是子智能体的调度中枢。
MasterAgent 处在请求链路的中后段。用户消息先经过"问题改写"和"意图识别"两级预处理,再进入 MasterAgent:
并非所有请求都会走 MasterAgent。当意图识别结果是"单意图 + 高置信 + 命中白名单"时,Pipeline 会走 tryPlanDirectDispatch 快路径直接命中某个子智能体,省掉 MasterAgent 一层 LLM 推理开销;只有多意图、低置信、或需要跨子智能体协调时才交给 MasterAgent。MasterAgent 是"兜底 + 复杂编排"的那条路径。
实现剖析
MasterAgent 用 Spring @Configuration + @Bean(name = "masterAgent") + @Scope("prototype") 定义。prototype 是刻意为之——每次请求都要根据当前会话上下文(用户是谁、哪个 session)重新构建一个 agent 实例,不能单例复用。
会话上下文
AgentSessionContext sessionCtx = AgentSessionContextHolder.get();
String userId = sessionCtx != null ? sessionCtx.getUserId() : null;
ToolExecutionContext masterToolCtx = sessionCtx != null
? ToolExecutionContext.builder().register(sessionCtx).build()
: ToolExecutionContext.empty();
AgentSessionContextHolder 用的是 TransmittableThreadLocal,能自动把上下文传播到 Reactor 的 boundedElastic 线程池,所以子智能体经由 SubAgentProvider 懒加载时无需手动重新 set。这个 sessionCtx 随后被注册进 ToolExecutionContext,所有工具方法都能透明读到。
✅使用TTL实现多智能体之间的上下文传递
在我们的项目中,有一些参数,需要全局传递,比如userId,从用户一进来就确定了,我们需要在多个智能体之间传递、并且在tools、hook中也可能需要用到,那么如何实现这个参数的传递呢。 gogo-agent 用 Transmittable LLMentor
工具装配:子智能体即工具(Agent-as-Tool)
这是 MasterAgent 最核心的部分。除了两个内置工具(InfoQueryTools 通用信息查询、UserInteractionTools 主动提问),它把 4 个子智能体用 SubAgentProvider + SubAgentConfig 注册成可被 LLM 调用的工具:
toolkit.registration()
.subAgent((SubAgentProvider<ReActAgent>) () ->
context.getBean("itineraryManageAgent", ReActAgent.class),
SubAgentConfig.builder()
.toolName("itinerary_manage_agent")
.description("行程单全生命周期管理:收集信息、提交审批、查询差旅单/审批状态、取消/修改申请。参数:message(任务描述),可选 session_id(继续会话)。")
.forwardEvents(false)
.build())
.apply();
// info_agent / itinerary_plan_agent / booking_agent 同理
几个要点: - 懒加载:子智能体通过 SubAgentProvider 的 lambda 在真正被调用时才从 Spring 容器 getBean 取出,配合 prototype 作用域保证每次拿到的都是带当前会话上下文的新实例。 - 跨 Agent 传参靠自然语言:子智能体作为工具,入参只有 message(任务描述)和可选 session_id。父 Agent 的 LLM 把意图序列化成自然语言 message 传下去,
模型选择:强模型推理 + 稳定模型压缩
.model(strongModel) // 主推理用强模型
.memory(AgentMemoryFactory.create(stableModel)) // 历史压缩/摘要用稳定模型
调度决策是"脑力活",用 strongModel;而记忆压缩/摘要是后台辅助,用更便宜稳定的 stableModel,成本与效果分离。
Hook 链:横切能力的挂载点
.hooks(List.of(new AutoContextHook(), executionLoggerHook, progressNotifierHook,
sessionPersistenceHook, activeAgentPersistenceHook, new PendingToolRecoveryHook()))
| Hook | 作用 |
|---|---|
| AutoContextHook | 配合 AutoContextMemory,实际执行压缩/卸载 |
| AgentExecutionLoggerHook | 记录 Agent 执行轨迹,便于排障 |
| ProgressNotifierHook | 向前端推送执行进度 |
| SessionPersistenceHook | 按 |
| ActiveAgentPersistenceHook | 每轮推理前记录当前 session 的活跃 Agent |
| PendingToolRecoveryHook | 为中断/孤立的 ToolUse 注入错误恢复,保证 HITL 稳定 |
执行配置与缓存
.toolExecutionConfig(ExecutionConfig.builder()
.timeout(Duration.ofMinutes(15)).maxAttempts(3).build()) // 工具超时15分钟、重试3次
.maxIters(15) // 最多15轮推理
.generateOptions(GenerateOptions.builder().cacheControl(true).build()) // 开启system+末条消息缓存
子智能体的一次调用可能很耗时(比如规划要搜航班、比价),所以工具超时给到 15 分钟、允许重试 3 次;推理上限 15 轮防止死循环。 cacheControl(true) 让百炼对 system messages 和最后一条消息自动加 cache_control,显著降低多轮长 system prompt 的重复计费。
行为约束:全部写在系统提示词里
MasterAgent 的"性格"由 master-agent-system.md 定义,几条硬规则值得强调: - 禁止重复改写/识别意图:上游已经做好,直接用,避免重复劳动和不一致。 - 禁止在路由前向用户提问:先分派再说,ask_user 只在自己直接处理 greeting/unknown、或子智能体返回后需确认时才用。 - 结果优先透传:子智能体返回的完整方案(含确认问句)原样透传,禁止用自己生成的内容覆盖,尤其是审批摘要、行程方案这类含关键细节的内容。 - 内部名称脱敏:面向用户的文案里绝不出现 snake_case 的工具名/子智能体名/字段名,遗漏的要改写成中文自然语。