子代理零上下文成本:拆解 hermes-agent 的 subagent + Python RPC 流水线
- 搞懂"上下文成本"为什么是多步 Agent 越跑越笨的元凶
- 看懂 hermes-agent 的隔离子代理:独立对话+独立终端+Python RPC,主 Agent 只收结果
- 学会用 RPC 把一条多步流水线"折叠"成主上下文里的一行
- 用标准库 subprocess/json 写出一个仿 hermes 思路、能真跑的最小派发器
- 拿到"什么活值得开子代理、什么活主 Agent 直接干"的判断口诀
你有没有遇到过这种情况:让 Agent 办一件需要好几步的活——先翻一堆日志、再跑个脚本、再读返回值——它办着办着就变笨了。前面塞进上下文的那一大坨日志、命令回显、中间产物,把后面要紧的指令全挤没了。这不是模型不行,是上下文被垃圾撑爆了。
开源项目 hermes-agent(NousResearch 出品)对这件事给了一个很漂亮的解法:把脏活丢给隔离的子代理去干,主 Agent 只收一行干净结果。 这一节我们把它的「subagent + Python RPC」流水线拆开,看懂它怎么做到"多步流程不占主上下文",再用标准库写一个你今天就能跑的最小版本。
这篇适合谁:已经能写出一个能跑的 Agent,但被"上下文越用越乱"卡住的人。读完你会有一套把脏活"折叠"出主上下文的具体打法。
钩子:先算一笔"上下文账"
假设主 Agent 要完成"检查服务健康度",朴素写法是它自己一步步来:
- 跑
cat error.log,回显 2000 行——全进上下文。 - 跑统计脚本,输出一屏——也进上下文。
- 再读配置文件——还进上下文。
- 最后才说一句"服务正常,错误日志里有 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 一个干净上下文"的,不是用来包装每一次工具调用的。
动手挑战
- 把上面的
subagent_health.py改成真去读一个大文件(比如你机器上某个日志),确认主进程拿到的只有结论、终端里看不到那一大坨内容。 - 给派发器加一个"参数透传":让主进程能把目标(比如要查的服务名)作为命令行参数传给子代理,子代理
sys.argv接收。 - 进阶:把并行版本接到你现有 Agent 上,做一个"一键体检"工具——一次并行派三个子代理,主 Agent 只汇总三行结论。体会一下上下文比朴素写法干净了多少。
小结 · 你现在掌握了什么
- 你看懂了"上下文成本"才是多步 Agent 越跑越笨的元凶,而不是模型不行。
- 你理解了 hermes-agent 隔离子代理的核心:独立对话+独立终端+Python RPC,主 Agent 只收一行结果,过程垃圾零占用主上下文。
- 你会用 RPC 把"读→算→判"的多步流水线折叠成主上下文里的一次调用、一个返回值。
- 你能用标准库
subprocess/json写出能真跑的最小派发器,还能并行派多个子代理。 - 你拿到了"脏、长、可独立占两条就开子代理"的判断口诀。
记住:隔离不是为了花哨,是为了让主 Agent 始终在一个干净、要紧的上下文里思考。 这一节解决了"怎么把一个 Agent 内部的脏活折叠掉";下一节我们跨出单个 Agent,看 A2A 协议怎么让不同 Agent 之间互相发现、互相委派。
下一步:往后是跨 Agent 协作。看 AI Agent 智能体阶梯的 L5 后段;想看整条路的位置就对照三支柱路线图。前置如果没读,先补 hermes 的文件即记忆和 AI Agent 是什么。
👉 看看 AI 数字员工落地指南,或了解 数字员工搭建实战课。需要为企业落地方案,欢迎找我们聊 企业服务。