别把 Agent 写成"全能选手"——聊聊 Subagent 设计的正确姿势

你写 Agent,大概率踩过这个坑:

一个主 Agent 包揽一切。读代码、改代码、跑测试、出报告全扔给它。任务一复杂,token 就炸,LLM 就开始乱序,状态越来越混。

根本原因不是模型不够强,是架构设计错了。Agent 跟人一样,单线程处理复杂任务天然容量有限。

这篇文章从我自己用 Java 实现的 MVP Agent 出发,聊聊 Subagent(子 Agent)这个模式的设计思路——为什么要拆、怎么拆、拆出来的 Subagent 怎么保证安全、怎么跟父 Agent 通信。


一、为什么需要 Subagent

先说根本问题:上下文窗口是有限的,注意力是分散的。

一个 128K token 的上下文窗口,塞满系统提示、对话历史、工具声明之后,真正留给任务本身的空间并不多。你让它同时探索代码结构、思考方案、动手改代码、跑测试验证——这四件事的信息全挤在一个历史里,互相干扰。

更糟的是:这四件事的安全级别不同

  • 探索代码:只需要读,写权限是多余的风险
  • 出设计方案:压根不需要执行任何命令
  • 改代码:需要写,但不该影响主工作区(万一改错了)
  • 验证结果:只需要跑只读诊断命令

如果全塞给一个 Agent,你要么给它所有权限(太危险),要么给它最小权限(很多任务干不了)。

Subagent 的出现解决了这个矛盾:把任务按安全边界和职责拆分,每个 Subagent 拿刚好够用的权限,在独立上下文里跑。


二、四种 Subagent 类型

在我的 MVP 里,我参考 Claude Code 的架构设计了四种 Subagent:

┌─────────────────────────────────────────────────────────────┐
│                      父 Agent (AgentLoop)                   │
│              协调者 + 决策者 + 任务分发                       │
└──────────────┬──────────────────────────────────────────────┘
               │ task(prompt, agent_type, maxRounds)
       ┌───────┴──────────────────────────┐
       │        SubagentManager           │
       │     线程池 + 通知队列             │
       └──┬────────┬────────┬────────┬───┘
          ▼        ▼        ▼        ▼
      EXPLORE   PLAN   VERIFY  GENERAL
      只读探索  方案设计  验证审查  通用执行

每种类型的核心特征:

类型 工具集 适用场景
EXPLORE file + search(只读) 扫描代码库、找所有 SQL 注入风险
PLAN file + search(最严格) 读代码分析架构、只输出方案文字
VERIFICATION file + search + bash(只读诊断) 跑 mvn validate、检查代码质量
GENERAL 除 task 外全部 完整的代码修改任务

关键点是最后一列的限制:GENERAL 类型唯独没有 task 工具,子 Agent 不能创建孙 Agent,防止无限递归。


三、双层安全保障

Subagent 最容易被人忽略的设计是:安全不能只靠 Prompt 约束。

很多人写 Subagent 只在系统提示里加一句 “你不能写文件”,然后就放心了。这是错的。

LLM 是概率机器,Prompt 的本质是"建议",不是"锁"。

我在 MVP 里用了两道防线:

第一道:物理隔离(工具白/黑名单)

// EXPLORE:物理移除写工具和 bash
subDispatcher = dispatcher.without("task", "TodoWrite", "bash",
    "background_run", "check_background");

// PLAN:只保留白名单,最严格
subDispatcher = dispatcher.only("file", "search");

工具根本就不在 DispatchMap 里,LLM 想调也调不了——ToolDispatcher 直接返回 “Unknown tool”。

第二道:Prompt 约束(否定指令)

case EXPLORE -> """
    你是 Explore Agent,代码探索专家。
    NEVER 写文件或修改任何代码。
    NEVER 执行 Shell 命令。
    NEVER 创建子 Agent。
    """;

Prompt 失效不等于安全失效。两道防线互补:第一道是机制保证,第二道是行为引导。


四、GENERAL 类型还有一层:Worktree 隔离

GENERAL Subagent 可以写文件,这意味着多个并行 Subagent 如果写同一个文件,会互相踩踏。

解法:每个 GENERAL Subagent 跑在独立的 git worktree 里。

主工作区(父 Agent)
├── src/main/java/...   ← 父 Agent 操作这里
│
子 Agent 1 worktree
├── src/main/java/...   ← 独立副本,改它不影响主区
│
子 Agent 2 worktree
├── src/main/java/...   ← 独立副本

Subagent 跑完之后,通过 git diff 把变更摘要注入父 Agent 的对话历史,由父 Agent 决定是否合并。这跟 Claude Code 的 Agent 工具带 isolation: 'worktree' 参数是一个思路。

// 完成后把 diff 附到结果里
if (worktreePath != null) {
    String diff = worktreeManager.getDiff(worktreePath);
    result = result + "\n\n[Worktree 修改摘要]\n```diff\n" + diff + "\n```";
}

五、父子通信:Drain 模式

父 Agent 和 Subagent 的通信是关键——Subagent 在线程池里异步跑,父 Agent 继续处理其他事情,怎么知道子 Agent 完成了?

答案是 Drain 模式

SubagentManager
│
├── runningTasks: ConcurrentHashMap<id, Future>
│
└── notifications: ConcurrentLinkedQueue   ← 子 Agent 完成后写这里
         │
         │   每轮 AgentLoop 开头 drain() 一次
         ▼
父 Agent History ← <subagent-results>结果注入</subagent-results>

父 Agent 每轮循环开头调一次 drain(),把所有完成的子 Agent 结果作为 UserMessage 注入对话历史。这样父 Agent 不需要轮询,自然地在下一轮看到结果。

还有一个细节:如果子 Agent 还没跑完,父 Agent 准备返回纯文本答案时,要强制拦截。

// 子 Agent 还没跑完 → 不让结束
if (subagentManager.hasRunning()) {
    history.add(UserMessage.from("<reminder>还有 "
        + subagentManager.runningCount()
        + " 个子 Agent 正在后台,请等待结果。</reminder>"));
    continue;
}

不然父 Agent 会在子 Agent 结果回来之前就给出一个不完整的答案。


六、跟"自主领取任务 Agent"对比

2025 年以来,GitHub 上有一类流行的 Agent 叫"自主领取任务 Agent",它的典型模式是:

主 Agent 扫任务队列(Jira/GitHub Issues)
→ 按标签 or 优先级自动领取
→ 为每个 Issue 启动一个子 Agent
→ 子 Agent 独立探索、改代码、提 PR
→ 结果汇报回主 Agent

和我的 MVP 比,差异在于任务来源

对比维度 MVP 方案 自主领取任务 Agent
任务来源 LLM 主动决策拆分 外部队列(Jira/Issues)驱动
子 Agent 生命周期 父 Agent 控制 每个 Issue 一个生命周期
并发模型 线程池固定并发 通常无界并发
隔离机制 git worktree 独立 git 分支 or 容器
回归机制 diff 注入父 Agent PR Review + CI

两种方案的 Subagent 设计思路是相通的:独立上下文 + 最小权限 + 结果回传。区别在于谁触发、怎么确认结果。


七、几个容易被忽视的设计细节

1. Subagent 超限时返回部分结果,而非丢弃

// 超限时返回 lastPartialResult,而非空字符串
if (lastPartialResult != null) {
    return "[部分结果(已达最大轮次)]\n" + lastPartialResult;
}

Subagent 没跑完不等于什么都没收获。把中间结果告诉父 Agent,比返回空好得多。

2. 连续失败要指数退避 + 提前退出

LLM API 偶发超时、限速很正常。子 Agent 不应无脑重试,要加退避:

// 1s, 2s, 4s
Thread.sleep(1000L * (1 << (consecutiveFailures - 1)));

3 次连续失败后提前退出,避免占用线程池。

3. GENERAL 类型 worktree 创建失败要报错,不能降级

if (worktreePath == null) {
    // 报错,不降级到主 workspace
    notif.put("result", "GENERAL 子 Agent 启动失败: git worktree 创建失败");
    return;
}

降级到主 workspace 会破坏隔离性,这比直接失败危险得多。


八、总结

Subagent 的设计核心可以浓缩成三句话:

  1. 按安全边界拆,不按功能拆——EXPLORE/PLAN/VERIFY/GENERAL 的分法不是"按模块",是"按危险程度"
  2. 双层安全:物理隔离优先,Prompt 约束辅助——不要只靠 Prompt 说"你不能写文件"
  3. 通信用 Drain,不用轮询——子 Agent 完成往队列写,父 Agent 每轮 drain 一次,干净且无阻塞

Agent 架构是后端工程师能真正掌控的地方。Model 是司机,Harness(含 Subagent 体系)是车——造好的车,比换更贵的司机更值钱。

如果你也在做 Java Agent 项目,欢迎交流,评论区见。

点赞 + 收藏 = 你的认可就是我继续写的动力。


Logo

这里是“一人公司”的成长家园。我们提供从产品曝光、技术变现到法律财税的全栈内容,并连接云服务、办公空间等稀缺资源,助你专注创造,无忧运营。

更多推荐