【系列:手搓自主 AI Agent:Hermes 架构原理剖析 · 第 7 篇】

📅 2026/8/14 15:32:03
【系列:手搓自主 AI Agent:Hermes 架构原理剖析 · 第 7 篇】
安全审批与子 Agent 委派让 Agent 敢干活、不乱来导读Agent 能自主干活了但 rm -rf /、DROP TABLE 这些命令一旦由模型自主执行后果不堪设想。Hermes 用 15 条危险模式正则 两层缓存审批把终端命令锁进笼子再用子 Agent 委派机制让复杂任务拆解到独立上下文里并行推进。本文拆解 s09 与 s10 两个核心模块讲透敢干活与不乱来的工程平衡。模型想做的不能直接变成系统真的做的你的 Agent 学会了调用工具、读写文件、执行终端命令。它很能干但也很危险——如果模型在推理中突然决定执行rm -rf /或者DROP TABLE users;系统真的照做吗在 Hermes 架构里答案是不会。因为阶段 2 引入了两个关键模块s09_permission_system.py33694 字节负责危险命令审批s10_subagent_delegation.py38908 字节负责子 Agent 委派。一个管不乱来一个管敢干活。为什么只管终端命令先说一个反直觉的设计决策Hermes 的权限系统不是通用的 deny/allow/ask 三步管道。它只针对终端命令做模式匹配。为什么因为 read_file、web_search 这些工具最坏情况是读到错误信息或搜到无关结果——可逆可重试。但终端命令不一样mkfs.ext4 /dev/sda执行后数据直接没了没有任何撤销按钮。在所有工具里终端是唯一能造成不可逆损害的那个。所以权限系统的设计原则很明确把最危险的管住其他的放行。15 条危险模式覆盖六类破坏行为s09里定义了DANGEROUS_PATTERNS列表共 15 条正则预编译后用于每次终端命令检测DANGEROUS_PATTERNS[(rrm\s(-[a-zA-Z]*f[a-zA-Z]*\s|.*--no-preserve-root),Recursive/force file deletion),(rrm\s-[a-zA-Z]*r,Recursive file deletion),(rmkfs\.,Filesystem format),(rdd\sif,Raw disk write),(r\s*/dev/sd[a-z],Direct device write),(rchmod\s(-R\s)?777,World-writable permissions),(rchown\s-R\s,Recursive ownership change),(rshutdown|reboot|poweroff|init\s[06],System shutdown/reboot),(rkill\s-9\s(-1|1\b),Kill all processes),(r:\(\)\s*\{\s*:\|\s*:\s*\s*\}\s*;,Fork bomb),(rDROP\s(TABLE|DATABASE|INDEX),SQL destructive operation),(rTRUNCATE\sTABLE,SQL truncate),(rDELETE\sFROM\s\w\s*;?\s*$,SQL delete without WHERE),(rcurl\s.*\|\s*(bash|sh|zsh),Pipe remote script to shell),(rwget\s.*\|\s*(bash|sh|zsh),Pipe remote script to shell),]注意两个细节预编译——列表定义后立即re.compile避免每次命令都重新编译正则IGNORECASE——匹配时忽略大小写防止RM -RF绕过。15 条模式覆盖六类破坏行为文件删除rm -rf、磁盘操作mkfs/dd/设备写入、SQL 破坏DROP/TRUNCATE/DELETE 无 WHERE、系统服务shutdown/reboot、进程管理kill -9 -1、远程脚本执行curl|sh。两层缓存session 内存 磁盘持久化如果每次执行pip install都弹确认框用户大概率会直接关掉系统。所以 s09 实现了两层缓存_session_approved:set[int]set()# 本次进程内pattern_index 已被 session 批准_ALLOWLIST_FILEHERMES_HOME/allowlist.json_permanent_allowlist:set[str]_load_allowlist()# 磁盘持久化第一层是进程内的session_approved集合存的是 pattern 索引——本次运行中批准过一次后续直接放行。第二层是磁盘上的allowlist.json存的是 pattern 字符串——永久生效下次启动还在。审批流程四选项 once / session / always / deny当detect_dangerous_command命中危险模式后approve_command开始工作先查两层缓存session 内已批准 → 放行永久 allowlist 里的 pattern → 放行打印*** DANGEROUS COMMAND DETECTED ***展示命令和描述提示用户选择四选项之一四个选项对应不同的放行策略oonce只放行这一次下次再遇到同样命令继续拦截ssession本次进程内放行记入session_approved集合aalways永久放行写入allowlist.json持久化ddeny拒绝执行命令直接丢弃这个设计很务实pip install这种高频但安全的操作用户选一次 session 就够了rm -rf这种高危操作永远选 deny。审批逻辑在工具 handler不在核心循环这是 s09 最容易被忽视的架构决策审批逻辑放在run_terminal工具 handler 里核心循环完全无感知。defrun_terminal(command):detect_dangerous_command(command)# 检测approve_command(command)# 审批execute(command)# 执行核心循环只负责模型决定调工具不关心工具内部怎么处理安全。好处是权限系统可以独立演进不影响循环逻辑未来可以给不同工具挂不同的安全策略互不干扰。三个独特设计反绕过 / smart approval / 超时s09 还有三个值得学习的细节Unicode 反绕过。全角字符 -能骗过普通正则但 s09 在匹配前会做 Unicode 规范化把全角字符转成半角再匹配。smart approval。对于低风险命令会启动一个辅助 LLM 评估自动放行而不打扰用户。注意这是可选配置默认关闭。审批超时默认拒绝。如果用户长时间不响应默认拒绝执行——安全优先。过渡安全了但任务复杂了怎么办权限系统解决了不乱来但下一个问题来了任务太复杂单个上下文装不下。比如让 Agent “分析这个项目并写一份优化报告”——需要读代码、查文档、跑测试、对比方案。所有中间过程全堆在同一个 messages 里上下文迅速膨胀而且两个不相关的子任务比如读配置文件和跑性能测试会互相污染推理质量。解法是子 Agent 委派把局部任务放进独立上下文里做做完只把必要结果带回来。子 Agent 是什么子 Agent 是父 Agent 临时创建的、拥有独立 messages 的 Agent 实例。执行完返回结果后销毁。关键点它和父 Agent共享同一代码路径——都是run_conversation。区别在于三点独立 messages上下文隔离、工具受限、消耗父 Agent 的 iteration budget。三个硬限制blocked tools / budget / 深度s10 用三个硬限制防止子 Agent 失控DELEGATE_BLOCKED_TOOLS{delegate_task,memory,skill_manage}MAX_CHILD_ITERATIONS15DELEGATE_BLOCKED_TOOLS子 Agent 不能递归委派、不能改记忆、不能改技能。子 Agent 是一次性的不应该产生持久副作用。MAX_CHILD_ITERATIONS 15子 Agent 给更少的预算。父 Agent 可能有 90 次迭代子 Agent 最多 15 次防止子任务无限循环。深度限制 2 层父 → 子子不能再派子。递归委派是失控的根源。build_child_agent不继承父的 SOUL/MEMORY看真实代码build_child_agent的设计很精细DELEGATE_BLOCKED_TOOLS{delegate_task,memory,skill_manage}defbuild_child_agent(goal,context,toolsets):# 从父 registry 同一个工具池取按黑名单过滤child_tools[]fortool_definregistry.get_definitions(toolsets):function_nametool_def[function][name]iffunction_namenotinDELEGATE_BLOCKED_TOOLS:child_tools.append(tool_def)# 子 agent system prompt 一次性、任务专属不继承父的 SOUL/MEMORYchild_prompt(You are a focused sub-agent. Complete the assigned task and report results.\nDo NOT delegate further. Do NOT modify memory or skills.\n\nf# Task\n{goal}\n\n)ifcontext:child_promptf# Context\n{context}\n\nchild_messages[{role:user,content:goal}]return{system_prompt:child_prompt,messages:child_messages,tools:child_tools,}注意子 Agent不继承父的 SOUL/MEMORYsystem prompt 是任务专属的。而且双保险——prompt 层明确说不要委派、不要改记忆代码层用DELEGATE_BLOCKED_TOOLS拦截。budget 共享从父的预算里扣这是最容易做错的地方。很多实现会给子 Agent 单独预算——3 个并行子 Agent 各 90 次总共 270 次直接失控。Hermes 的做法是子 Agent 从父 Agent 的 iteration budget 里扣。子 Agent 不是另外给 15 次而是消耗父的预算。这样总迭代次数可控不会因为委派而无限膨胀。进度回传与心跳机制子 Agent 执行时间可能很长用户会以为系统挂了。s10 用两个机制解决进度回传通过tool_progress_callback观察者模式闭包捕获父的 spinner子 Agent 每执行一步就更新父的进度显示。心跳机制后台线程每 30 秒更新_last_activity_ts骗过 Gateway 的不活动检测——防止父 Agent 等子 Agent 时被当卡死杀掉。这两个机制是敢干活的保障用户能看到进度系统不会误杀。两章避坑10 个初学者错误精选权限系统 5 错想做通用权限系统——先把最危险的终端管住只检测 rm——SQL DROP、mkfs、kill -9 -1、curl|sh、fork bomb 都要拦不做 session 缓存——每次 pip install 都弹确认用户会关掉系统不处理 Unicode 绕过——全角字符轻松骗过正则把审批逻辑放核心循环——应该在工具 handler子 Agent 5 错让子 Agent 也能委派——递归失控把子 Agent 完整 messages 带回父——失去隔离意义给子 Agent 单独 budget——3 并行 270 次让子 Agent 改记忆——并行写冲突不做进度回传——用户以为挂了小结与预告s09和s10解决的是 Agent 工程化的两个核心问题安全边界不乱来和任务分解敢干活。前者的关键是只管最危险的终端命令 两层缓存 工具 handler 内审批后者的关键是独立上下文 受限工具 共享 budget 深度限制。下一篇第 8 篇将拆解配置系统优先级链——当 CLI 参数、环境变量、配置文件、默认值同时存在时谁说了算你在设计 Agent 权限系统时是选择白名单放行还是黑名单拦截欢迎在评论区聊聊你的取舍。参考文献Hermes Agent 教学仓库agents/s09_permission_system.py本文代码素材33694 字节真实可运行Hermes Agent 教学仓库agents/s10_subagent_delegation.py本文代码素材38908 字节真实可运行Hermes Agent 教学仓库docs/zh/s09-permission-system.md与docs/zh/s10-subagent-delegation.md审批流程、委派设计详解源码获取如需本系列全部源码请在以下链接克隆https://gitcode.com/ganxin7932508/learn-hermes-agent.git