子代理监督:Hermes 对你隐藏的循环
子代理监督:Hermes 对你隐藏的循环
### 承诺
Hermes Agent 自带一个 delegate_task 工具。父代理将目标交给子代理,子代理在自己的会话中运行,使用自己的工具,然后返回一份摘要。理论上,这让你可以并行研究、隔离代码运行、执行多步骤任务,同时不阻塞主对话。
实践中,父循环信任摘要,忽略副作用,几乎不保留任何审计痕迹。Dr J 已经记录了这一信任契约失败的各种情况。本文是工程跟进:Hermes 父循环实际做了什么,监督缺口在哪里,以及我一直在运行的一个小型 Python 工具框架来填补这些缺口。
我写这篇文章是为了给那些已经在使用 Hermes、并且希望不再猜测子代理是否真正交付了结果的开发者。
从父视角看 delegate_task 的样子
当你在 Hermes 会话中调用 delegate_task 时,该工具大致执行以下操作:
- 生成一个带有全新 HermesState 会话的 AIAgent 实例。
- 在有界循环中运行子代理(max_iterations 可配置,默认通常为 50)。
- 收集最终助手文本并作为 JSON 字符串返回给父代理。
父代理收到一个类似这样的数据块:`json
{
"summary": "I researched ROCm gfx1151 support on Linux 6.17. The kernel module loads with amdgpu...",
"status": "success"
}`
如果子代理达到 max_iterations、出错或没有生成最终文本,工具可能会返回一个错误字段。但父代理不需要验证摘要是否符合原始目标,不需要检查文件产物,也不需要保留委托事件的持久日志。工具契约是:我给你了一段文本。祝你好运。
这就是 Hermes 对你隐藏的循环。它并非恶意;它只是不完整。
三个监督缺口
在过去一个月中,我在实际任务上运行委托后,发现了父循环中三个一致的失败点:
| 缺口 | 症状 | 为什么有害 |
|---|---|---|
| 1. 仅返回摘要 | 父代理看到的是润色后的文字,而非结构化产物。 | 无法验证数量、路径或模式。 |
| 2. 无副作用检查 | 子代理声称已写入文件/调用 API/推送提交。 | 父代理从不确认文件系统或服务状态。 |
| 3. 无持久审计记录 | 每次委托只存在于一个会话转录中。 | 取证、计费和安全审查变得不可能。 |
这些缺口会叠加。如果父委托了十个并行研究任务,它会收到十份摘要。如果其中两份不完整,一份是捏造的,三份产生了父代理从未读取的文件,那么代理会话的其余部分就建立在部分虚假的基础上。
最小委托契约
解决方案不是停止委托。解决方案是给每个委托包裹一个父代理可以确定性执行的小型契约。
我使用一个三部分契约:
- 输入模式:确切的目标、预期的交付物和成功标准。
- 输出模式:子代理必须返回的内容,包括机器可读的产物。
- 验证步骤:父代理在使用结果之前检查产物是否符合契约。
以下是我在 Python 中使用的具体形式:`python
from dataclasses import dataclass
from typing import List, Dict, Any, Optional
import json
@dataclass
class DelegateContract:
goal: str
deliverables: List[str] # e.g. ["price_table.json", "summary.md"]
success_criteria: List[str] # e.g. [">= 5 providers", "each row has price"]
artifacts: Dict[str, Any] = None # populated by verifier
passed: Optional[bool] = None
def to_prompt(self) -> str:
return f"""
You are a delegated subagent. Your goal: {self.goal}
Required deliverables:
{chr(10).join(f"- {d}" for d in self.deliverables)}
Success criteria:
{chr(10).join(f"- {c}" for c in self.success_criteria)}
Return a JSON object with two keys:
- "artifacts": a dict mapping each deliverable to its contents or path
- "summary": a concise human-readable explanation
"""`
子代理被告知输出 JSON。父代理解析它并在接受任何内容之前运行验证。
验证:检查真实世界
关键见解是父代理必须在子代理返回后执行 I/O。验证器需要四个原语:
- file_exists(path)
- file_contains(path, substring)
- json_schema_ok(data, required_keys)
- count_at_least(data, n)
以下是我在文件系统产物上运行的小型验证器类:`python
import os
from pathlib import Path
class ArtifactVerifier:
def __init__(self, work_dir: Path):
self.work_dir = work_dir.resolve()
def _safe(self, path: str) -> Path:
p = self.work_dir / path
p = p.resolve()
if not str(p).startswith(str(self.work_dir)):
raise ValueError(f"path traversal rejected: {path}")
return p
def file_exists(self, path: str) -> bool:
return self._safe(path).exists()
def file_contains(self, path: str, needle: str) -> bool:
p = self._safe(path)
if not p.exists():
return False
return needle in p.read_text()
def json_load(self, path: str) -> dict:
p = self._safe(path)
return json.loads(p.read_text())`
路径遍历通过 resolved-prefix 检查而非 naive string prefix 来拒绝。工作目录是可配置的。这与我们在 smf-praxis 中对真实工具应用的沙箱逻辑相同。
然后,验证器使用声明式检查清单:`python
def verify_research_contract(contract: DelegateContract, verifier: ArtifactVerifier) -> bool:
ok = True
notes = []
# Required artifact exists
if not verifier.file_exists("price_table.json"):
ok = False
notes.append("missing price_table.json")
else:
table = verifier.json_load("price_table.json")
if len(table) < 5:
ok = False
notes.append(f"only {len(table)} providers, expected >= 5")
missing_prices = [r for r in table if "price" not in r]
if missing_prices:
ok = False
notes.append(f"{len(missing_prices)} rows lack price key")
contract.passed = ok
contract.artifacts["verification_notes"] = notes
return ok`
如果验证失败,父代理会做以下三件事之一:
1. 用更严格的提示重试委托。
2. 将验证笔记上报给用户。
3. 将结果降级为“部分”并限制下游使用。
我默认选择选项 3。如果父代理知道它是部分的,部分结果仍然有用。
将其接入 Hermes
Hermes 没有暴露第一类验证器钩子,所以我包装了工具调用。思路是:调用 delegate_task,解析它返回的 JSON,然后在父循环消费摘要之前作为第二步运行验证器。
`python
def run_delegation(goal: str, contract: DelegateContract, parent_ctx):
# 1. call Hermes delegate_task
raw = parent_ctx.tools.delegate_task(
task=contract.to_prompt(),
context=goal,
timeout_seconds=180
)
# 2. parse the structured return
try:
payload = json.loads(raw)
except json.JSONDecodeError:
return {"status": "parse_error", "raw": raw}
# 3. verify artifacts
verifier = ArtifactVerifier(Path(os.environ.get("PRAXIS_WORK_DIR", "/tmp/agent-work")))
passed = verify_research_contract(contract, verifier)
return {
"status": "verified" if passed else "failed",
"contract": contract,
"payload": payload
}`
现在父代理可以根据状态做出决策。这就是缺失的监督循环。
那么 hermes chat -q 子进程呢?
Hermes 也允许你通过 terminal() 或 hermes chat -q 生成独立的 Hermes 进程。这些不是 delegate_task——它们是完全独立的代理——但监督问题是一样的。你得到 stdout、stderr 和一个退出码。除非你主动要求,否则你不会得到结构化产物。
我处理子进程委托的模式:`bash
hermes chat -q "Research local LLM inference on AMD ROCm. Write findings to /tmp/agent-work/rocm-report.json as JSON with keys: supported_gpus, driver_versions, recommended_tools, caveats." --toolsets web,file -Q`
然后父代理用相同的验证器验证 /tmp/agent-work/rocm-report.json。子进程拥有完整的工具访问权限,但父代理仍然强制执行契约。
保留审计跟踪
每个委托事件都应该记录足够的上下文供日后审查:`json
{
"timestamp": "2026-07-08T10:23:17Z",
"parent_session": "20260708_050054_c54d44",
"delegate_goal": "research ROCm gfx1151 support",
"contract_deliverables": ["rocm-report.json"],
"status": "failed",
"verification_notes": ["missing rocm-report.json"],
"raw_summary_length": 1420
}`
我将这些追加到项目保管库中的一个 JSONL 文件中。对于 SMF Works 项目,这是通过我们的跨通道上下文桥强制执行的;每个出站消息和每个重要的代理动作都会被记录,这样不同的通道可以在没有失忆的情况下接续线程。委托也应得到同样的待遇。
决策树:信任、验证或重试
以下是委托返回时我使用的确切流程:`
Did the child return valid JSON?
├─ No → log parse_error, retry once with "output must be JSON"
└─ Yes → artifacts present?
├─ No → mark incomplete, ask child to produce artifacts
└─ Yes → run verifier
├─ pass → accept, use in parent loop
└─ fail → inspect notes
├─ recoverable → retry with narrower goal
└─ unrecoverable → escalate to user`
决策永远不是“默认信任”。而是“验证后信任,可恢复失败则重试,不可恢复失败则上报”。
Hermes 可以添加什么
这些都不需要分叉。但 Hermes 核心可以使其更简洁:
- 结构化委托输出:让 delegate_task 可选地强制 JSON schema 返回。
- 产物清单:允许子代理声明它创建的文件,并向父代理暴露验证钩子。
- 内置审计日志:将委托事件与父/子会话 ID、状态和 token 成本一起存储在 HermesState 中。
在此之前,上面的工具框架是有效的。我已经在我们的 smf-praxis 代理和独立研究脚本中运行它。
注意事项
这会增加延迟。每个经过验证的委托至少花费一次额外的工具调用加上文件系统检查。对于速度比正确性更重要的任务,你可能想要一个更轻的契约。
它假设子代理能够遵循 JSON 输出指令。较小的本地模型有时会忽略 schema。在这些情况下,我添加一个使用小型清理模型的第二轮,或者使用正则表达式提取作为后备。
文件系统验证只有在父代理和子代理共享工作目录时才有效。子进程委托需要一个明确的目录;delegate_task 委托在同一个进程树内,因此如果你设置了 PRAXIS_WORK_DIR,它们可以继承它。
总结
Hermes 的 delegate_task 很强大,因为它隔离了工作。它很危险,因为父循环将返回的摘要视为地面实况。添加一个三部分契约——输入模式、输出模式、验证步骤——你就可以将委托从一个黑盒变成一个可检查的流水线。从 DelegateContract 数据类开始,加上一个检查真实文件系统的 ArtifactVerifier,以及一个决策树:重试可恢复失败,上报其余部分。
这里的代码不是假设的。我今天就在我们的代理上运行它。如果你在委托给子代理时不进行验证,你就是在盲飞。循环就在那里。你只需要关闭它。