文章目录
- [1. 概述](#1. 概述)
- [2. 三种声明方式](#2. 三种声明方式)
-
- [2.1 工作区 spec 文件](#2.1 工作区 spec 文件)
- [2.3 编程式声明](#2.3 编程式声明)
- [2.3 内置 general-purpose](#2.3 内置 general-purpose)
- [3. 工作区](#3. 工作区)
-
- [3.1 ISOLATED(默认):自己独立的工作区](#3.1 ISOLATED(默认):自己独立的工作区)
- [3.2 SHARED:直接共用主工作区](#3.2 SHARED:直接共用主工作区)
- [4. 同步和异步执行](#4. 同步和异步执行)
-
- [4.1 后台任务自动反向通知](#4.1 后台任务自动反向通知)
- [4.2 后台任务工具(逃生口)](#4.2 后台任务工具(逃生口))
- [4.3 给已存在的子 agent 补一条消息](#4.3 给已存在的子 agent 补一条消息)
- [5. 持久会话](#5. 持久会话)
- [6. 远程子 Agent](#6. 远程子 Agent)
- [7. 注意事项](#7. 注意事项)
- [8. 入门案例](#8. 入门案例)
1. 概述
主 Agent 在一个会话里把所有事都做完,有两个坏处:
- 上下文膨胀(无关细节挤占主线对话)
- 无法并行(一件件串着干)。
子 Agent 的思路是:
- 把"可独立处理、上下文重、可并行"的任务委派出去;
- 每个子
Agent是一个临时实例 (本地的HarnessAgent或远程stub); - 子
Agent跑自己独立的会话,结果通过工具调用返回给父agent。
这样主 Agent 的对话保持清爽,只看到"我派了个子任务、拿回了一份结果",而调研/审查/检索这类重活在子会话里完成。
2. 三种声明方式
子 Agent 支持三类来源,构建时合并:
| 方式 | 适用 | 怎么配 |
|---|---|---|
| 内置 general-purpose | 通用兜底(镜像主 agent 能力) | 总是有,不需要配 |
| 工作区 spec 文件 | 项目特有、可版本控制 | workspace/subagents/<id>.md |
| 编程式声明 | 跑时才能确定(远程、动态参数) | builder.subagent(SubagentDeclaration.builder()...) |
2.1 工作区 spec 文件
框架非递归 扫描 workspace/subagents/*.md 文件,文件名(去掉 .md)即 agent_id 唯一标识,不要在 front matter 里再写 name。
完整的 front matter 字段:
text
---
description: 代码评审专家 # 必填,agent 选择是否委派的关键依据
workspace:
mode: isolated # 默认 isolated;shared 表示与父共享工作区
path: ./defs/reviewer # 可选;不写则用默认子目录
model: openai:gpt-4o-mini # 可选;不写则继承父 agent
steps: 8 # 可选;该子 agent 单次最多迭代次数
temperature: 0.2 # 可选;覆盖父的 GenerateOptions
top_p: 0.95 # 可选
hidden: false # true 时不出现在可见列表(仍可程序化 spawn)
mode: subagent # primary / subagent / all,默认 all;primary 不允许被 spawn
expose_to_user: true # 可选三态;强制/禁止向用户暴露(见【37】)
tools: [read_file, grep_files] # 可选;继承工具的白名单
---
你是一个专注代码评审的子 agent。
2.3 编程式声明
跑时才能确定的子 agent(远程地址、动态参数)用 builder 声明:
java
import io.agentscope.harness.agent.subagent.SubagentDeclaration;
import io.agentscope.harness.agent.subagent.WorkspaceMode;
HarnessAgent.builder()
.name("orchestrator")
.model(model)
.workspace(workspace)
// 本地子 agent:独立工作区 + 指定模型 + 工具白名单
.subagent(SubagentDeclaration.builder()
.name("reviewer")
.description("代码审查专家")
.workspace(Path.of("./defs/reviewer")) // 来源一:本地工作区目录
.workspaceMode(WorkspaceMode.ISOLATED)
.model("qwen3-max") // 不写则继承父 agent
.steps(8) // 单次最多迭代 8 步
.tools(List.of("read_file", "grep_files")) // 工具白名单
.build())
// 远程子 agent:只填 url + 可选 headers,走 Agent Protocol
.subagent(SubagentDeclaration.builder()
.name("remote-researcher")
.description("远端调研子 agent")
.url("http://agent-task-server:8080") // 来源三:远程 HTTP 服务
.headers(Map.of("Authorization", "Bearer xxx"))
.build())
.build();
三种来源互斥 :
workspace(...)、inlineAgentsBody(...)、url(...)三选一。workspace指向本地定义目录,inlineAgentsBody直接内联spec正文,url走远程。
2.3 内置 general-purpose
不需要写任何声明文件,总是可用 (除非你显式 disableSubagents())。general-purpose 就是主 agent 的一个克隆,模型、工具、技能、工作区都和主 agent 一致,唯一区别是它跑在独立的子会话里、且不能再往下委派。
它的价值是 上下文隔离 + 零配置 :当你想把一个子任务丢到一段干净的新对话 里跑(避免一堆中间过程污染主线对话),而这个子任务又需要完整能力 (读写文件、执行命令、调技能......)、且不值得为它专门写一份 spec 时,直接用它最省事。
HarnessAgent 默认会创建这个 general-purpose 并告知大模型,于是模型实际看到的系统提示里,有这么一段:
text
### Available agent ids
- `general-purpose`: General-purpose subagent with same capabilities as the main agent.
- `reviewer`: 代码审查专家。当用户需要 review PR、找代码问题、检查代码规范时使用。
自定义 spec 子 agent 的区别:
| 内置 general-purpose | 工作区 spec 子 agent(如 reviewer) | |
|---|---|---|
| 是否要写文件 | 不用,总是有 | 要写 subagents/<id>.md |
| 能力范围 | 全量镜像主 agent(同模型/工具/技能) | 可收窄 (如只给 read_file/grep_files + 自定义角色提示词) |
| 工作区 | SHARED(共享主工作区) | 默认 ISOLATED(独立) |
| 适合 | 临时的、任意的子任务,需要完整能力 | 反复使用、需要约束、可版本控制的专门角色 |
3. 工作区
workspaceMode 决定子 agent 的运行时工作区根目录 怎么算,它直接影响子 agent 能读写哪些文件、状态存在哪、以及多租户会不会串。
3.1 ISOLATED(默认):自己独立的工作区
子 agent 有一块属于自己的运行时工作区 ,和主 agent 物理隔开:
- 声明里写了
workspace.path:该路径既是运行时根,也是它AGENTS.md(系统提示)的来源。适合这个子 agent 有一套自己的定义目录(含skills/knowledge)。 - 没写
workspace.path:框架自动在mainWorkspace/agents/<name>/workspace/开一个子目录作运行时根,并用spec的正文(inline body)作系统提示。
ISOLATED 还有一个关键特性,持久化状态按父会话 + 用户分桶 ,当 spawn 时的 RuntimeContext 带了 userId/parentSessionId,子 agent 的状态 key 形如:
text
{declarationName}[@{parentSessionId}][#{userId}]
这个分桶规则在所有 AgentStateStore(Workspace / Redis / InMemory / 自定义)上统一生效。带来的直接好处是:
- 同一个用户在不同对话里
spawn同名子agent,彼此互不污染; - 不同用户之间更是天然隔离,多租户安全边界不会因为委派而被打破。
适用场景 :要干净隔离、不想让子 agent 的临时文件/状态弄脏主工作区、或有多租户隔离诉求时。这也是默认值,拿不准就用它。
3.2 SHARED:直接共用主工作区
子 agent 的运行时根永远是 mainWorkspace ,无论声明里 workspace.path 写没写:
- 写了
workspace.path:只借用它的AGENTS.md作系统提示正文;但定义目录里的skills/、knowledge/、MEMORY.md都会被忽略(因为运行时根不是它)。 - 没写
workspace.path:用inline body作系统提示。
SHARED 不做按用户/会话分桶 :它有意复用父的 bucket、不做多租户隔离。
使用场景:子 agent 的产出需要被父 agent 立即读到 (父子共享同一份文件),或者你就是想让子 agent"站在主工作区里干活"。
内置的 general-purpose 恒为 SHARED ,正是因为它的定位就是"主 agent 的克隆、产出即时可见"。
4. 同步和异步执行
主 agent 通过 agent_spawn 创建子 agent,关键参数是:
timeout_seconds > 0(默认30):执行同步调用,主agent在这一步 block 等待 结果,子agent的结果作为工具结果返回。timeout_seconds = 0:执行后台调用,立即返回一个task_id,子agent在后台跑。
4.1 后台任务自动反向通知
后台任务跑完,主 agent 不需要轮询 ,下一次推理开始前,框架会把已完成的任务结果作为系统提醒注入对话末尾:
text
<system-reminder>
后台任务已交付:
- task_id=xxx,agent=research-analyst,status=COMPLETED
结果摘要:...
</system-reminder>
主 agent 看到这条 reminder 自然地回应或继续行动。所以你不需要在 prompt 里写记得调 task_output 轮询,那是旧版本的做法。
4.2 后台任务工具(逃生口)
子 agent 的生命周期由两组工具配合完成:
| 工具 | 职责 |
|---|---|
agent_spawn |
创建子 agent,可选地执行任务(同步或后台) |
agent_send |
向已存在的子 agent 追加消息 |
agent_list |
列出当前活跃的子 agent 实例 |
task_output |
通过 task_id 获取后台任务结果(阻塞或非阻塞) |
task_cancel |
取消正在运行的后台任务 |
task_list |
列出所有后台任务及其当前状态 |
其中:
agent_spawn/agent_send管理子 agent 实例(创建、复用、通信);task_output/task_cancel/task_list管理后台任务结果(查状态、取结果、取消)。
两者的桥梁是 task_id:在 agent_spawn 或 agent_send 用 timeout_seconds=0 时返回。
大多数情况下"自动反向通知"会把结果推回来,不需要显式调用这些任务工具。它们主要作为逃生口:反向通知触发前主动查进度、取消不再需要的任务、或对话压缩后恢复任务状态。
4.3 给已存在的子 agent 补一条消息
agent_spawn 返回值里有一个 agent_key(运行时实例句柄),用它或 label 就能后续追加消息:
text
agent_send agent_key="agent:reviewer:abc-123" message="顺便也看下 schema 变更"
spawn 时若设了 label,也可以用 label 寻址:
agent_spawn agent_id="reviewer" task="review 这次 PR" label="pr-reviewer"
agent_send label="pr-reviewer" message="顺便也看下 schema 变更"
要列当前活跃的子 agent:agent_list。
5. 持久会话
默认每次 agent_spawn 都创建新的子 agent 实例和会话,不保留之前调用的上下文。在声明里设 persistSession(true),可让同一子 agent 在多次 spawn 之间复用:
java
.subagent(SubagentDeclaration.builder()
.name("note-taker")
.description("跨对话轮次积累笔记")
.persistSession(true) // 复用实例:对话历史与状态都保留
.build())
开启后,框架按 (parentSessionId, agentId, label) 生成确定性的 key 。再次 spawn 相同组合时,会复用已存在的 agent 实例,对话历史和状态都保留。
6. 远程子 Agent
声明里只填 url + 可选 headers,子 agent 就走远程 HTTP 服务(Agent Protocol)执行:
java
.subagent(SubagentDeclaration.builder()
.name("remote-researcher")
.description("远端调研子 agent")
.url("http://agent-task-server:8080")
.headers(Map.of("Authorization", "Bearer xxx"))
.build())
同样支持同步(timeout_seconds>0)和后台(timeout_seconds=0)。
后台任务的状态默认写到 workspace/agents/<parentAgentId>/tasks/<sessionId>.json。
这带来三个特性:
- 共享存储模式(多副本)下,任意节点都能读到任务状态;
- 任务执行粘在创建节点 ,但完成结果会被任意节点读到、并能正常推回父
agent; - 想取消可从任意节点调
task_cancel,执行节点轮询取消标记后中止。
7. 注意事项
| 项目 | 详细说明 |
|---|---|
| 工具描述优化 | description 是模型判断是否委派任务的核心依据。 劣质写法:代码评审 优质写法:当用户要 review PR、找代码风格问题时使用 必须写明触发场景,提升调用命中率。 |
| 递归保护 | 子Agent强制标记为叶子节点(leaf worker),系统提示禁止继续派生子Agent。 委派链路固定为:主Agent → 子Agent,不允许无限递归spawn。 |
| 租户上下文透传 | 父Agent RuntimeContext.userId 自动透传给子Agent,多租户隔离链路不会断裂。 |
| 权限继承策略 | 1. 默认开启:父Agent所有 DENY 拒绝规则自动继承给子Agent,防止权限逃逸; 2. 可手动关闭:inheritParentPermissions(false); 3. PlanMode只读状态不会自动向下继承。 |
| 父子事件流式转发 | 父Agent调用 streamEvents() 时,子Agent的中间事件实时回流到父级Flux流,并携带来源标记。 |
8. 入门案例
最简单的用法:把子 agent 的描述 写到工作区 里就行,文件名就是 agent_id 。
示例,在 workspace/subagents/reviewer.md 定义一个子智能体:
text
---
description: 代码审查专家。当用户需要 review PR、找代码问题、检查代码规范时使用。
---
你是一个专注代码评审的子 agent。请按以下流程工作:
1. 先 read_file / grep_files 收集上下文
2. 给出按文件 / 行号的具体建议
3. 末尾给一个 1-5 的总体评分
其中:
description是主 agent 决定要不要委派的关键依据,务必写清楚"什么时候该用它";- 下面的正文就是这个子
agent的系统提示词。
然后主 Agent 在推理时就能直接调用(无需任何注册):
text
agent_spawn agent_id="reviewer" task="review 这次 PR 的所有改动"