主会话别被调研淹没:同步 `Agent` 子代理
系列回顾:主循环 · 代码库工具 · REPL · 项目上下文 · Skills · 权限 + Write · MCP 概念 · MCP 实现 · Context Budget · Bash · compact 2.0 · autocompact · Hooks · Memory
主会话一路 Read / Grep / Bash,细节全堆进
messages[]——下一轮还要带着这些噪音继续聊。
人会说:「你去旁边查一下,回来只告诉我结论。」
这篇讲v6-subagents:内置Agent工具,同步嵌套一次query(),子任务用独立消息列表跑完,只把摘要塞回父会话的tool_result——父循环骨架仍是原来的 ReAct。
为什么需要「旁边那间屋」?
主会话适合:定目标、做决策、改代码。
不适合:一次性扫半个仓库、跑一长串探索工具,却把每一步轨迹永久留在父历史里。
| 主会话自己干 | Agent子代理 | |
|---|---|---|
| 消息历史 | 写进QueryEngine.messages | 独立数组,不写回父 |
| 回给主模型的 | 整段 tool 轨迹 | 末条 assistant 文本摘要 |
| 上下文污染 | 高 | 低 |
| 费用/延迟 | 少一次嵌套 | 多一次完整子循环 |
所以子代理不是「另一个产品」,而是:同一套query(),换一间干净房间,干完只交报告。
工具长什么样?
Agent({description:'Find auth entrypoints',// 必填:3–5 词短描述(TRACE / 状态)prompt:'在仓库里找出登录相关入口文件,只列路径与一句理由。',// 必填:子任务全文tool_names:['Glob','Grep','Read'],// 可选:子工具白名单})| 字段 | 必填 | 作用 |
|---|---|---|
description | 是 | 给人看的短标签;不当子推理正文 |
prompt | 是 | 子会话的第一条 user 消息 |
tool_names | 否 | 白名单;缺省 = 父工具表去掉Agent后的全部 |
父侧tool_result成功时大致是:
[Agent: Find auth entrypoints] src/auth/login.ts — 登录表单入口 …中间子工具的tool_use/tool_result不会进入父messages。
一张图:嵌套怎么跑
父 query(depth=0) → 模型 tool_use: Agent → AgentTool.call → createSubagentContext(depth=1,新 AbortController 链到父) → toolsForSubagent(去掉 Agent,可选白名单) → 嵌套 query({ messages: [prompt], depth: 1, … }) → 子自己 ReAct:callModel → 工具 → … → 取最后一条 assistant 的 text(≤32KB,超长留尾) → 父 tool_result = [Agent: description] + 摘要 → 父继续下一轮对应实现:src/tools/AgentTool.ts+src/utils/subagent.ts。
防递归:两道闸
子代理若还能再调Agent,就会无限套娃。mini 用双保险:
- 子工具池排除
Agent(toolsForSubagent) depth上限默认 1:父为 0 时可 spawn depth=1;已在 depth≥1 再调 → 直接错误tool_result,不启嵌套
depth 0(主会话)──Agent──▶ depth 1(子) └── 再 Agent?→ 拒绝 / 池子里根本没有depth与 Stop hooks共用同一语义:只有depth === 0跑 Stop;子代理收尾不跑 Stop,也不做单独的SubagentStop。
权限、中止、失败
| 主题 | 行为 |
|---|---|
| 权限 | 默认复用父canUseTool;父 deny 对子生效;写操作仍可能弹 REPLy/N |
| Abort | 子有独立AbortController,但链到父:父中止 → 子中止 |
| 失败 | 异常 /aborted/ 无文本 → 父侧is_error的tool_result(fail-soft,不炸主循环) |
| Compact | 子用同一套 deps;子历史独立,不写回父 Engine |
Agent标成非只读、非并发安全:派生子任务有副作用与串行成本,不跟只读工具抢并发。
和 Skills / Memory / Hooks 怎么区分?
| 机制 | 一句话 |
|---|---|
| Skills | 往主会话注入说明书(仍在同一 messages) |
| Memory | system 里跨会话偏好 |
| Hooks | 工具前后拦 / 记;Stop 只在顶层 |
| Agent | 另开一轮嵌套 query,只交摘要 |
Skills 是「给当前大脑多读一页纸」;Agent 是「派一个实习生去查,回来交纪要」。
30 秒感受一下
bun run dev# 或 mock:需能走到真实工具时再试 Agent自然语言示例:
用 Agent 工具:在仓库里找与 MCP 配置相关的文件,只返回路径列表。 description 用 "Locate MCP config"。TRACE=1时 stderr:
[trace] agent.start description=Locate MCP config depth=1 [trace] agent.end description=Locate MCP config ok=true reason=completed和主循环的关系
L1 主 query / QueryEngine → 照旧;Agent 只是多一个 Tool L2 AgentTool.call → 嵌套 query(独立 messages) L3 createSubagentContext → depth+1、abort 链、工具池裁剪 L4 摘要回传 → 父 tool_result;子轨迹不进父历史子代理复用
query(),不另写一套循环;隔离的是消息列表与深度,不是换引擎。
刻意没做什么?
| 没做 | 意味着什么 |
|---|---|
| swarm / 多 worker / Coordinator | 一次一个同步子任务 |
| worktree / 后台 fork | 子跑完才回父;无并行隔离目录 |
subagent_type命名 agent 目录 | 不按角色配置多套 agent |
SubagentStop | 子收尾不跑 Stop;顶层 Stop 另算 |
| 完整 resume UI / 换模型字段 | 入参只有 description / prompt / tool_names |
这一刀验证的是「隔离调研」的最小面:
Agent 工具 → 嵌套 query → depth≤1 + 排除 Agent → 权限派生 → 摘要回父 → TRACE 可观测系列拼图
| 篇 | 能力 |
|---|---|
| 主循环 | 单会话 ReAct |
| compact / Memory | 主会话怎么瘦、怎么记住 |
| Hooks | 工具生命周期 |
| 本篇 | 旁路子任务,结论回传 |
Harness 再多一根柱子:循环、工具、会话、上下文、技能、权限、MCP、预算、hooks、memory、subagent。
你可以从这里带走什么?
- 子代理 = 嵌套
query()+ 独立 messages——不是新框架。 - 父只要摘要——中间工具轨迹留在子会话里。
- 防递归靠池子 + depth——去掉
Agent,且默认 maxDepth=1。 - 权限与中止要继承——派生
canUseTool,abort 链到父。 - depth 是全局语义——顶层才 Stop;子不跑 Stop。
- 失败 fail-soft——错误进父
tool_result,主循环继续。
仓库与相关文档
- GitHub:https://github.com/jimchou-h/react-agent-mini
- 上一篇:Memory
- 源码:AgentTool.ts · subagent.ts
- 术语:src/tools/CONTEXT.md
欢迎 Star、Issue 和 PR。
本文基于 react-agent-mini 变更v6-subagents(同步Agent工具 + depth/防递归 + 摘要回传)撰写。