🤖HermesBlog
Hermes 功能详解 · 第12篇2026/8/9· Easy Understand Hermes Agent

第12篇:子代理委派——AI 的分身术

Hermes 功能详解第12篇:子代理。把大任务拆给多个 AI 并行处理,效率翻倍。

这篇讲的是:如何让 Hermes Agent 同时派出多个“分身”去干活,再把结果汇总给你——就像你同时雇了三个实习生,各干各的,最后交报告。


子代理委派:同时雇三个实习生

为什么要用“分身”?

想象一下:你让一个助手去查资料、改代码、做调研。如果它一件一件做,又慢又容易乱。子代理委派(Subagent Delegation)就是让主代理临时创建几个独立的“小代理”,每个小代理有全新的对话记忆,只专注于你交给它的那一件事。干完后,它只把最终结论汇报给主代理,中间过程完全不打扰你。

⚠️ 重要:分身什么都不知道!
小代理的脑子里是空的。它不知道你之前聊了什么,也不知道“那个错误”指的是什么。你必须在派活时,把所有背景信息写清楚。

(有个小例外:如果主代理已经确定了工作目录,那这个目录里的项目上下文文件——.hermes.md、AGENTS.md 那一串、CLAUDE.md、.cursorrules——会自动塞进分身的系统提示里。所以分身进到仓库里干活,能直接按仓库自己的规矩来,不用重新摸索一遍。)

另外,分身不能随便用某些工具:delegate_task(叶子分身不能用,只有 role="orchestrator" 的编排分身才保留,还受 max_spawn_depth 限制)、clarify(不能和用户交互)、memory(不能写共享记忆)、send_message(不能跨平台发消息)、cronjob(不能以主代理名义排新任务)、start_chat(不能以用户名义开新对话)。两种角色都保留 execute_code(程序化工具调用),这样分身可以批量处理机械活,而不是把推理迭代烧光。


第1步:派一个分身干单件事

最基础的用法,就是给一个目标(goal)和背景(context):

delegate_task(
    goal="调试为什么测试失败",
    context="错误信息:test_foo.py 第42行断言失败"
)

注意:背景写得越详细,分身干得越好。下面这个例子就是“坏”和“好”的对比:

# ❌ 坏——分身不知道“那个错误”是什么
delegate_task(goal="修复那个错误")

# ✅ 好——所有信息都给全了
delegate_task(
    goal="修复 api/handlers.py 里的 TypeError",
    context="""文件 api/handlers.py 第47行报错:
    'NoneType' object has no attribute 'get'。
    函数 process_request() 从 parse_body() 拿数据,
    但 Content-Type 缺失时 parse_body() 返回 None。
    项目在 /home/user/myproject,用 Python 3.11。"""
)

第2步:一次派多个分身(并行批处理)

默认最多同时跑 10 个分身(可以改配置,没有硬性上限)。把任务放在一个列表里就行:

delegate_task(tasks=[
    {"goal": "调研主题A", "context": "重点关注近期原始资料"},
    {"goal": "调研主题B", "context": "比较主流解释"},
    {"goal": "修复构建失败", "context": "项目根目录:/home/user/project"}
])

Hermes 会并行跑这三个任务,全部完成后,把结果按你给的顺序汇总成一条消息发给你。


第3步:给分身换一个更便宜的模型

分身默认用和主代理一样的 AI 模型。如果你想省钱,可以让分身用便宜快速的模型:

# 在 ~/.hermes/config.yaml 里
delegation:
  model: "google/gemini-flash-2.0"   # 便宜模型
  provider: "openrouter"             # 可选:换供应商

在 Desktop 里,同样的键在 Settings → Model → Subagents 页面设置。该页面编辑的是 Applies to 下选中的 profile,只影响你点 Apply 之后启动的分身,不会改动主模型,也不会影响已经在跑的分身。


第4步:控制分身的工作量

  • 最大迭代次数:分身最多能调用多少次工具(默认250次)。这个值在 config.yaml 里全局设置,对所有分身生效,不是 delegate_task 的每次调用参数(默认策略下如此;换成下面的委派策略插件后,模型可以按任务挑选项):
# 在 ~/.hermes/config.yaml 里
delegation:
  max_iterations: 60   # 简单任务可以调小,长调查可以调大

分身把预算用光后会带着 exit_reason: max_iterations 和 truncated: true 返回,父代理就能区分“预算用完”和“任务完成”。

  • 超时时间:默认没有硬性超时。以前有10分钟上限,结果深度代码审查这种活经常被掐断。现在只要分身还在干活(比如在等模型回复),就不算卡死。只有它完全没动静才会被判定为“卡住”——心跳监控会盯着每个分身的进度信号(API 调用、工具启动、活动时间戳),一旦彻底冻住(两轮之间闲置 450 秒,或者卡在某个工具里 1200 秒),就把它中断掉,父代理会收到一条 status: "timeout" 的结果。这样即使是一次性运行(比如 hermes chat -Q、Bot Chat 单次、cron)也不会被一个卡死的分身永远拖住。

    如果你确实想加个上限(比如无人值守的 cron 委派要控成本),可以按安装逐个开启:

    # child_timeout_seconds: 1800  # 可选:闲置上限(下限 30 秒)

    正数限制的是闲置时间,不是总运行时长:它指的是分身完全没进展(没有完成的 API 调用、没有工具状态变化、没有活动时钟跳动)能持续的最长时间,超过才被放弃。任何一点进展都会重置这个窗口,所以一个在等好几分钟才返回的 completion 的分身——正是以前会丢掉已完成工作的那种情况——不会因为耗时长被杀掉;而一个真的停住不动的分身依然会被抓住(正在飞行的请求由每次调用的 stale watchdog 单独兜底)。填 0 或负数则关闭这个上限;不管开不开,下面的心跳陈旧监控都照常生效。

    当闲置窗口走到大约 80% 时,分身会通过它的 steer 通道收到一行 [delegation budget warning](在它下一个迭代边界送达),告诉它已经闲置了多久、让它现在就返回总结——这样一个慢但还能救的分身可以自己收尾,而不是丢掉上下文。这个警告每个闲置窗口只触发一次,进展恢复后会重新武装。

    超时结果里还会带上 timeout_seconds(实际生效的那个限制——如果陈旧阈值先于配置的上限触发,就是陈旧阈值,否则就是配置的上限)、timed_out_after_seconds(实际墙钟时间)、last_event_age(等待结束时分身已经沉默多久——快速区分是供应商慢还是分身跑飞了)和 timeout_phase(比如 after_llm_calls)。非超时错误时这四个字段都是 null。


第5步:用 output_schema 约束分身的输出格式

如果你希望分身返回的结果是固定结构(比如 JSON),可以给任务加一个 output_schema——它是一个 JSON Schema 对象,分身的最终答案必须通过它的校验。

分身一开始就能看到这个 schema,相当于一份“输出合同”(会明确告诉它:只返回 JSON 值,别写散文、别加代码围栏)。答案回来后,主代理会校验;如果没通过,会给分身一次纠正机会,把具体的校验错误原样告诉它(不会重新贴一遍 schema)。任务结果里会多出 schema_valid(true/false),失败时还有 schema_errors。

就算纠正后还是没通过,也不会丢掉分身干的活:结果依然是 status: completed,summary 里保留它的原始最终文本,schema_valid: false,附上 schema_errors 和一条 schema_note 说明这段文本没经过校验。父代理直接从原始文本里挑需要的东西就行,不用把一个可能跑了一小时的任务重跑一遍。另外,如果 JSON 外面包了层散文或者代码围栏(对象或数组都行),校验器是能容忍的。

delegate_task(
    tasks=[{
        "goal": "检查这三个端点哪些返回 200",
        "context": "https://a.example, https://b.example, https://c.example",
        "output_schema": {
            "type": "object",
            "properties": {
                "healthy": {"type": "array", "items": {"type": "string"}},
                "failing": {"type": "array", "items": {"type": "string"}}
            },
            "required": ["healthy", "failing"]
        }
    }]
)

写 schema 时别太苛刻:只要求你真正会读的字段。没写 output_schema 的任务不受影响。


第6步:给分身递图片

有些活光靠文字说不清——比如用户发来的截图、设计稿、渲染出来的图表。这时候可以给任务加一个 images 列表(最多 8 个;支持本地文件路径、http(s) 链接或者 data:image/... 链接):

delegate_task(tasks=[{
    "goal": "对比渲染出的仪表盘和设计稿,列出布局偏差",
    "context": "应用跑在 http://localhost:3000;仓库在 /home/user/dash。",
    "images": ["/home/user/mocks/dashboard-v2.png",
               "https://cdn.example.com/current-render.png"],
}])

图片的走法和用户自己发的图一样(看 agent.image_input_mode 配置):

  • 分身模型支持视觉:图片会作为原生多模态内容出现在它的第一轮对话里,本地文件会转成 data URL(跟其他文件读取一样受读取守卫限制),远程和 data: 链接原样传过去。分身看到的是真正的像素。
  • 分身模型不支持视觉:目标里会加上 [Image attached at: <path>] 这样的提示行,并告诉分身用 vision_analyze 去看。

图片转发是尽力而为的:读不了的路径会跳过并记一条日志,图片环节出任何问题都会退回纯文本目标——绝不会因此让分身创建失败。图片是给分身必须亲眼看的东西用的;普通的文本文件路径还是照常写在 context 里。


第7步:在 dock 里盯着分身和后台进程

分身跑起来后,你可以在 dock 里实时看到它们的状态。后台进程也共用这个 dock:只要代理用 terminal(background=true) 起了东西(构建、测试、开发服务器、CI 轮询),它一 spawn 就会出现在子代理行下面的 Processes 区块里——显示成 ⚙ <命令> · 42s · last: <最新输出行>,结束时原地翻成 ✔ exit 0 / ✘ exit 1 / ✘ killed,大约 60 秒后从 dock 消失(对话里的完成通知才是持久记录)。在 Classic CLI 监控器里,进程行排在代理下面:Enter 看该进程的日志尾部,x 停掉这一个进程;进程不能被 steer。TUI 的 /agents 浮层会在 spawn 树下列出同一个区块(/stop 会结束所有后台进程)。Desktop 则把这些进程显示成输入框上方的卡片。

后台进程也共用这个 dock:只要代理用 terminal(background=true) 起了东西(构建、测试、开发服务器、CI 轮询),它一 spawn 就会出现在子代理行下面的 Processes 区块里——显示成 ⚙ <命令> · 42s · last: <最新输出行>,结束时原地翻成 ✔ exit 0 / ✘ exit 1 / ✘ killed,大约 60 秒后从 dock 消失(对话里的完成通知才是持久记录)。在 Classic CLI 监控器里,进程行排在代理下面:Enter 看该进程的日志尾部,x 停掉这一个进程;进程不能被 steer。TUI 的 /agents 浮层会在 spawn 树下列出同一个区块(/stop 会结束所有后台进程)。Desktop 则把这些进程显示成输入框上方的卡片。

在 Classic CLI 或 TUI 的输入框里按 Ctrl+R,可以在多行预览和单行阴影摘要之间切换 dock。终端能转发功能键时,F7 仍是可选的别名。摘要保留实时计数和展开/还原提示,空间够时还会加上活动信息。打字和发送都不受影响;打开、关闭监控器会保留你的草稿和插入点。这只是本地显示选择,不会写进配置。


第8步:分身什么时候“压缩记忆”?

分身干久了,对话上下文也会变长,需要压缩(compaction)——这一步和主代理用的是同一套规则:

  • 默认在 compression.threshold × 上下文窗口时触发(默认比例 0.50,也就是用到一半就开始压)。
  • 如果全局设了 compression.threshold_tokens 上限,且它比比例阈值更低,就按这个更低的上限来。

如果你想让某个分身更早开始压缩(比如怕它跑飞、想省点 token),可以加一个可选的绝对上限:

# 在 ~/.hermes/config.yaml 里
delegation:
  compression_threshold_tokens: 0   # 默认 0,即关闭

这个值只影响分身什么时候触发压缩,取“它”和“比例阈值”里更低的那一个;它不会动请求内容,也不会影响主代理。注意两个坑:

  • 数值至少要 16000 才会生效,比这更小等于没设。
  • 写成 true 或者 "200k" 这种是配置错误——Hermes 会警告并忽略,不会报错退出。

为什么默认关着? 官方做过一次 1393 个分身的大回放,发现只要缓存前缀没坏,200K 和 400K 这两种上限的成本差距在 5% 以内——省不了多少钱;而每一次压缩都是一次丢细节的机会。所以默认不开,需要时再手动加。


在消息平台上:你想收到“一条”还是“两条”?

后台委派在聊天平台上的默认行为是:父代理先发一句“我在弄了”,结束这一轮,分身跑完再发真正的答案。但如果那个平台关掉了中间消息(display.interim_assistant_messages: false),那句“我在弄了”就成了这一轮的最终答复,于是你会先收到一句废话、再收到答案——两条消息。

delegation.gateway_mode 就是管这个的:

delegation:
  gateway_mode: auto   # auto(默认)| inline | background
  • auto(默认):消息平台上如果该平台解析出来的 display.interim_assistant_messages 是关的(看全局值、display.platforms.<平台> 覆盖或平台默认值),就把分身“拉进本轮”,父代理在 delegate_task 调用里等结果,然后只发一条完成的答复;中间消息开着的聊天、API server、本地会话和 webhook 仍然用后台委派。
  • inline:所有网关会话都在本轮里等分身跑完。
  • background(别名 async):所有网关会话都用后台委派。

inline 会话的 delegate_task 会把结果在调用里返回,所以也不带那几个实时控制参数(action list/steer/stop、subagent_id、message),和一次性运行一样。等待期间“正在输入”一直亮着,这一轮就像一个普通的长工具调用;/stop 会停掉分身,默认 busy_input_mode: interrupt 下的后续消息也会(每个分身返回 status="interrupted" 和它已完成的部分输出)。

超过 agent.gateway_notify_interval(默认 180 秒)的 inline 轮次仍然会收到周期性的“还在干”提示,它由 display.long_running_notifications 单独控制(Telegram、Slack、Discord、WhatsApp 默认开)。想严格做到“一轮一条消息”,把那个平台的它一起关掉:

display:
  interim_assistant_messages: false
  platforms:
    telegram:
      long_running_notifications: false

改 gateway_mode 或 interim_assistant_messages 属于会话工具 schema 的一部分,下一个会话(/new)才生效;正在跑的聊天会一直用它启动时的模式,跨网关重启、上下文压缩、/reload-mcp 都不变。

实用场景举例

并行调研:一次查三个话题(WebAssembly、RISC-V、量子计算),各写各的报告。

按任务挑模型:装上官方 advanced-delegation 插件,把 delegation.backend 设成 model_map,你就能列出 fast、deep 这样的带描述的选项(各自指定 provider/model),让模型按任务自己挑;或者用 profile_routes 让每个分身以某个 Hermes profile 的身份运行(带那个 profile 的模型、凭据、技能和人设)。模型只看得到你配置的名字和描述,看不到原始 provider/model/key 字段。路由失败是显式的:后端缺失会直接报错、不启动任何分身;批处理里某个任务路由不了,只有它自己返回 status: "error" 和 route.refused: true,其余照跑。想切回来,用 /delegation default(下次会话生效),或 /delegation <backend> --now 让当前对话下一轮就换过去。

代码审查+修复:让分身在一个全新环境里审查登录模块的安全性,发现问题直接改,然后跑测试。

大范围重构:把“把所有 print() 改成 logging”这种会刷爆主代理记忆的活,丢给分身去干,它干完只汇报结果。


小总结

子代理委派就是把大任务拆成小任务,分给多个“失忆”但专注的分身。关键记住两点:背景信息给足、任务描述清楚。这样你就能同时干好几件事,效率翻倍。

下篇预告:分身们干活时,你还能继续和主代理聊天——下一篇讲讲后台任务与消息队列,看看 Hermes 是怎么做到“边聊边干”的。

📖 官方文档

本文根据 Hermes Agent 官方文档编写,原文见:官方文档 › user-guide/features/delegation