AgentScope Java 源码深读:一次请求如何触发工具、Middleware 和子 Agent

很多 Agent 框架文章会把流程讲成一句话:用户输入进来,模型思考,需要工具就调用工具,需要分工就调用子 Agent。这个说法没错,但太粗了。真正读源码时,关键问题不是"有没有子 Agent",而是:

  • 子 Agent 是什么时候注册的?
  • 模型为什么能看到 agent_spawn
  • agent_spawn 被调用后,Java 里到底是谁创建 child Agent?
  • 流式模式下,child Agent 的事件怎么回到父 Agent?
  • 2.0 里 middleware 到底插在请求生命周期的哪一层?

本文按源码调用链拆。结论先说清楚:AgentScope Java 不是在用户请求进来时立刻创建子 Agent,而是先在构建期注册子 Agent 工具和 factory;每轮 reasoning 前,middleware 把可用子 Agent 和使用规则注入 system;模型输出 agent_spawnagent_send 后,acting 阶段才通过 Toolkit 调到 AgentSpawnTool,再由 DefaultAgentManager 用 factory 创建或复用 child Agent。

先看整体流程

读这张小图时,先抓四条线。

第一,构建期只注册能力,不创建 child Agent。Builder.build 准备 middleware、manager、tool 和 factory,真正实例化要等到 agent_spawn

第二,2.0 的主扩展点是 MiddlewareChainonAgent 包住一次 Agent 调用,onReasoning 影响模型输入,onActing 影响工具执行。

第三,reasoning 阶段只让模型看见子 Agent:middleware 注入 system 说明,buildSchemas 暴露 agent_spawn / agent_send

第四,真正执行发生在 acting -> Toolkit.callTools -> AgentSpawnTool。这里才会通过 DefaultAgentManagerSubagentFactory 创建或复用 child Agent。

完整时序图我放在文末。它更适合放大看,或者作为文章最后的"完整链路复盘"。

1. 构建期:子 Agent 先变成工具和 factory

源码入口在 agentscope-harness/src/main/java/io/agentscope/harness/agent/HarnessAgent.javaBuilder.build()

核心判断是这段逻辑:

scss 复制代码
if (!leafSubagent && !disableSubagents && model != null) {
    inner.middleware(dynMw);
    for (Object t : dynMw.getTools()) {
        agentToolkit.registerTool(t);
    }
}

这里有三个信息很关键。

第一,leafSubagent 会阻止继续安装子 Agent middleware。这是为了避免声明式 child Agent 默认继续无限派生下去。源码里 buildDeclaredFactory 创建 child agent 时会调用 .asLeafSubagent(),所以默认 child 是叶子。

第二,inner.middleware(...)agentToolkit.registerTool(...) 是两件事。middleware 负责在生命周期里注入信息;Toolkit 负责让模型看到并执行工具。少了 middleware,模型不知道什么时候该用子 Agent;少了工具注册,模型即使想用也没有 agent_spawn 入口。

第三,此时还没有真正创建 child Agent。这里创建的是"可创建 child Agent 的能力",也就是 SubagentEntrySubagentFactory

2. buildSubagentEntries:把声明文件变成注册表

源码位置:HarnessAgentBuilderSupport.buildSubagentEntries(...)

它把子 Agent 来源合成一个列表:

less 复制代码
entries.add(new SubagentEntry("general-purpose", ...));
allDeclarations.addAll(AgentSpecLoader.loadFromDirectory(subagentsDir, resolvedWorkspace));
entries.add(new SubagentEntry(decl.getName(), decl.getDescription(), buildDeclaredFactory(...), decl));

这解释了为什么 general-purpose 总是一个特殊默认项。它不是从 subagents/*.md 读出来的,而是框架构建时直接加入的默认子 Agent。

声明式子 Agent 则来自两个地方:

  • builder 里显式传入的 SubagentDeclaration
  • workspace 下的 subagents/*.md

每个声明最后会被包装成 SubagentEntry。这个 entry 里最重要的不是描述,而是 SubagentFactory。因为 AgentSpawnTool 后面真正创建 child Agent 时,调用的就是这个 factory。

3. buildDeclaredFactory:真正的 child Agent 构造蓝图

buildDeclaredFactory(...) 返回的是一个 lambda,签名等价于:

scss 复制代码
RuntimeContext parentRc -> {
    HarnessAgent.Builder sub = HarnessAgent.builder()
        .name(decl.getName())
        .model(effectiveModel)
        .toolkit(allowlistedInheritedToolkit(...))
        .workspace(runtimeWorkspace)
        .defaultSessionId(childSessionId)
        .asLeafSubagent();
    return sub.build();
}

这段是源码级理解子 Agent 的关键:child Agent 不是一个轻量函数调用,而是一个新的 HarnessAgent。它会继承或派生父 Agent 的模型、工具、workspace、状态存储、执行超时、plan mode、skills 和 middlewares。

但它不是无条件继承全部能力。allowlistedInheritedToolkit(...) 会按声明里的 tools allowlist 过滤工具;deriveChildSessionId(...) 会把 parent runtime context 纳入 child session id,避免不同用户或父会话串状态。

所以子 Agent 的设计不是"新开一个模型请求"这么简单,而是"用父 Agent 配置派生出一个隔离的 child Agent 实例"。

4. 请求期:MiddlewareChain 让子 Agent 出现在模型上下文里

一次请求进入 ReActAgent.buildAgentStream 后,源码会构造 middleware 链:

arduino 复制代码
MiddlewareChain.build(
    middlewares,
    ReActAgent.this,
    rc,
    MiddlewareBase::onReasoning,
    reasoningCore
).apply(new ReasoningInput(modelInput, tools, options));

这里不要把重点放在"调用模型"上。模型调用只是 reasoningCore,外面包了一层 MiddlewareChain.onReasoning。子 Agent 的注入就发生在这层。

SubagentsMiddleware.onReasoning(...) 的核心流程是:

scss 复制代码
List<SubagentEntry> currentEntries = snapshotFor(rc).entries();
addition.append(renderSubagentSection(currentEntries, isSessionMode));
List<Msg> rebuilt = prependToSystemMessage(input.messages(), addition.toString());
return next.apply(new ReasoningInput(rebuilt, input.tools(), input.options()));

它做了两件事:

  1. 把当前可用子 Agent 渲染成 system prompt 片段,告诉模型有哪些 agent_id,什么时候该用。
  2. 把异步任务摘要也注入进去,让模型知道后台子任务是否完成。

如果使用 DynamicSubagentsMiddleware,它会在每轮 onReasoning 里重新扫描:

ini 复制代码
List<SubagentEntry> merged = reloadEntries(rc);
agentManager.replaceAgents(merged);

这就是动态子 Agent 的关键。你改了 subagents/*.md,下一轮 reasoning 可以重新加载,不需要重启整个 Agent。

5. buildSchemas:模型看到的是工具 schema,不是 Java 对象

仅有 system prompt 还不够。模型要能输出工具调用,还需要模型 API 收到 tool schema。

源码位置:agentscope-core/src/main/java/io/agentscope/core/tool/ToolSchemaProvider.java

核心逻辑很短:

scss 复制代码
if (groupManager.isGroupedTool(toolName) && !activeTools.contains(toolName)) {
    continue;
}
ToolSchema schema = ToolSchema.builder()
    .name(toolName)
    .description(tool.getDescription())
    .parameters(registered.getExtendedParameters())
    .build();

这一步把 AgentSpawnTool.agentSpawn 这种 Java 方法,转换成模型能理解的 agent_spawn schema。agent_sendagent_listtask_output 等也一样。

所以模型"自动调用子 Agent"的前提其实是两层同时成立:

  • middleware 在 system prompt 中告诉模型什么时候用
  • Toolkit 把 agent_spawn / agent_send 暴露成工具 schema

前者解决"该不该用",后者解决"能不能调用"。

6. acting:tool_use 进入 Toolkit,不是特殊分支

模型输出 tool_use 后,ReActAgent 进入 acting。源码里关键方法是 runToolBatch(...)dispatchToolCalls(...)

主流程是:

scss 复制代码
runToolBatch(toolCalls, deniedIds, replyId, resultHolder)
dispatchToolCalls(toolCalls)
toolkit.callTools(toolCalls, toolExecutionConfig, ReActAgent.this, runtimeContext)

这里子 Agent 没有被当成框架旁路处理。它和普通工具一样,先进入 Toolkit.callTools(...),再由 ToolExecutor.executeCore(...) 做工具查找、参数校验、上下文合并和真实调用。

反射工具的最后一跳是:

scss 复制代码
ReflectiveFunctionTool.callAsync(...)
methodInvoker.invokeAsync(toolObject, method, param, customConverter)

因此,agent_spawn 的本质就是一个带 @Tool(name = "agent_spawn") 的 Java 方法。模型输出工具名和参数,Toolkit 负责把它转成 Java 方法调用。

7. agent_spawn:真正创建 child Agent 的地方

源码位置:AgentSpawnTool.agentSpawn(...)

它不是直接 new 一个 child,而是先拿本轮 runtime context 里的 manager:

ini 复制代码
DefaultAgentManager manager = managerFor(runtimeContext);
Optional<Agent> agentOpt = manager.createAgentIfPresent(agentId, runtimeContext);

这里的 managerFor(runtimeContext) 很重要。SubagentsMiddleware.installSnapshot(...) 会把当前快照里的 DefaultAgentManager 放进 RuntimeContext

ini 复制代码
runtimeContext.put(AgentSpawnTool.CTX_AGENT_MANAGER, snapshot.agentManager());

也就是说,AgentSpawnTool 不一定用构造时那个固定 manager,而是优先用本轮上下文里的 manager。这解决了动态 reload 和多用户隔离问题。

接下来 agentSpawn 会处理几个关键分支:

  • MAX_SPAWN_DEPTH:限制递归创建深度。
  • persistSession:如果声明要求持久会话,则根据 parent session、agent id、label 生成稳定 key。
  • labelToKey:如果传了 label,后续 agent_send 可以按 label 找回。
  • timeout_seconds=0:直接提交到 TaskRepository,返回 task_id
  • remote declaration:走远程 subagent 调用。
  • local declaration:走 execWithTimeoutPromotion(...)

这也是为什么 agent_spawn 返回结果里有 agent_key。后续要继续给同一个 child 发消息,不能靠 agent_id,要靠 agent_key 或 label。

8. DefaultAgentManager:创建逻辑被压到 factory

DefaultAgentManager.createAgentIfPresent(...) 的逻辑非常克制:

kotlin 复制代码
SubagentFactory factory = agentFactories.get(agentId);
if (factory == null) return Optional.empty();
if (decl != null && decl.getMode() == PRIMARY) return Optional.empty();
return Optional.of(factory.create(parentRc != null ? parentRc : RuntimeContext.empty()));

它只做三件事:

  1. agent_id 是否存在。
  2. 拒绝 PRIMARY only 的声明被当成 subagent spawn。
  3. 调用 factory.create(parentRc)

这是一种很干净的职责划分。DefaultAgentManager 不关心 child Agent 怎么构建,不关心 workspace、model、skills 怎么继承,也不关心它是 remote 还是 local。这些复杂度都被封装在 SubagentFactory 或声明对象里。

9. execLocalSync:子 Agent 的事件如何回到父 Agent

创建 child 后,如果是同步本地执行,会进入 execWithTimeoutPromotion(...),再进入 execLocalSync(...)

execLocalSync 有三条路径:

  • streamEvents 路径:Reactor Context 里有 AgentEventEmitter,child events 会被打上 source 后转发给父流。
  • stream 兼容路径:有 SubagentEventBus 时通过 event bus 转发。
  • 非流式路径:直接 invokeAgent(...),不转发中间事件。

核心代码可以概括成:

ini 复制代码
AgentEventEmitter taggedEmitter =
    event -> parentEmitter.emit(event.withSource(sourcePath));
​
return manager.invokeAgent(agent, sessionId, userId, prompt, parentCtx)
    .contextWrite(c -> c.put(AgentEventEmitter.FORWARDING_CONTEXT_KEY, taggedEmitter));

这解释了一个容易漏掉的点:子 Agent 的输出不仅是最终文本,还可以作为父 Agent 事件流的一部分被 UI 或调用方观察到。sourcePath 是关键,它让外层消费者知道某个事件来自哪个 child。

10. agent_send:复用已有 child,不重新按 agent_id 创建

agent_send 的入口是 AgentSpawnTool.agentSend(...)

它不接收 agent_id,而是接收:

  • agent_key
  • 或 spawn 时设置的 label

源码会先解析 key:

ini 复制代码
key = labelToKey.get(label.trim().toLowerCase());
SpawnedAgent resolved = agentsByKey.get(key);
if (resolved == null) {
    resolved = tryRestoreFromState(parentState, key, runtimeContext);
}

这说明 agent_send 语义是"给已存在的 child session 继续发消息",不是"按类型再创建一个 child"。如果找不到内存里的 SpawnedAgent,还会尝试从父 AgentState 恢复。

11. 异步任务:子 Agent 可以先返回 task_id

timeout_seconds=0,或者同步等待超时时,执行会进入后台任务路线。

典型路径是:

ini 复制代码
String taskId = "task_" + UUID.randomUUID();
taskRepository.putTask(runtimeContext, taskId, agentId, parentSessionId, spec);

后续模型可以通过 TaskTool 查询结果。但更有意思的是,SubagentsMiddleware.onReasoning(...) 会在下一轮 reasoning 前构建 task summary,完成的任务还可以作为 system reminder 推回模型上下文。

所以异步子 Agent 不是"丢到后台就没人管"。它有任务仓库、查询工具、下一轮摘要注入三件套。

12. agent_generate:自动生成新子 Agent 声明,但默认不开

源码里还有 AgentGenerateTool.agentGenerate(...),它可以根据自然语言描述生成新的 subagents/name.md

但它不是默认注册的。SubagentsMiddleware.enableAgentGenerateTool(...) 的注释已经说明:生成器需要模型,还会写 workspace,所以必须显式开启。

这条链路是:

rust 复制代码
agent_generate -> SubagentSpecGenerator.generateAndValidate -> filesystem.write(subagents/name.md) -> 下一轮 DynamicSubagentsMiddleware reload -> agent_spawn 可见

所以"自动创建子 Agent"有两种含义,必须区分:

  • 创建 child Agent 实例:agent_spawn 调用 DefaultAgentManager.createAgentIfPresent
  • 创建新的子 Agent 声明文件:可选的 agent_generate 写入 subagents/*.md

前者是主链路,后者是可选能力。

设计理念:把智能决策和工程边界分开

这套实现最值得学的地方,不是"支持子 Agent"本身,而是边界分得很清楚。

模型负责选择:模型通过 system prompt 和 tool schema 知道有哪些能力,决定是否输出 agent_spawnagent_send

Middleware 负责注入上下文:SubagentsMiddlewareDynamicSubagentsMiddleware 在 reasoning 前把子 Agent 清单、使用规则、任务摘要放进本轮输入。

Toolkit 负责执行工具:模型输出的是 tool_use,Toolkit 做 schema 校验、上下文合并、并发控制和真实方法调用。

Manager 负责实例化:DefaultAgentManager 只做 agent_id 到 factory 的查找和创建,不把构建细节揉进去。

Factory 负责派生 child:buildDeclaredFactory 捕获父配置,构建继承但隔离的 child HarnessAgent。

TaskRepository 负责异步结果:后台任务不污染主推理循环,但能在后续 reasoning 里被重新注入。

这种分层让代码不至于变成"模型调用里到处 new Agent"。每一层都只做一件事,这也是 Agent 框架能扩展到多 Agent 协作时最重要的工程前提。

源码索引

  • agentscope-harness/src/main/java/io/agentscope/harness/agent/HarnessAgent.javaBuilder.build 安装 subagent middleware 和工具。
  • agentscope-harness/src/main/java/io/agentscope/harness/agent/HarnessAgentBuilderSupport.javabuildSubagentEntriesbuildDeclaredFactory
  • agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/SubagentsMiddleware.javaonAgentonReasoningrenderSubagentSectioninstallSnapshot
  • agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/DynamicSubagentsMiddleware.java:每轮 reload 子 Agent 声明。
  • agentscope-core/src/main/java/io/agentscope/core/tool/ToolSchemaProvider.javabuildSchemas(activeTools)
  • agentscope-core/src/main/java/io/agentscope/core/ReActAgent.javareasoningrunToolBatchdispatchToolCalls
  • agentscope-core/src/main/java/io/agentscope/core/tool/Toolkit.javacallTools
  • agentscope-core/src/main/java/io/agentscope/core/tool/ToolExecutor.javaexecuteCoreexecuteAll
  • agentscope-harness/src/main/java/io/agentscope/harness/agent/tool/AgentSpawnTool.javaagentSpawnagentSendexecLocalSync
  • agentscope-harness/src/main/java/io/agentscope/harness/agent/subagent/DefaultAgentManager.javacreateAgentIfPresentinvokeAgent
  • agentscope-harness/src/main/java/io/agentscope/harness/agent/tool/AgentGenerateTool.java:可选的 agent_generate

最后一层理解

如果只记一个句子,可以这样概括 AgentScope Java 的子 Agent 机制:

Builder 注册 factory 和工具,Middleware 让模型知道什么时候用,ToolSchema 让模型能调用,Toolkit 把 tool_use 变成 Java 方法调用,AgentSpawnTool 找 manager,DefaultAgentManager 调 factory 创建 child,child 的结果作为 tool_result 回到父 Agent 的下一轮 reasoning。

这才是源码里的完整闭环。

附:完整请求生命周期时序图

上面的局部图是为了读到哪一段源码,就只看那一段关系。下面这张图把构建期、请求入口、middleware、reasoning、工具执行、agent_spawnagent_send 和异步任务收回到一条主线。建议在读完正文后再看。

相关推荐
ckjoker1 小时前
我把Java多模态链路从0跑通了,结果先被4个坑狠狠干了一顿
后端·agent
瑞码空间2 小时前
Routing & API:前后端协作的本质与实现
前端·后端·接口·路由
SomeB1oody2 小时前
【RustyML入门】2.10. 主成分分析
开发语言·后端·机器学习·rust·教程
我真是泰库辣2 小时前
用TraeWork制作应用 —— 从 0 到 1 · 手把手搭建 opencode 网页对话网关
前端·后端
亚雷2 小时前
图解分布式架构:一个 Console,一群 Sidecar
后端·面试·程序员
篮框坏了2 小时前
配置改了不生效?我翻出 8 个坑,没一个报错
后端·spring cloud
云边有个稻草人2 小时前
KingbaseES多模架构下的MongoDB迁移方案
后端
水深火乐2 小时前
为什么map不能声明为const
后端