很多 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_spawn 或 agent_send 后,acting 阶段才通过 Toolkit 调到 AgentSpawnTool,再由 DefaultAgentManager 用 factory 创建或复用 child Agent。
先看整体流程
读这张小图时,先抓四条线。
第一,构建期只注册能力,不创建 child Agent。Builder.build 准备 middleware、manager、tool 和 factory,真正实例化要等到 agent_spawn。
第二,2.0 的主扩展点是 MiddlewareChain。onAgent 包住一次 Agent 调用,onReasoning 影响模型输入,onActing 影响工具执行。
第三,reasoning 阶段只让模型看见子 Agent:middleware 注入 system 说明,buildSchemas 暴露 agent_spawn / agent_send。
第四,真正执行发生在 acting -> Toolkit.callTools -> AgentSpawnTool。这里才会通过 DefaultAgentManager 和 SubagentFactory 创建或复用 child Agent。
完整时序图我放在文末。它更适合放大看,或者作为文章最后的"完整链路复盘"。
1. 构建期:子 Agent 先变成工具和 factory

源码入口在 agentscope-harness/src/main/java/io/agentscope/harness/agent/HarnessAgent.java 的 Builder.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 的能力",也就是 SubagentEntry 和 SubagentFactory。
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()));
它做了两件事:
- 把当前可用子 Agent 渲染成 system prompt 片段,告诉模型有哪些
agent_id,什么时候该用。 - 把异步任务摘要也注入进去,让模型知道后台子任务是否完成。
如果使用 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_send、agent_list、task_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()));
它只做三件事:
- 查
agent_id是否存在。 - 拒绝
PRIMARYonly 的声明被当成 subagent spawn。 - 调用
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_spawn 或 agent_send。
Middleware 负责注入上下文:SubagentsMiddleware 和 DynamicSubagentsMiddleware 在 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.java:Builder.build安装 subagent middleware 和工具。agentscope-harness/src/main/java/io/agentscope/harness/agent/HarnessAgentBuilderSupport.java:buildSubagentEntries、buildDeclaredFactory。agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/SubagentsMiddleware.java:onAgent、onReasoning、renderSubagentSection、installSnapshot。agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/DynamicSubagentsMiddleware.java:每轮 reload 子 Agent 声明。agentscope-core/src/main/java/io/agentscope/core/tool/ToolSchemaProvider.java:buildSchemas(activeTools)。agentscope-core/src/main/java/io/agentscope/core/ReActAgent.java:reasoning、runToolBatch、dispatchToolCalls。agentscope-core/src/main/java/io/agentscope/core/tool/Toolkit.java:callTools。agentscope-core/src/main/java/io/agentscope/core/tool/ToolExecutor.java:executeCore、executeAll。agentscope-harness/src/main/java/io/agentscope/harness/agent/tool/AgentSpawnTool.java:agentSpawn、agentSend、execLocalSync。agentscope-harness/src/main/java/io/agentscope/harness/agent/subagent/DefaultAgentManager.java:createAgentIfPresent、invokeAgent。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_spawn、agent_send 和异步任务收回到一条主线。建议在读完正文后再看。
