<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Tool-Scheduling | 有志者事竟成</title><link>https://www.liwenshen.com/tags/tool-scheduling/</link><atom:link href="https://www.liwenshen.com/tags/tool-scheduling/index.xml" rel="self" type="application/rss+xml"/><description>Tool-Scheduling</description><generator>Hugo Blox Builder (https://hugoblox.com)</generator><language>zh</language><lastBuildDate>Sat, 04 Jul 2026 09:00:00 +0800</lastBuildDate><image><url>https://www.liwenshen.com/media/icon_hu_dd5d76fef920c49e.png</url><title>Tool-Scheduling</title><link>https://www.liwenshen.com/tags/tool-scheduling/</link></image><item><title>工具调度系统：从注册到执行的完整生命周期</title><link>https://www.liwenshen.com/note/hermes-agent-engineering/06-tool-scheduling/</link><pubDate>Sat, 04 Jul 2026 09:00:00 +0800</pubDate><guid>https://www.liwenshen.com/note/hermes-agent-engineering/06-tool-scheduling/</guid><description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;系列：通过 Hermes 探秘 Agent 工程 | 第 6 篇&lt;/strong&gt;
上一篇：&lt;a href="https://www.liwenshen.com/note/hermes-agent-engineering/05-memory-system/"&gt;记忆系统：跨会话持久化的工程实现&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id="问题70-工具怎么高效调度"&gt;问题：70+ 工具怎么高效调度&lt;/h2&gt;
&lt;p&gt;Agent 的&amp;quot;手&amp;quot;是各式各样的工具——&lt;code&gt;terminal&lt;/code&gt; 执行命令、&lt;code&gt;read_file&lt;/code&gt; 读取文件、&lt;code&gt;browser_navigate&lt;/code&gt; 操控浏览器、&lt;code&gt;delegate_task&lt;/code&gt; 生成子 Agent……加在一起有 70 多个。&lt;/p&gt;
&lt;p&gt;这些工具面临的挑战是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;注册&lt;/strong&gt;：如何在不硬编码的前提下让工具自己声明 schema？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;过滤&lt;/strong&gt;：如何根据当前会话快速决定&amp;quot;模型此刻能看到哪些工具&amp;quot;？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;执行&lt;/strong&gt;：如何并发执行多个工具调用，同时保证消息对的合法性？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;延迟加载&lt;/strong&gt;：如何只把&amp;quot;可能用到&amp;quot;的工具提前注册？&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Hermes 的工具调度系统用一套分层架构解决了这些问题。&lt;/p&gt;
&lt;h2 id="工具注册自注册--自动发现"&gt;工具注册：自注册 + 自动发现&lt;/h2&gt;
&lt;p&gt;每个工具文件在&lt;strong&gt;模块导入时&lt;/strong&gt;调用 &lt;code&gt;registry.register()&lt;/code&gt; 声明自己的 schema。注册中心是线程安全的单例 &lt;code&gt;ToolRegistry&lt;/code&gt;，使用 &lt;code&gt;RLock&lt;/code&gt; 保护内部状态。&lt;/p&gt;
&lt;p&gt;一个典型的注册调用包含：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;name&lt;/code&gt;：工具名称（如 &lt;code&gt;read_file&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;toolset&lt;/code&gt;：所属工具集（如 &lt;code&gt;file&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;schema&lt;/code&gt;：OpenAI 格式的 JSON Schema&lt;/li&gt;
&lt;li&gt;&lt;code&gt;handler&lt;/code&gt;：执行函数&lt;/li&gt;
&lt;li&gt;&lt;code&gt;check_fn&lt;/code&gt;：可用性检查（如 Docker 是否安装）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;requires_env&lt;/code&gt;：必需的环境变量列表&lt;/li&gt;
&lt;li&gt;&lt;code&gt;max_result_size_chars&lt;/code&gt;：最大返回结果长度&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dynamic_schema_overrides&lt;/code&gt;：运行时动态修改 schema 的回调&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;check_fn&lt;/code&gt; 的设计很精妙——它不是每次获取工具定义时都调用（太昂贵），而是在 registry 内部缓存 30 秒。如果检查失败，还有 60 秒的&amp;quot;宽限期&amp;quot;：只要上次成功过，短期内就继续认为工具可用。这避免了 Docker daemon 瞬时抖动导致的工具丢失。&lt;/p&gt;
&lt;p&gt;注册还做了一个防呆设计：如果两个工具同名但来自不同的 toolset，注册会被拒绝（防止插件意外覆盖内置工具）。除非显式传入 &lt;code&gt;override=True&lt;/code&gt;，比如用 headed-Chrome CDP 后端替换默认的浏览器工具。&lt;/p&gt;
&lt;h2 id="schema-获取过滤--多级缓存"&gt;Schema 获取：过滤 + 多级缓存&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;get_tool_definitions()&lt;/code&gt; 是&amp;quot;模型即将被发送工具列表&amp;quot;时的入口。&lt;/p&gt;
&lt;h3 id="过滤逻辑"&gt;过滤逻辑&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;根据 &lt;code&gt;enabled_toolsets&lt;/code&gt; 决定包含哪些工具集&lt;/li&gt;
&lt;li&gt;如果都没传，默认包含所有工具集&lt;/li&gt;
&lt;li&gt;再根据 &lt;code&gt;disabled_toolsets&lt;/code&gt; 做减法&lt;/li&gt;
&lt;li&gt;对剩余工具逐个调用 &lt;code&gt;check_fn&lt;/code&gt;（带缓存）决定是否可用&lt;/li&gt;
&lt;li&gt;把最终通过的 schema 返回&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="三层缓存"&gt;三层缓存&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;层&lt;/th&gt;
&lt;th&gt;位置&lt;/th&gt;
&lt;th&gt;Key&lt;/th&gt;
&lt;th&gt;用途&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;L1&lt;/td&gt;
&lt;td&gt;registry 内部 &lt;code&gt;_check_fn_cache&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;check_fn 引用 + TTL&lt;/td&gt;
&lt;td&gt;避免 Docker/Playwright 等外部检查过于频繁&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L2&lt;/td&gt;
&lt;td&gt;registry 内部 &lt;code&gt;_check_fn_last_good&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;check_fn 引用 + 绝对时间&lt;/td&gt;
&lt;td&gt;flake 宽限：失败后 60s 内仍认为可用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L3&lt;/td&gt;
&lt;td&gt;model_tools 层 &lt;code&gt;_tool_defs_cache&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;(enabled, disabled, generation, cfg_fp, kanban_flag, skip_tool_search_assembly)&lt;/td&gt;
&lt;td&gt;避免每次请求走完整的过滤流程&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;L3 缓存最多存 8 个条目，LRU 淘汰。缓存 key 里包含了 &lt;code&gt;config.yaml&lt;/code&gt; 的 mtime + size——这意味着你在 &lt;code&gt;hermes config&lt;/code&gt; 里改了并发限制，下次请求就会自动失效缓存（因为动态 schema 依赖配置）。&lt;/p&gt;
&lt;p&gt;7 个不同的配置指纹就能覆盖典型的使用场景：web-only、coding-only、browser + file、acr、delegate 子 Agent、kanban worker、full-hermes。&lt;/p&gt;
&lt;h2 id="主调度器handle_function_call"&gt;主调度器：handle_function_call()&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;handle_function_call()&lt;/code&gt; 是工具调用的核心入口，处理从&amp;quot;模型输出 tool_calls&amp;quot;到&amp;quot;返回执行结果&amp;quot;的完整链路。&lt;/p&gt;
&lt;h3 id="调用链"&gt;调用链&lt;/h3&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;model_tools.handle_function_call()
├─ coerce_tool_args() // 类型强制：&amp;#34;42&amp;#34; → 42
├─ Tool Search bridge // tool_call 解包成真实工具
├─ apply_tool_request_middleware() // 中间件修改参数
├─ _AGENT_LOOP_TOOLS 检查 // 拒绝直接调度循环内工具
├─ pre_tool_call hook // 插件拦截
├─ edit_approval (ACP/Zed) // 文件修改审批
├─ dispatch 到 registry // 实际执行
├─ run_tool_execution_middleware() // 执行后中间件
├─ transform_tool_result hook // 插件修改结果
└─ post_tool_call hook // 通知监听器
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="类型强制"&gt;类型强制&lt;/h3&gt;
&lt;p&gt;模型有时会返回 &lt;code&gt;&amp;quot;42&amp;quot;&lt;/code&gt;（字符串）而不是 &lt;code&gt;42&lt;/code&gt;（数字），Hermes 的 &lt;code&gt;coerce_tool_args()&lt;/code&gt; 会根据 schema 声明的类型自动转换。这解决了一个常见的大模型&amp;quot;小毛病&amp;quot;。&lt;/p&gt;
&lt;h3 id="错误统一格式"&gt;错误统一格式&lt;/h3&gt;
&lt;p&gt;所有工具执行异常都被捕获并返回统一的 &lt;code&gt;{&amp;quot;error&amp;quot;: &amp;quot;...&amp;quot;}&lt;/code&gt; 格式 JSON。错误字符串还会经过 &lt;code&gt;_sanitize_tool_error()&lt;/code&gt; 清洗——去除可能干扰模型的 framing token（如 &lt;code&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/code&gt;、&lt;code&gt;&amp;lt;/tool_call&amp;gt;&lt;/code&gt; 等），防止模型&amp;quot;读&amp;quot;到错误字符串里的结构标记而产生幻觉。&lt;/p&gt;
&lt;h2 id="工具执行并发-vs-顺序"&gt;工具执行：并发 vs 顺序&lt;/h2&gt;
&lt;p&gt;Hermes 支持两种执行模式，由 &lt;code&gt;execution_mode&lt;/code&gt; 配置决定。&lt;/p&gt;
&lt;h3 id="顺序执行"&gt;顺序执行&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;_execute_tool_calls_sequential()&lt;/code&gt;：一个接一个地执行工具调用。&lt;/p&gt;
&lt;p&gt;适用于有依赖关系的调用（比如先 &lt;code&gt;write_file&lt;/code&gt; 再 &lt;code&gt;terminal&lt;/code&gt; 测试）。&lt;/p&gt;
&lt;h3 id="并发执行"&gt;并发执行&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;execute_tool_calls_concurrent()&lt;/code&gt;：用线程池并行执行所有工具调用。&lt;/p&gt;
&lt;p&gt;并发执行的 pre-flight 流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;中断检查&lt;/strong&gt;：如果用户按了 Ctrl+C，直接跳过所有工具，返回取消标记&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tool Search 解包&lt;/strong&gt;：把 &lt;code&gt;tool_call&lt;/code&gt; 桥解包成真实工具名 + 参数&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;会话范围检查&lt;/strong&gt;：验证解包后的工具在当前会话的工具集范围内（防止子 Agent 越权）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;中间件应用&lt;/strong&gt;：运行请求修改中间件&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;拦截评估&lt;/strong&gt;：插件钩子 + 工具守卫（Tool Guardrails）评估是否允许执行&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;检查点预检&lt;/strong&gt;：对 &lt;code&gt;write_file&lt;/code&gt;、&lt;code&gt;patch&lt;/code&gt; 和危险 terminal 命令提前创建 Git checkpoint&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;然后通过线程池（最多 8 个 worker）并发执行。每个 worker 注册自己的线程 ID，让 agent 的 &lt;code&gt;interrupt()&lt;/code&gt; 可以精准中断特定的 worker 线程。&lt;/p&gt;
&lt;p&gt;结果按&lt;strong&gt;原始 tool_call 顺序&lt;/strong&gt;写入消息列表——即使并发执行完成顺序不同，模型看到的结果顺序必须和它发出的调用顺序一致。&lt;/p&gt;
&lt;p&gt;执行前后还会触发两个钩子：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;tool_start_callback&lt;/code&gt; / &lt;code&gt;tool_progress_callback&lt;/code&gt;：通知 UI 显示进度&lt;/li&gt;
&lt;li&gt;&lt;code&gt;post_tool_call&lt;/code&gt;：通知插件执行已完成&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="工具搜索桥延迟加载"&gt;工具搜索桥：延迟加载&lt;/h2&gt;
&lt;p&gt;70+ 个工具不可能全部塞进每次 API 请求（有些场景只需要其中十几个）。Hermes 用&amp;quot;工具搜索桥&amp;quot;实现了延迟加载：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;只注册 &lt;code&gt;tool_search&lt;/code&gt;、&lt;code&gt;tool_describe&lt;/code&gt;、&lt;code&gt;tool_call&lt;/code&gt; 三个桥工具&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;模型调用 &lt;code&gt;tool_search&lt;/code&gt; 查询&amp;quot;有哪些工具可用&amp;quot;&lt;/li&gt;
&lt;li&gt;模型调用 &lt;code&gt;tool_describe&lt;/code&gt; 获取某个工具的完整 schema&lt;/li&gt;
&lt;li&gt;模型调用 &lt;code&gt;tool_call&lt;/code&gt; 调用一个&amp;quot;还未加载&amp;quot;的工具&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;桥的内部流程：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;tool_call(function_name=&amp;#34;delegate_task&amp;#34;, args={...})
├─ 解析出底层工具名和参数
├─ 检查当前会话是否被授权使用该工具（scope gate）
├─ 重新调度：handle_function_call(function_name=&amp;#34;delegate_task&amp;#34;, args)
└─ 返回执行结果
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;关键点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;桥对插件钩子完全透明——所有 downstream 检查看到的是&lt;strong&gt;真实工具名&lt;/strong&gt;（不是 &lt;code&gt;tool_call&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;会话范围隔离：子 Agent 不能通过桥调用父会话的其他工具，即使 schema 碰巧在进程全局注册表里&lt;/li&gt;
&lt;li&gt;结果缓存：&lt;code&gt;_tool_search_scoped_names()&lt;/code&gt; 的 scope 信息缓存在 agent 实例上，registry generation 变化时才刷新&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="中间件体系"&gt;中间件体系&lt;/h2&gt;
&lt;p&gt;贯穿工具调用全链路的中间件有两层：&lt;/p&gt;
&lt;h3 id="请求中间件-apply_tool_request_middleware"&gt;请求中间件 (&lt;code&gt;apply_tool_request_middleware&lt;/code&gt;)&lt;/h3&gt;
&lt;p&gt;在工具执行&lt;strong&gt;之前&lt;/strong&gt;，可以修改参数或阻止调用。比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;将相对路径转绝对路径&lt;/li&gt;
&lt;li&gt;添加默认参数（如 terminal 的 env）&lt;/li&gt;
&lt;li&gt;记录修改历史到 trace 字段&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="执行中间件-run_tool_execution_middleware"&gt;执行中间件 (&lt;code&gt;run_tool_execution_middleware&lt;/code&gt;)&lt;/h3&gt;
&lt;p&gt;在工具执行&lt;strong&gt;前后&lt;/strong&gt;包装逻辑。比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;延迟加载依赖（某个工具需要特定 SDK 时才 import）&lt;/li&gt;
&lt;li&gt;执行超时控制&lt;/li&gt;
&lt;li&gt;执行结果的后处理&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;两层的 trace 字段（&lt;code&gt;_tool_middleware_trace&lt;/code&gt;）会一直传递到 &lt;code&gt;post_tool_call&lt;/code&gt; 钩子，让插件能观测到工具调用经过了哪些中间件的处理。&lt;/p&gt;
&lt;h3 id="执行后转换钩子"&gt;执行后转换钩子&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;transform_tool_result&lt;/code&gt; 允许插件&lt;strong&gt;替换&lt;/strong&gt;工具的最终返回结果。比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;格式化错误消息&lt;/li&gt;
&lt;li&gt;隐藏敏感信息&lt;/li&gt;
&lt;li&gt;把大输出换成摘要&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;和 &lt;code&gt;post_tool_call&lt;/code&gt; 钩子不同，后者只是&amp;quot;观察&amp;quot;，前者能实际修改返回给模型的内容。&lt;/p&gt;
&lt;h2 id="执行预算与结果截断"&gt;执行预算与结果截断&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;tools/budget_config.py&lt;/code&gt; 定义了每个工具结果的默认最大长度（&lt;code&gt;DEFAULT_RESULT_SIZE_CHARS&lt;/code&gt;），并根据上下文窗口大小动态缩放——小上下文的本地模型（如 65K）得到比大上下文模型（100K/200K）更严格的预算，避免单个大工具输出把请求撑爆。&lt;/p&gt;
&lt;p&gt;每个工具还可以有自己的 &lt;code&gt;max_result_size_chars&lt;/code&gt; 覆盖全局默认。比如 &lt;code&gt;read_file&lt;/code&gt; 可能允许更大的输出，而 &lt;code&gt;search_files&lt;/code&gt; 的结果会被截断。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;tool_result_storage.py&lt;/code&gt; 负责：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;持久化大的工具结果到磁盘（避免内存占用）&lt;/li&gt;
&lt;li&gt;强制执行每轮的工具输出预算&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="总结"&gt;总结&lt;/h2&gt;
&lt;p&gt;Hermes 的工具调度系统是一个&lt;strong&gt;四层架构&lt;/strong&gt;：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;层&lt;/th&gt;
&lt;th&gt;职责&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;注册层&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;模块导入时自注册，线程安全单例&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;过滤层&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;toolset 过滤 + check_fn 可用性检查 + 三级缓存&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;调度层&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;类型强制、中间件、插件钩子、ACP 审批&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;执行层&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;并发/顺序执行、中断传播、结果顺序保证&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;这套设计让 Hermes 能在 70+ 工具的场景下保持高效的 API 请求生成，同时通过工具搜索桥实现了灵活的延迟加载——需要多少，就加载多少。&lt;/p&gt;
&lt;p&gt;下一篇，我们将进入 Hermes 的安全防护体系——当 Agent 拥有终端操控能力时，如何防止它&amp;quot;做出格&amp;quot;的事&lt;/p&gt;</description></item></channel></rss>