← 返回教程库

子代理零上下文成本:拆解 hermes-agent 的 subagent + Python RPC 流水线

最后更新 2026-06-21
你将学到
  • 搞懂"上下文成本"为什么是多步 Agent 越跑越笨的元凶
  • 看懂 hermes-agent 的隔离子代理:独立对话+独立终端+Python RPC,主 Agent 只收结果
  • 学会用 RPC 把一条多步流水线"折叠"成主上下文里的一行
  • 用标准库 subprocess/json 写出一个仿 hermes 思路、能真跑的最小派发器
  • 拿到"什么活值得开子代理、什么活主 Agent 直接干"的判断口诀

你有没有遇到过这种情况:让 Agent 办一件需要好几步的活——先翻一堆日志、再跑个脚本、再读返回值——它办着办着就变笨了。前面塞进上下文的那一大坨日志、命令回显、中间产物,把后面要紧的指令全挤没了。这不是模型不行,是上下文被垃圾撑爆了

开源项目 hermes-agent(NousResearch 出品)对这件事给了一个很漂亮的解法:把脏活丢给隔离的子代理去干,主 Agent 只收一行干净结果。 这一节我们把它的「subagent + Python RPC」流水线拆开,看懂它怎么做到"多步流程不占主上下文",再用标准库写一个你今天就能跑的最小版本。

这篇适合谁:已经能写出一个能跑的 Agent,但被"上下文越用越乱"卡住的人。读完你会有一套把脏活"折叠"出主上下文的具体打法。


钩子:先算一笔"上下文账"

假设主 Agent 要完成"检查服务健康度",朴素写法是它自己一步步来:

  1. cat error.log,回显 2000 行——全进上下文
  2. 跑统计脚本,输出一屏——也进上下文
  3. 再读配置文件——还进上下文
  4. 最后才说一句"服务正常,错误日志里有 3 条超时"。

你真正想要的只有第 4 句话,前面三步的几千 token 全是过程垃圾。它们留在上下文里有两个害处:一是越积越多把后续指令挤掉,二是每多一轮都要带着这堆垃圾重新喂给模型,又贵又慢。

hermes 的思路:第 1~3 步根本不该在主 Agent 的对话里发生。


最小可用:hermes 的隔离子代理长什么样

hermes 让主 Agent 能派一个子代理去干一段独立任务。这个子代理的关键特征是「隔离」:

  • 它有自己的对话历史,和主 Agent 完全分开。
  • 它有自己的终端/工作环境,命令回显落在它自己的上下文里。
  • 它跑完之后,只把最终结果回传给主 Agent——中间那几千行日志,主 Agent 一个字都看不到。

这就是「零上下文成本」的含义:不是真的零开销,而是子任务的过程开销零地占用主 Agent 的上下文。 主 Agent 的对话里,那一整段流水线只留下一行"服务正常,3 条超时"。

而连接主、子的桥,hermes 用的是 Python RPC:主代理不是一步步口述命令,而是把"要干的一串活"写成一段 Python 脚本,丢给子环境执行,子环境跑完把返回值序列化回来。一段脚本里可以连着跑好几个工具调用、做好几次判断——这一整串多步流程,在主上下文里被"折叠"成了一次调用、一个返回值。

折叠示意(主 Agent 视角):

折叠前(脏):cat 日志 → [2000行] → 跑脚本 → [一屏] → 读配置 → [一屏] → 结论
折叠后(净):派子代理跑 check_health.py  →  收到 "{healthy: true, timeouts: 3}"

中间的方括号全部留在子代理那边,主 Agent 的上下文只多了最后一个 JSON。


原理:为什么 RPC 比"一条条发命令"省

很多人写 Agent 用工具是这样的:模型说"跑命令 A"→ 框架执行 → 把回显塞回对话 → 模型看了回显说"跑命令 B"……每跑一步都往主上下文塞一次回显,还要多一轮模型推理。 五步流程就是五轮往返、五坨回显。

RPC 折叠把这件事压扁:模型一次性写出"A 然后 B 然后 C,把 C 的结果返回"的脚本,一次派发、一次返回。五步在子环境里跑完,主上下文只增加最后那一个返回值。省的是两样东西——主上下文的 token,和来回往返的模型轮次。

类比一下:朴素工具调用像你站在仓库门口,每次只让搬运工搬一箱、搬来给你看一眼、你再说搬下一箱;RPC 折叠像你递给搬运工一张清单"这十箱按这个顺序搬完,搬完告诉我总重"——你只在最后看到一个数字。


进阶:用标准库写一个能真跑的最小派发器

下面这段不依赖 hermes 本体,只用 Python 标准库 subprocess + json 就能真跑。它复刻 hermes 的核心思路:主进程派子任务、子进程在隔离环境里跑多步、只把结果序列化回来。

先写「子代理脚本」——它就是被折叠的那一串脏活,跑在自己的进程里:

# subagent_health.py —— 子代理:自己干一整串脏活,只在最后打印一行 JSON 结果
import json, sys

def read_log():
    # 真实场景这里 open("error.log").read(),可能是 2000 行——但它只活在本进程
    return "ERROR timeout\n" * 3 + "INFO ok\n" * 500

def run():
    log = read_log()                                  # 第1步:脏(大)
    timeouts = log.count("timeout")                   # 第2步:在本地把大输入嚼成小结论
    healthy = timeouts < 10                            # 第3步:判断
    # 关键:只把"主 Agent 真正要的东西"序列化出去,过程一概不外泄
    print(json.dumps({"healthy": healthy, "timeouts": timeouts}))

if __name__ == "__main__":
    run()

再写「主进程派发器」——它就是主 Agent 的工具,负责派子代理、只接结果:

# dispatcher.py —— 主 Agent 侧:派一个隔离子代理,只拿回最终结果
import subprocess, json

def dispatch(script_path: str, timeout: int = 30) -> dict:
    """跑一个子代理脚本,返回它打印的最后一行 JSON。
    子进程的 stdout 全程不进主上下文——这就是'零上下文成本'。"""
    proc = subprocess.run(
        ["python", script_path],
        capture_output=True, text=True, timeout=timeout,
    )
    if proc.returncode != 0:
        # 子代理炸了,也只把精简的错误摘要带回主上下文,别把整坨 traceback 塞回去
        raise RuntimeError(f"子代理失败(code={proc.returncode}): {proc.stderr.strip()[:200]}")
    last_line = proc.stdout.strip().splitlines()[-1]   # 只认最后一行结构化结果
    return json.loads(last_line)

if __name__ == "__main__":
    result = dispatch("subagent_health.py")
    # 主 Agent 的上下文里,这一整条流水线只留下了下面这一行:
    print("主 Agent 收到:", result)

把这俩放同一目录,python dispatcher.py 即可。

逐步预期

  • 子代理 subagent_health.py 内部"读了 500 多行日志",但这 500 行只活在子进程的 stdout 缓冲里
  • 派发器只 json.loads最后一行——{"healthy": true, "timeouts": 3}
  • 终端打印:主 Agent 收到: {'healthy': True, 'timeouts': 3}
  • 也就是说:主 Agent 这一步上下文只增加了一个十几字符的字典,那 500 行日志它永远不会知道。这就是把多步流水线折叠掉的效果。

read_log 换成真去读一个大日志文件、把 print 的内容换成真实结论,这段就是生产可用的骨架了。


变体:并行派发多个子代理

子代理之间互不污染上下文,那"同时派好几个"就很自然——比如一次性查健康度、查磁盘、查证书到期,三件互不依赖的活并行跑:

# 并行派多个隔离子代理,各自折叠,主上下文只汇总三行结果
import subprocess, json
from concurrent.futures import ThreadPoolExecutor

def dispatch(script):
    proc = subprocess.run(["python", script], capture_output=True, text=True, timeout=30)
    return json.loads(proc.stdout.strip().splitlines()[-1])

jobs = ["subagent_health.py", "subagent_disk.py", "subagent_cert.py"]
with ThreadPoolExecutor(max_workers=3) as pool:
    results = list(pool.map(dispatch, jobs))   # 三个脏活并行,各自的过程都不进主上下文

print("主 Agent 汇总:", results)   # 只多了三行干净结论

三个子代理各读各的大文件、各跑各的命令,主上下文只多了三个小字典。这就是 hermes 思路在多任务下的延展:隔离不只省上下文,还顺手解锁了并行。


避坑:故障排查表

现象 原因 怎么破
主上下文还是越来越大 子代理把整坨过程也 print 出来了 子代理只 print 最后一行结构化结果,调试日志走 stderr 或日志文件
json.loads 报错 子代理 stdout 混进了非 JSON(print 了调试信息) 约定"最后一行必须是 JSON",调试输出一律走 stderr
子代理卡死、主进程跟着卡 没设超时 subprocess.run(..., timeout=N),超时按失败处理并回传精简摘要
子代理失败把整坨 traceback 带回主上下文 直接把 stderr 全文塞回去 只截取前 200 字摘要回传,详细日志留在子代理侧
什么活都开子代理,反而更慢 起进程/序列化本身有固定开销 小活、单步活直接主 Agent 干,别为省几十 token 多起一个进程
子代理之间想共享中间结果 用了隔离就拿不到对方过程 需要共享就别拆成独立子代理,或把共享数据显式写到文件/参数里传

增量:什么活值得开子代理,什么活主 Agent 直接干

这是用好这套设计的真正分水岭。给你一个判断口诀——「脏、长、可独立」三条占两条就开子代理;否则主 Agent 直接干。

  • :过程会吐大量中间垃圾(大日志、长回显、一堆探索性命令),但你只要一个结论 → 开子代理折叠掉。
  • :要好几步串起来才出结果(读→算→判→再读)→ 写成一段 RPC 脚本一次折叠。
  • 可独立:这段活不需要主 Agent 的上下文细节,给个目标就能自己干完 → 适合隔离。

反过来,主 Agent 直接干的情形:单步就出结果、输出本来就很短、或者这一步强依赖主对话里的上下文(需要边干边跟用户确认)。给单行命令套个子代理,只会白白多一次进程启动和序列化开销。

一句话:子代理是用来"折叠脏活、还主 Agent 一个干净上下文"的,不是用来包装每一次工具调用的。


动手挑战

  1. 把上面的 subagent_health.py 改成真去读一个大文件(比如你机器上某个日志),确认主进程拿到的只有结论、终端里看不到那一大坨内容。
  2. 给派发器加一个"参数透传":让主进程能把目标(比如要查的服务名)作为命令行参数传给子代理,子代理 sys.argv 接收。
  3. 进阶:把并行版本接到你现有 Agent 上,做一个"一键体检"工具——一次并行派三个子代理,主 Agent 只汇总三行结论。体会一下上下文比朴素写法干净了多少。

小结 · 你现在掌握了什么

  • 你看懂了"上下文成本"才是多步 Agent 越跑越笨的元凶,而不是模型不行。
  • 你理解了 hermes-agent 隔离子代理的核心:独立对话+独立终端+Python RPC,主 Agent 只收一行结果,过程垃圾零占用主上下文。
  • 你会用 RPC 把"读→算→判"的多步流水线折叠成主上下文里的一次调用、一个返回值。
  • 你能用标准库 subprocess/json 写出能真跑的最小派发器,还能并行派多个子代理。
  • 你拿到了"脏、长、可独立占两条就开子代理"的判断口诀。

记住:隔离不是为了花哨,是为了让主 Agent 始终在一个干净、要紧的上下文里思考。 这一节解决了"怎么把一个 Agent 内部的脏活折叠掉";下一节我们跨出单个 Agent,看 A2A 协议怎么让不同 Agent 之间互相发现、互相委派。

下一步:往后是跨 Agent 协作。看 AI Agent 智能体阶梯的 L5 后段;想看整条路的位置就对照三支柱路线图。前置如果没读,先补 hermes 的文件即记忆AI Agent 是什么

👉 看看 AI 数字员工落地指南,或了解 数字员工搭建实战课。需要为企业落地方案,欢迎找我们聊 企业服务

📄 来源 / 自校链接

本文为学习整理,关键步骤与代码请结合下列官方来源验证。

内容有错、看不懂、或想看下一期?告诉我们 →

本文为学习与落地整理,AI 工具与平台更新较快,关键步骤请结合官方最新资料验证。见免责声明