子代理与委托:递归编排的设计
本章核心源码:
tools/delegate_tool.py(978 行)、run_agent.py(IterationBudget 168-209 行,中断传播 609-617 行)
定位:本章分析子代理的 spawn 机制、IterationBudget 在父子间的分配、会话隔离策略、以及 Memory Provider 的 on_delegation 钩子。 前置依赖:第 4 章(AIAgent 内核)、第 6 章(工具系统)。适用场景:想理解 agent 如何并行化工作流,或设计多 agent 协作系统。
为什么委托不只是"调用一个工具"
模型可以调用 delegate_task 把一个子任务交给另一个 agent 处理。这听起来像一次普通的工具调用,但实际涉及三个非平凡的工程问题:
- 资源隔离:子代理需要独立的 iteration budget、session、memory context
- 中断传播:用户中断时,父代理需要通知所有活跃的子代理
- 深度控制:防止子代理递归 spawn 孙代理导致资源爆炸
子代理的构造
delegate_task 最终通过 _build_child_agent()(tools/delegate_tool.py:202)构造子代理:
# tools/delegate_tool.py:287-316(简化)
child = AIAgent(
model=effective_model, # 继承父代理模型
max_iterations=max_iterations, # 独立预算(默认 50)
quiet_mode=True, # 静默运行
ephemeral_system_prompt=child_prompt, # 子任务专用指令
platform=parent_agent.platform, # 继承平台信息
skip_context_files=True, # 不加载 AGENTS.md 等
skip_memory=True, # 不加载记忆系统
session_db=parent_agent._session_db, # 共享 SessionDB
parent_session_id=parent_agent.session_id, # 建立父子血缘
iteration_budget=None, # 创建独立 budget
)
child._delegate_depth = parent_agent._delegate_depth + 1
继承与隔离的平衡
| 属性 | 策略 | 原因 |
|---|---|---|
| 模型/Provider/API key | 继承 | 子代理使用相同的 LLM 服务 |
| Toolset | 继承(支持覆盖) | 子代理通常需要和父代理相同的工具能力 |
| IterationBudget | 独立创建 | 防止子代理消耗父代理的预算 |
| Memory | 跳过 | 子代理不需要用户记忆,减少 token 开销 |
| Context files | 跳过 | 子代理不需要 AGENTS.md、SOUL.md |
| SessionDB | 共享 | 子代理的对话记录写入同一个数据库 |
| Session ID | 新建(parent_session_id 链接) | 保持会话血缘但独立记录 |
关键设计:skip_memory=True 和 skip_context_files=True。子代理不加载用户的 MEMORY.md、USER.md 和 SOUL.md——这不仅减少了约 2000+ token 的 system prompt 开销,更重要的是避免了子代理误修改用户记忆。
Reasoning Effort 配置
子代理现在支持通过 delegation.reasoning_effort 配置独立的推理强度(tools/delegate_tool.py:315-332):
delegation:
reasoning_effort: "low" # 有效值: none / minimal / low / medium / high / xhigh
解析优先级遵循"显式覆盖 > 继承":
- delegation 配置覆盖:如果
delegation.reasoning_effort有值且合法,子代理使用该值 - 继承父代理:否则继承父代理的
reasoning_config
# tools/delegate_tool.py:315-332(简化)
parent_reasoning = getattr(parent_agent, "reasoning_config", None)
child_reasoning = parent_reasoning
delegation_effort = str(delegation_cfg.get("reasoning_effort") or "").strip()
if delegation_effort:
parsed = parse_reasoning_effort(delegation_effort)
if parsed is not None:
child_reasoning = parsed
else:
logger.warning("Unknown delegation.reasoning_effort '%s', inheriting parent level", delegation_effort)
这个设计让用户可以对子代理使用更低的推理强度以节省 token 成本——子代理的任务通常比主 agent 更聚焦,不需要同等深度的推理。
IterationBudget 的独立分配
# run_agent.py:168-209
class IterationBudget:
def __init__(self, max_total: int):
self.max_total = max_total
self._used = 0
self._lock = threading.Lock()
父代理默认 90 次迭代,子代理默认 50 次迭代(通过 config.yaml 的 delegation.max_iterations 配置)。两者互不消耗——子代理的 50 次迭代不会从父代理的 90 次中扣除。
这意味着一次对话的总迭代次数可能超过 90——如果父代理 spawn 了 3 个子代理,理论最大值是 90 + 3×50 = 240 次。这是有意的设计:子代理处理的是独立子任务,不应受父代理预算的约束。
execute_code 的退款机制
# IterationBudget.refund()
def refund(self) -> None:
with self._lock:
if self._used > 0:
self._used -= 1
execute_code 工具(通过 RPC 调用工具的程序化接口)的迭代会被退款。原因:程序化工具调用是 agent 内部的效率优化手段,不应消耗面向用户的 budget。
中断传播
当用户中断父代理时,需要通知所有活跃的子代理:
# tools/delegate_tool.py:327-334
# 注册子代理用于中断传播
if hasattr(parent_agent, '_active_children'):
with parent_agent._active_children_lock:
parent_agent._active_children.append(child)
# run_agent.py:609-617
self._interrupt_requested = False
self._interrupt_message = None
self._active_children = [] # 运行中的子代理列表
self._active_children_lock = threading.Lock()
中断时,父代理遍历 _active_children,设置每个子代理的 _interrupt_requested = True。子代理在主循环的每次迭代开始时检查这个标志。
深度控制
# tools/delegate_tool.py:319
child._delegate_depth = parent_agent._delegate_depth + 1
_delegate_depth 从 0(顶层 agent)开始递增。子代理在构造时会检查深度——默认不允许超过 1 层(即子代理不能 spawn 孙代理)。这防止了递归委托导致的资源爆炸。
设计哲学:能力边界是架构不变量,不是配置项
DELEGATE_BLOCKED_TOOLS 是一个硬编码的 frozenset——没有任何配置开关允许子代理使用 delegate_task、clarify、memory 或 send_message(tools/delegate_tool.py:29-36)。这不是遗漏。团队将能力边界视为架构不变量而非用户偏好:允许子代理递归委托会造成无界的资源消耗;允许子代理修改共享记忆会破坏隔离保证。这些约束在设计层面就是不可商量的。
Memory Provider 的 on_delegation 钩子
当子代理完成任务后,MemoryProvider 的 on_delegation() 钩子被触发:
# agent/memory_provider.py(ABC 方法)
def on_delegation(self, task: str, result: str, **kwargs) -> None:
"""Called when a delegate task completes. Observe subagent work."""
pass
这让外部 memory provider(如 Honcho)可以观察子代理的工作内容,将其纳入用户建模。例如:如果用户经常把"代码审查"委托给子代理,Honcho 可以记录这个偏好。
详见第 11 章(Memory Provider)。
设计哲学:摘要可见性(认知隔离)
父代理永远看不到子代理的中间工具调用、推理轨迹或内部状态——只能看到最终摘要(tools/delegate_tool.py:15-17)。这是刻意的认知隔离:如果父代理观察到子代理未完成的推理过程,可能被中间状态或幻觉步骤误导。在 agent 边界处实施信息隐藏,确保每个 agent 在自己干净的命名空间内推理。
设计哲学:认知诚实优先于便利
_resolve_workspace_hint() 只在能验证绝对路径目录确实存在时才注入 workspace 路径——它永远不会注入一个猜测的路径如 /workspace(tools/delegate_tool.py:111-133)。宁可不给提示也不给错误提示。如果子代理收到一个虚假路径,其所有文件操作都会静默失败或产生令人困惑的错误。
并行委托
delegate_task 支持同时 spawn 多个子代理处理不同子任务。每个子代理在独立线程中运行,通过 ThreadPoolExecutor 并行执行。结果收集后合并返回给父代理。
# tools/delegate_tool.py(并行执行简化)
with ThreadPoolExecutor(max_workers=min(len(tasks), 4)) as executor:
futures = [executor.submit(_run_single_child, i, task, child) for i, (task, child) in enumerate(zip(tasks, children))]
results = [f.result() for f in futures]
设计启示
子代理机制展示了"扩展 agent 能力"的两种互补方式:
| 方式 | 适用场景 | 优势 | 代价 |
|---|---|---|---|
| 工具 | 单步操作(搜索、读写文件) | 低开销,一次调用 | 不能多步推理 |
| 子代理 | 多步任务(代码审查、独立调研) | 完整的 agent 推理能力 | 独立 budget + LLM 调用 |
Hermes 通过 delegation.max_iterations 让用户控制子代理的成本上限,通过 _delegate_depth 防止递归失控,通过 skip_memory + skip_context_files 最小化子代理的 token 开销。
第 10 章将进入状态层,分析 SessionDB 如何持久化这些父子代理的对话记录。
设计赌注回扣:子代理机制服务于 Run Anywhere(子代理在独立线程中运行,Gateway 场景下多个用户的子代理可以并发执行)和 Learning Loop(
on_delegation钩子让 memory provider 观察子代理行为,纳入用户建模)。
版本演化说明
本章核心分析基于 Hermes Agent v0.8.x(2026 年 4 月)。 子代理机制在 v0.3.0 发布窗口就已经具备雏形;独立的
IterationBudget也属于这一时期的设计。之后 v0.4.0-v0.6.0 之间继续补齐了更强的后台审查、中断传播和深度控制等细节。v0.8.x 新增了delegation.reasoning_effort配置,允许子代理使用独立于父代理的推理强度。