Agent Scope Java 2.x 系列【36】Harness:子 Agent 基础入门

文章目录

  • [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、找代码问题、检查代码规范时使用。

自定义 specagent 的区别:

内置 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}]

这个分桶规则在所有 AgentStateStoreWorkspace / 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_spawnagent_sendtimeout_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 变更"

要列当前活跃的子 agentagent_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

这带来三个特性:

  1. 共享存储模式(多副本)下,任意节点都能读到任务状态
  2. 任务执行粘在创建节点 ,但完成结果会被任意节点读到、并能正常推回父 agent
  3. 想取消可从任意节点调 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 的所有改动"