Hermes-Agent——(三)工具执行策略
上篇聊了 AI Agent 怎么知道自己有哪些工具可以用。那问题来了工具注册完了Agent 手里攥着几十个能力接下来一个很直接的事——你让它同时干好几件事的时候它是一件一件来还是一起上这个问题看着简单但往里挖一层就复杂了。举个例子。你跟 AI 说「帮我对比这三个文件」AI 可能一次性想干三件事读 a 文件、读 b 文件、读 c 文件。两个选择摆在这。要么串行a 读完了读 bb 读完了读 c。安全但你要等三轮。要么并行三个一起读。快但如果里面混了一个写文件的操作两个写同时往同一个文件里塞东西呢那叫竞态条件文件直接乱掉。如果三个都只是读那并行显然更好。但如果一个是写文件另一个也在写同一个文件呢或者有个「向用户提问」混在里面你总不能同时弹三个问题让用户回答。这就是这篇文章要拆的东西hermes-agent 怎么判断一批操作能不能一起跑以及这背后的安全分级思想。先搞清楚 AI 到底能干什么在看怎么执行之前得先知道 AI 手里有哪些牌。hermes-agent 有 46 个内置工具外加动态加载的 MCP 工具大部分跟并行串行没关系我把跟执行策略相关的挑出来。文件操作读文件让 AI 去看你电脑上的某个文件内容。写文件创建或覆盖一个文件。文件修补修改文件的某几行而不是整个重写用过 git 的朋友知道git diff长什么样。搜文件在项目目录里按文件名匹配搜索。搜索类网页搜索上网搜东西。网页抓取给一个具体网址把里面的内容扒下来读。搜文件上面说了。翻聊天记录在跟 AI 的历史对话里全文搜索。系统操作类命令行让 AI 直接在电脑上敲 shell 命令。跑代码在沙箱里执行一段 Python。AI 内部管理类问用户AI 拿不准的时候弹框让你确认「这文件删吗」。待办清单AI 给自己维护一个任务列表做完划掉。记事情把信息存到长期记忆里关了对话下次还能想起来。识图分析图片内容。技能列表和看技能查看自己有哪些能力。派活给子 AI把大任务拆出去交给另一个 AI 实例并行干。智能家居类跟 Home Assistant 系统对接比较小众查设备状态比如「客厅灯现在开着还是关着」。列设备清单家里有哪些智能设备。列服务清单有哪些自动化可用。调用服务执行开关灯之类的写操作。另外还有浏览器操作、RL 训练等二十多个工具跟本文的并行安全分级没有直接关系先略过。现在你知道 AI 手里有哪些牌了接下来看它怎么出牌。不是所有牌都能一起出最核心的设计是一个四级分类。每个能力被标上一个安全等级这个等级直接决定它能不能跟别人一起跑。第一级随时可以一起跑。总共 11 个工具。网页搜索、网页抓取、识图、搜文件、翻聊天记录、技能列表、看技能、查设备状态、列设备清单、列服务清单、读文件。共同点是只读不改变任何东西没有共享状态。第二级看操作的文件是不是同一个。就 3 个工具。读文件、写文件、文件修补。它们都操作文件但操作不同文件的时候完全可以一起跑。读 a 文件跟写 b 文件互不干扰。但如果两个操作都指向同一个文件就不能一起跑了得排队。这里有个细节容易让人困惑。读文件同时在两级里都有名字。什么意思呢实际判断的时候先走第二级的路径冲突检测。两个读文件如果读的是不同文件那就并行相安无事。但如果读的是同一个文件检测到路径重叠就退回串行。你可能会问两个读操作读同一个文件操作系统层面完全安全啊为什么要串行说实话这个设计偏保守了。作者的思路是统一逻辑只要是文件操作不管读写都走路径检测。代价是读同一个文件的场景被过度串行化了。好处是代码简单不会漏判。这是一个典型的工程取舍——用少量不必要的串行化换取冲突检测的零误判。第三级绝对不能一起跑。就 1 个问用户。同时弹三个问题给用户用户该回答哪个交互式操作必须排队一个问完再问下一个。第四级没标注的默认不能一起跑。46 个内置工具里进了上面三个安全名单的只是少数剩下 32 个——命令行、跑代码、派活给子 AI、记事情、待办清单、浏览器操作、RL 训练等等——全都没标注。没说自己安全的就当你不安全。白名单思维跟网络安全的默认拒绝一个道理。四级分类落到代码里是这样# 第一级只读、无共享状态 —— 11 个随时可以并行_PARALLEL_SAFE_TOOLSfrozenset({read_file,search_files,# 读文件、搜文件web_search,web_extract,# 网页搜索、网页抓取vision_analyze,# 识图session_search,# 翻聊天记录skill_view,skills_list,# 看技能、技能列表ha_get_state,ha_list_entities,ha_list_services,# 智能家居查状态、列设备、列服务})# 第二级操作文件路径不重叠时可以并行 —— 3 个_PATH_SCOPED_TOOLSfrozenset({read_file,write_file,patch})# 第三级绝对不能并行 —— 1 个_NEVER_PARALLEL_TOOLSfrozenset({clarify})# 问用户# 第四级没标注的默认不能并行 —— 32 个# 没有对应常量不在上面三个名单里的自动回串行怎么判断整批一起判当 AI 收到一批操作时决策不是一个个工具单独判断的而是整批一起定要么全并行要么全串行。判断逻辑就一条线只有 1 个操作 → 串行没有并行的意义混进了问用户 → 全部串行哪怕另外两个是读文件也不行不搞混合执行参数解析失败 → 全部串行。LLM 返回的东西不是 100% 合法 JSON缺引号、多逗号、括号没配对太常见了。解析不了就不知道参数里有没有路径也就不知道会不会冲突安全第一有文件操作且路径重叠 → 全部串行。这里的重叠判断很激进不是只判断完全相同的路径而是路径前缀匹配——/home/project/和/home/project/src/main.py共享同一个目录前缀就算重叠。它不分析你到底在读写什么只看路径有没有关联。有关联就串行宁可多判不要漏判所有操作都在安全名单里 → 整批并行有没标注分类的操作 → 串行上面这六条翻译成代码不到 40 行def_should_parallelize_tool_batch(tool_calls)-bool:iflen(tool_calls)1:returnFalse# 只有 1 个 → 串行tool_names[tc.function.namefortcintool_calls]ifany(namein_NEVER_PARALLEL_TOOLSfornameintool_names):returnFalse# 混进了问用户 → 全部串行reserved_paths[]fortool_callintool_calls:tool_nametool_call.function.nametry:function_argsjson.loads(tool_call.function.arguments)exceptException:returnFalse# 参数解析失败 → 串行ifnotisinstance(function_args,dict):returnFalse# 非标准参数 → 串行iftool_namein_PATH_SCOPED_TOOLS:path_extract_parallel_scope_path(tool_name,function_args)ifpathisNone:returnFalse# 路径提取失败 → 串行ifany(_paths_overlap(path,p)forpinreserved_paths):returnFalse# 路径重叠 → 串行reserved_paths.append(path)continueiftool_namenotin_PARALLEL_SAFE_TOOLS:returnFalse# 未分类 → 串行returnTrue# 全部通过 → 并行执行起来的工程细节决策做完了开始执行。并行用的是线程池不是异步。原因上一篇讲过了Agent 核心是同步的大部分工具的处理函数是同步的线程池适配这些场景更自然。但有个更细的问题。一堆任务提交到线程池之后主线程怎么等结果as_completed()是标准做法哪个先跑完就先处理哪个。它虽然也支持 timeout 参数做轮询但用起来很别扭——在一个迭代器循环里既要处理已完成的任务、又要检查中断、又要发心跳三个职责搅在一起。所以这里换成了wait(timeout5.0)。每 5 秒醒一次醒来看看还有没有没跑完的任务检查一下用户有没有发停止信号该发心跳发心跳然后继续等。等待结果和中断检查两个职责在循环里交替执行逻辑清晰。长任务里的响应性就靠这种可中断轮询来保证。中断处理分三层。线程池启动前如果中断信号已经亮了直接跳过全部操作。执行中检查到中断取消所有还没开始的任务给已经跑起来的 3 秒宽限期。收尾阶段被中断的操作结果塞一条取消提示追加到对话记录里。串行执行也一样每个操作执行前先看一眼中断信号亮了就跳过剩下的。有些操作比较特殊无论是并行还是串行最终每个操作都要走到一个统一的入口函数。上篇讲过工具注册中心是个纯粹的查表机制handler 只收参数返回结果无状态。但有 4 个工具被标记为必须由 Agent 实例亲自处理加上另外 2 个特殊工具一共 6 个不能直接走注册中心。待办清单需要当前会话的任务列表对象。记事情需要持久化的记忆仓库跨会话保留。翻聊天记录需要数据库连接来搜历史对话。派活给子 AI需要当前 Agent 实例的引用子 AI 得知道它爹是谁。问用户需要回调函数弹交互框外部记忆提供者需要对接向量数据库。这 6 个都依赖 Agent 自身的内部状态注册中心是个全局单例压根不知道当前是哪个 Agent 在跑。所以这个入口函数做的事就是按需注入状态。安全插件拦截最先然后是有状态工具逐个匹配注入最后才丢给通用调度。两层各管各的互不侵入。跟上篇的核心层纯粹、调度层脏完全一致的思路。好的分层设计是有传染性的。防手滑自动存档上篇文章提过 Agent 有防手滑的机制这里展开说一下。这个东西叫 Checkpoint。它自动存的但回滚是你手动触发的。Agent 只负责在危险操作前拍个快照不会自己帮你回滚控制权在你手里。你发现 AI 改坏了文件敲/rollback列出所有存档点然后/rollback 3恢复到第 3 个或者只恢复单个文件。底层就是 git 的快照机制回滚之前还会自动多存一个存档这样回滚操作本身也可以撤销。存档触发点有两个。写文件和文件修补操作前危险终端命令前rm、mv、sed -i、git reset这些通过正则匹配识别。注意是在执行前存档不是执行后。坏了再存就晚了。总结第一个安全分级加白名单模型。把工具按安全等级分级没声明安全的默认不安全。跟网络安全里的默认拒绝一个思路。你在自己的系统里加工具时先想清楚这个东西能不能跟别人一起跑它操作的是共享状态还是独立资源第二个路径作用域。不是简单的读写串行只读并行。文件工具操作不同文件时可以并行这个粒度用极少量的路径检查换来了大量场景的性能提升。第三个主线程不能睡觉。可中断轮询把等待结果和检查心跳/中断两个职责交织在一个循环里长任务里的响应性就得靠这个。第四个状态注入。注册中心不知道 Agent 实例的存在Agent 内部状态通过入口函数注入给那几个特殊工具。两层各管各的互不侵入。