Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

子代理与委托:递归编排的设计

本章核心源码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 处理。这听起来像一次普通的工具调用,但实际涉及三个非平凡的工程问题:

  1. 资源隔离:子代理需要独立的 iteration budget、session、memory context
  2. 中断传播:用户中断时,父代理需要通知所有活跃的子代理
  3. 深度控制:防止子代理递归 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=Trueskip_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

解析优先级遵循"显式覆盖 > 继承":

  1. delegation 配置覆盖:如果 delegation.reasoning_effort 有值且合法,子代理使用该值
  2. 继承父代理:否则继承父代理的 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_taskclarifymemorysend_messagetools/delegate_tool.py:29-36)。这不是遗漏。团队将能力边界视为架构不变量而非用户偏好:允许子代理递归委托会造成无界的资源消耗;允许子代理修改共享记忆会破坏隔离保证。这些约束在设计层面就是不可商量的。

Memory Provider 的 on_delegation 钩子

当子代理完成任务后,MemoryProvideron_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 路径——它永远不会注入一个猜测的路径如 /workspacetools/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 Loopon_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 配置,允许子代理使用独立于父代理的推理强度。