<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Agent | 有志者事竟成</title><link>https://www.liwenshen.com/tags/agent/</link><atom:link href="https://www.liwenshen.com/tags/agent/index.xml" rel="self" type="application/rss+xml"/><description>Agent</description><generator>Hugo Blox Builder (https://hugoblox.com)</generator><language>zh</language><lastBuildDate>Wed, 01 Jul 2026 14:00:00 +0800</lastBuildDate><image><url>https://www.liwenshen.com/media/icon_hu_dd5d76fef920c49e.png</url><title>Agent</title><link>https://www.liwenshen.com/tags/agent/</link></image><item><title>工具系统：从注册到调度</title><link>https://www.liwenshen.com/post/hermes-agent-engineering/02-tool-system/</link><pubDate>Wed, 01 Jul 2026 14:00:00 +0800</pubDate><guid>https://www.liwenshen.com/post/hermes-agent-engineering/02-tool-system/</guid><description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;系列：通过 Hermes 探秘 Agent 工程 | 第 2 篇&lt;/strong&gt;
上一篇：&lt;a href="https://www.liwenshen.com/post/hermes-agent-engineering/01-agent-loop/"&gt;Agent Loop：Agent 的核心执行循环&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id="工具系统解决了什么问题"&gt;工具系统解决了什么问题？&lt;/h2&gt;
&lt;p&gt;一个 Agent 再聪明，如果只能&amp;quot;说话&amp;quot;不能&amp;quot;做事&amp;quot;，就只是一个聊天机器人。工具系统就是 Agent 的手脚——让模型能读写文件、执行命令、搜索网络、操作浏览器、管理定时任务……&lt;/p&gt;
&lt;p&gt;但工具越多，管理越复杂：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;怎么让新增的工具被 Agent 感知又不改核心代码？&lt;/li&gt;
&lt;li&gt;怎么让不同平台（CLI vs 网关 vs 子代理）看到不同的工具集？&lt;/li&gt;
&lt;li&gt;怎么让工具调用安全可控？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Hermes 用三个机制解决这些问题：&lt;strong&gt;自注册、toolset 分层、capabilities 检查&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id="机制一模块级自注册"&gt;机制一：模块级自注册&lt;/h2&gt;
&lt;p&gt;最精巧的设计是——每个工具文件自己注册自己。&lt;/p&gt;
&lt;p&gt;Hermes 规定：任何放在 &lt;code&gt;tools/&lt;/code&gt; 目录下的 Python 文件，只要在模块顶层调用 &lt;code&gt;registry.register()&lt;/code&gt;，就会被系统自动发现。你不需要在一个&amp;quot;总清单&amp;quot;里添加新工具的引用。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# 以文件工具为例&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;registry&lt;span style="color:#f92672"&gt;.&lt;/span&gt;register(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; name&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;read_file&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; toolset&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;file&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; schema&lt;span style="color:#f92672"&gt;=&lt;/span&gt;READ_FILE_SCHEMA,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; handler&lt;span style="color:#f92672"&gt;=&lt;/span&gt;_handle_read_file,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; check_fn&lt;span style="color:#f92672"&gt;=&lt;/span&gt;_check_file_reqs,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; emoji&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;📖&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; max_result_size_chars&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;100_000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&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;code&gt;name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;工具名，模型用这个名称调用工具&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;toolset&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;工具归属的分组，用于批量启用/禁用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;schema&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;OpenAI function calling 格式的参数描述&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;handler&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;实际执行函数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;check_fn&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;可用性检查函数（比如检查 Docker 是否可用）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;emoji&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;显示用图标&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;max_result_size_chars&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;输出结果的最大字符数&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id="发现过程ast-扫描"&gt;发现过程：AST 扫描&lt;/h3&gt;
&lt;p&gt;你可能会问：系统怎么知道 &lt;code&gt;tools/&lt;/code&gt; 下哪些文件调用了 &lt;code&gt;registry.register&lt;/code&gt;？&lt;/p&gt;
&lt;p&gt;答案很巧妙——&lt;strong&gt;不用运行代码就能发现&lt;/strong&gt;。Harmes 的 &lt;code&gt;discover_builtin_tools()&lt;/code&gt; 函数在导入模块之前，先对文件做 AST（抽象语法树）扫描，寻找模块顶层的 &lt;code&gt;registry.register()&lt;/code&gt; 调用。只有被 AST 判定为&amp;quot;会注册工具&amp;quot;的模块，才会被真正导入。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;tools/*.py → AST 扫描 → 发现 registry.register() → 导入模块 → 执行注册
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这个设计的好处是&lt;strong&gt;零配置扩展&lt;/strong&gt;：丢一个新文件到 &lt;code&gt;tools/&lt;/code&gt;，加一行 &lt;code&gt;registry.register()&lt;/code&gt;，Agent 下次启动就能用。&lt;/p&gt;
&lt;h2 id="机制二toolset-分层"&gt;机制二：Toolset 分层&lt;/h2&gt;
&lt;p&gt;光有注册还不够。50+ 个工具不能同时塞给所有场景——CLI 用户需要 terminal，Telegram bot 不需要；子代理可能只给 read_file 和 write_file；webhook 回调必须严格限制为只读工具。&lt;/p&gt;
&lt;p&gt;Hermes 的办法是&lt;strong&gt;toolset（工具集）&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;toolsets.py&lt;/code&gt; 定义了一组命名的工具集：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;TOOLSETS &lt;span style="color:#f92672"&gt;=&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;web&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;description&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;Web research and content extraction tools&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;tools&amp;#34;&lt;/span&gt;: [&lt;span style="color:#e6db74"&gt;&amp;#34;web_search&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;web_extract&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;includes&amp;#34;&lt;/span&gt;: []
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;terminal&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;description&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;Terminal/command execution and process management tools&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;tools&amp;#34;&lt;/span&gt;: [&lt;span style="color:#e6db74"&gt;&amp;#34;terminal&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;process&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;includes&amp;#34;&lt;/span&gt;: []
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;file&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;tools&amp;#34;&lt;/span&gt;: [&lt;span style="color:#e6db74"&gt;&amp;#34;read_file&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;write_file&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;patch&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;search_files&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;browser&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;tools&amp;#34;&lt;/span&gt;: [&lt;span style="color:#e6db74"&gt;&amp;#34;browser_navigate&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;browser_snapshot&amp;#34;&lt;/span&gt;, &lt;span style="color:#f92672"&gt;...&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;每个 toolset 可以引用其他 toolset（&lt;code&gt;includes&lt;/code&gt; 字段），形成组合。比如 CLI 默认可能启用 &lt;code&gt;terminal + file + web + browser&lt;/code&gt;，而子代理可能只给 &lt;code&gt;file + web&lt;/code&gt;。&lt;/p&gt;
&lt;h3 id="三层过滤"&gt;三层过滤&lt;/h3&gt;
&lt;p&gt;最终给模型的工具列表，经过三层过滤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;启用集（enabled_toolsets）&lt;/strong&gt;：用户配置启用了哪些 toolset，取并集&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;禁用集（disabled_toolsets）&lt;/strong&gt;：从并集中减去这些&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;capabilities 检查&lt;/strong&gt;：对每个工具调用 &lt;code&gt;check_fn()&lt;/code&gt;，去掉当前环境不支持的&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="机制三capabilities-检查与缓存"&gt;机制三：capabilities 检查与缓存&lt;/h2&gt;
&lt;p&gt;很多工具的可用性取决于外部环境。比如 &lt;code&gt;terminal&lt;/code&gt; 工具在非 local 后端（比如 Docker 远程）可能不可用，&lt;code&gt;browser&lt;/code&gt; 需要 Playwright 安装，&lt;code&gt;ha_list_entities&lt;/code&gt; 需要 &lt;code&gt;HASS_TOKEN&lt;/code&gt; 环境变量。&lt;/p&gt;
&lt;p&gt;每个工具可以定义自己的 &lt;code&gt;check_fn&lt;/code&gt;。但这个检查有个问题：外部环境（比如 Docker 守护进程的 socket 连接）可能瞬断，每次调用 LLM 前都检查一遍会增加延迟。&lt;/p&gt;
&lt;p&gt;Hermes 用了两层优化：&lt;/p&gt;
&lt;h3 id="ttl-缓存"&gt;TTL 缓存&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;check_fn&lt;/code&gt; 的结果缓存 30 秒，同一个会话里连续调用不重复检查。&lt;/p&gt;
&lt;h3 id="瞬断抑制"&gt;瞬断抑制&lt;/h3&gt;
&lt;p&gt;更精妙的是&amp;quot;&lt;strong&gt;最近一次成功抑制&lt;/strong&gt;&amp;ldquo;机制：如果某个 &lt;code&gt;check_fn&lt;/code&gt; 最近成功过（证明功能确实可用），那么接下来 60 秒内的失败会被当作&amp;quot;瞬断&amp;quot;忽略掉——工具仍然可用，不报错。&lt;/p&gt;
&lt;p&gt;为什么需要这个？想象一下：Docker daemon 的 Unix socket 偶尔超时（容器负载高时很常见），如果一刀切地&amp;quot;失败就禁用&amp;rdquo;，会导致 CLI 工具在你的会话中被随机移除。有了瞬断抑制，单次超时不影响使用，只有持续失败才会真正禁用。&lt;/p&gt;
&lt;h2 id="分发handle_function_call"&gt;分发：handle_function_call()&lt;/h2&gt;
&lt;p&gt;当模型返回工具调用时，Agent 通过 &lt;code&gt;handle_function_call()&lt;/code&gt; 分发。这个函数是整个工具系统的&amp;quot;路由器&amp;quot;。&lt;/p&gt;
&lt;h3 id="路由逻辑"&gt;路由逻辑&lt;/h3&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;模型输出 tool_calls
↓
handle_function_call(name, args, task_id, ...)
↓
参数类型强制转换（字符串 &amp;#34;42&amp;#34; → 整数 42）
↓
工具 Search bridge 特殊处理（让模型能&amp;#34;搜索工具目录&amp;#34;）
↓
查找 ToolEntry → 调用 handler(args) → 返回 JSON 结果
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="参数类型强制转换"&gt;参数类型强制转换&lt;/h3&gt;
&lt;p&gt;不同模型的 function calling 实现有差异——有些模型会把数字参数传成字符串（&lt;code&gt;&amp;quot;42&amp;quot;&lt;/code&gt; 而不是 &lt;code&gt;42&lt;/code&gt;）。&lt;code&gt;handle_function_call()&lt;/code&gt; 会自动根据 schema 做类型强制转换，确保 handler 收到正确类型的参数。&lt;/p&gt;
&lt;h3 id="tool-search-bridge"&gt;Tool Search Bridge&lt;/h3&gt;
&lt;p&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;，合称&amp;quot;工具搜索桥&amp;quot;。它让模型可以搜索当前会话可用的工具目录，而不需要在 system prompt 里塞进所有工具的定义。&lt;/p&gt;
&lt;p&gt;启用场景：当工具数量太大（50+）会消耗大量上下文 token 时，模型可以先用 &lt;code&gt;tool_search&lt;/code&gt; 找到需要的工具，再用 &lt;code&gt;tool_describe&lt;/code&gt; 查看参数细节，最后用 &lt;code&gt;tool_call&lt;/code&gt; 调用。相当于给工具系统加了一层&amp;quot;按需索引&amp;quot;。&lt;/p&gt;
&lt;h2 id="并行执行"&gt;并行执行&lt;/h2&gt;
&lt;p&gt;如果模型一次返回多个独立的工具调用，Hermes 支持并行执行。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;模型返回：[tool_A(args_A), tool_B(args_B), tool_C(args_C)]
↓
并行分发到 ThreadPoolExecutor / asyncio
↓
等待所有结果
↓
合并到历史对话
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;并行执行的关键是每个工具 handler 要线程安全。大部分工具本质是无状态的（接收参数 → 执行 → 返回结果），天然支持并行；但有副作用的工具（比如写文件同一路径）需要开发者自己保证安全。&lt;/p&gt;
&lt;h2 id="为什么这样设计"&gt;为什么这样设计？&lt;/h2&gt;
&lt;p&gt;回顾这三个机制，你会发现 Hermes 工具系统的设计哲学：&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;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;AST 自注册&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 分层&lt;/td&gt;
&lt;td&gt;CLI 和 Telegram 看到不同工具&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;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;Tool Search Bridge&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;强制转换&lt;/td&gt;
&lt;td&gt;不同模型输出的格式差异被抹平&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="工程启示"&gt;工程启示&lt;/h2&gt;
&lt;p&gt;工具系统的好坏不取决于工具数量，而在于&lt;strong&gt;管理质量&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;一些值得借鉴的设计决策：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;**让工具自己说&amp;quot;我需要什么&amp;quot;&lt;code&gt;**——通过 &lt;/code&gt;check_fn` 声明依赖，而不是让外部代码猜测工具是否可用&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;缓存有退路&lt;/strong&gt;——30 秒 TTL + 60 秒瞬断抑制，既避免重复检查，又不会把抖动当死机&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;渐进式暴露&lt;/strong&gt;——不是所有工具平铺给模型，而是通过 toolset + bridge 按需暴露，控制上下文开销&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="总结"&gt;总结&lt;/h2&gt;
&lt;p&gt;工具系统是 Agent 可靠性的基础。Hermes 用自注册让扩展零成本，用 toolset 让多平台共享代码，用 capabilities 检查让工具可用性可感知，用并行分发让执行不阻塞。&lt;/p&gt;
&lt;p&gt;下一篇，我们将深入 System Prompt 的组装——Hermes 是如何把工具定义、记忆快照、用户规则拼成一个既紧凑又完整的系统提示的。&lt;/p&gt;</description></item><item><title>Agent Loop：Agent 的核心执行循环</title><link>https://www.liwenshen.com/post/hermes-agent-engineering/01-agent-loop/</link><pubDate>Wed, 01 Jul 2026 10:00:00 +0800</pubDate><guid>https://www.liwenshen.com/post/hermes-agent-engineering/01-agent-loop/</guid><description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;系列：通过 Hermes 探秘 Agent 工程 | 第 1 篇&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id="为什么需要一个循环"&gt;为什么需要一个&amp;quot;循环&amp;quot;？&lt;/h2&gt;
&lt;p&gt;如果你用过 ChatGPT 或 Claude 的聊天界面，你会发现它们的工作方式是一次性的：你发一条消息，模型回一条消息，对话结束。&lt;/p&gt;
&lt;p&gt;但 Agent 不同。Agent 的任务往往不是&amp;quot;问一句答一句&amp;quot;能解决的。一个典型的 Agent 请求可能是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;ldquo;帮我查一下今天 A 股涨幅前 10 的板块，把结果写成 CSV 文件，然后分析一下这些板块的共同特征。&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这个任务需要：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;调用工具查询数据&lt;/li&gt;
&lt;li&gt;把数据写入文件&lt;/li&gt;
&lt;li&gt;读取文件内容进行分析&lt;/li&gt;
&lt;li&gt;输出最终结论&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;每一步都依赖上一步的结果，模型需要&amp;quot;看到&amp;quot;工具执行的结果，才能决定下一步做什么。这就是 Agent Loop——一个让 LLM 和工具反复交互的 while 循环。&lt;/p&gt;
&lt;h2 id="从入口开始"&gt;从入口开始&lt;/h2&gt;
&lt;p&gt;Hermes 的 Agent 入口是一个叫 &lt;code&gt;run_conversation&lt;/code&gt; 的函数。从名字就能看出，它处理的是一轮&amp;quot;对话&amp;quot;（一个用户请求），但内部的轮数远不止一轮。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;用户消息 → [循环开始] → 构建消息 → 调用 LLM → 有工具调用？→ 执行工具 → 结果回灌 → 再次调用
↓
[循环结束] ← 无工具调用 → 输出最终回复
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;整个循环可以分成三个阶段：&lt;strong&gt;初始化、迭代、终结&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id="阶段一系统-prompt-的构建"&gt;阶段一：系统 Prompt 的构建&lt;/h2&gt;
&lt;p&gt;在循环开始之前，Agent 需要构建一个系统 Prompt。这个 Prompt 不是用户写的，是 Agent 自己&amp;quot;拼装&amp;quot;出来的，目的只有一个：&lt;strong&gt;告诉 LLM 你是谁、你能做什么、你现在处于什么环境&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;Hermes 把它拆成三层：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Stable（稳定层）&lt;/strong&gt;：身份声明、工具使用指南、环境信息、Skill 索引。这部分每个会话只构建一次，因为它是&amp;quot;不变的&amp;quot;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Context（上下文层）&lt;/strong&gt;：用户自定义的规则文件（比如项目根目录的 &lt;code&gt;AGENTS.md&lt;/code&gt;），以及一些平台特有的提示。这部分是&amp;quot;半稳定&amp;quot;的——用户改了规则文件，下次会话会刷新。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Volatile（易变层）&lt;/strong&gt;：记忆快照、用户画像、当前时间、会话 ID、当前模型。这部分每个会话都会不同，甚至有时会动态刷新。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这三层拼在一起，就是一个完整的系统 Prompt。比如它的典型结构可能是：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;[身份] 你是一个运行在终端里的 AI Agent...
[工具使用] 你可以调用以下工具：terminal, read_file, write_file...
[环境] 操作系统 Linux，当前目录 /root，后端终端 local...
[技能] 已安装技能：akshare-data-fetcher, github-pr-workflow...
[记忆] 用户偏好：简洁高效，不喜欢废话...
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="阶段二循环的燃料迭代预算"&gt;阶段二：循环的&amp;quot;燃料&amp;quot;——迭代预算&lt;/h2&gt;
&lt;p&gt;进入循环之前，Agent 需要知道一件事：&lt;strong&gt;最多能循环多少次？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果没有任何限制，Agent 可能陷入死循环：模型不断调用工具，工具结果不理想，模型再尝试，再失败，无限循环下去。&lt;/p&gt;
&lt;p&gt;Hermes 用一个叫做 &lt;code&gt;IterationBudget&lt;/code&gt; 的对象来管理这个预算。它有两个维度：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;max_total&lt;/strong&gt;：整个会话的总循环次数上限（默认 90 次）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;单轮预算&lt;/strong&gt;：每轮对话消耗 1 个预算&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;每次调用 LLM 之前，预算会被&amp;quot;消耗&amp;quot;一次。当预算耗尽时，循环强制终止。&lt;/p&gt;
&lt;p&gt;但还有一个细节：&lt;strong&gt;execute_code 工具可以&amp;quot;退款&amp;quot;&lt;/strong&gt;。因为 execute_code 本质上是一个程序化的调用（程序生成代码 → Agent 自动执行 → 返回结果），不算真正的&amp;quot;对话轮次&amp;quot;。所以调用这个工具时，预算会被退还。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;调用 execute_code → 消耗预算 → 执行完毕 → 退还预算
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这就是为什么你能用 Agent 跑很多代码，但不会快速耗尽预算。&lt;/p&gt;
&lt;h2 id="阶段三每次迭代内部发生了什么"&gt;阶段三：每次迭代内部发生了什么&lt;/h2&gt;
&lt;p&gt;每次循环内部，其实分成几个子步骤：&lt;/p&gt;
&lt;h3 id="1-中断检查"&gt;1. 中断检查&lt;/h3&gt;
&lt;p&gt;在调用 LLM 之前，先检查用户是否发来了新消息或 /stop 命令。如果有，立刻中断循环，返回迄今为止的结果。这种设计很实用——你可以在 Agent 执行到一半时喊停。&lt;/p&gt;
&lt;h3 id="2-调用-llm"&gt;2. 调用 LLM&lt;/h3&gt;
&lt;p&gt;构建好消息列表（系统 Prompt + 历史对话 + 当前工具结果）后，Agent 调用 LLM 的 API。这一步可能失败（网络超时、限流、模型错误），所以有一套重试和退避机制。&lt;/p&gt;
&lt;h3 id="3-解析响应"&gt;3. 解析响应&lt;/h3&gt;
&lt;p&gt;LLM 返回的响应有两种可能：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;纯文本&lt;/strong&gt;：模型认为任务已经完成，不再需要调用工具&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;带工具调用&lt;/strong&gt;：模型输出了工具调用指令（function call）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="4-工具调度的三道安检"&gt;4. 工具调度的&amp;quot;三道安检&amp;quot;&lt;/h3&gt;
&lt;p&gt;如果模型要求调用工具，Agent 不会立即执行，而是经过三层验证：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;名称校验&lt;/strong&gt;：检查工具名是否存在。如果模型产生幻觉（叫了一个不存在的工具名），尝试自动修复（比如 fuzzy 匹配相似的工具名）。如果修不好，最多重试 3 次，然后终止。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;参数校验&lt;/strong&gt;：检查工具的参数是否是合法的 JSON。如果是空字符串，自动补成 &lt;code&gt;{}&lt;/code&gt;（模型常见行为）。如果是损坏的 JSON，先尝试重试 3 次，还不行就注入一个&amp;quot;错误结果&amp;quot;让模型看到，让它自己修正。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;去重与限制&lt;/strong&gt;：检查并去除重复的调用（比如同一个工具被连续调两次相同的参数）。同时限制 &lt;code&gt;delegate_task&lt;/code&gt; 子代理的调用数量，防止无限递归。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="5-执行与结果回灌"&gt;5. 执行与结果回灌&lt;/h3&gt;
&lt;p&gt;通过验证的工具调用会被分发执行。Hermes 支持并行执行（如果模型一次返回多个独立的工具调用），也支持串行。&lt;/p&gt;
&lt;p&gt;执行结果会被格式化为统一的 tool role 消息，追加到历史对话中。下一次循环时，模型就能看到这些结果。&lt;/p&gt;
&lt;h3 id="6-压缩检查"&gt;6. 压缩检查&lt;/h3&gt;
&lt;p&gt;每次工具执行完毕，Agent 会估算当前上下文（历史对话 + 工具结果）的 token 数。如果接近上下文窗口的阈值（默认 50%），就会触发压缩——用一个便宜的模型把早期的对话摘要替换掉，保留关键信息，释放空间。&lt;/p&gt;
&lt;p&gt;这就是为什么你可以在一个会话里聊很久，而不会遇到&amp;quot;上下文太长&amp;quot;的错误。&lt;/p&gt;
&lt;h2 id="什么时候循环结束"&gt;什么时候循环结束？&lt;/h2&gt;
&lt;p&gt;循环终止的条件有四个：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;模型返回纯文本&lt;/strong&gt;（没有工具调用）→ 任务完成，返回最终回复&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;预算耗尽&lt;/strong&gt;（达到 max_total）→ 强制停止，返回已完成的进度&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用户中断&lt;/strong&gt;（发送新消息或 /stop）→ 优雅退出，保留当前状态&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工具护栏触发&lt;/strong&gt;（检测到危险操作）→ 立即停止，输出安全警告&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="工程启示"&gt;工程启示&lt;/h2&gt;
&lt;p&gt;从 Hermes 的实现中，我们可以看到几个 Agent 工程的设计原则：&lt;/p&gt;
&lt;h3 id="1-循环必须有预算"&gt;1. 循环必须有预算&lt;/h3&gt;
&lt;p&gt;没有预算限制的 Agent 是不可靠的。预算不仅是&amp;quot;保护伞&amp;quot;，也是&amp;quot;进度表&amp;quot;——用户可以据此判断任务的复杂度。&lt;/p&gt;
&lt;h3 id="2-工具执行是窗口不是黑盒"&gt;2. 工具执行是&amp;quot;窗口&amp;quot;，不是&amp;quot;黑盒&amp;quot;&lt;/h3&gt;
&lt;p&gt;Agent 不是盲目信任模型的输出。名称校验、参数校验、去重、限流——这些&amp;quot;安检&amp;quot;步骤确保工具调用是安全、合理、可预测的。&lt;/p&gt;
&lt;h3 id="3-错误恢复是对话式的"&gt;3. 错误恢复是&amp;quot;对话式&amp;quot;的&lt;/h3&gt;
&lt;p&gt;当工具调用失败时，Agent 不是直接报错退出，而是把错误信息作为 tool result 喂回给模型，让它有机会自己修正。这种&amp;quot;容错对话&amp;quot;是 Agent 智能的核心体现。&lt;/p&gt;
&lt;h3 id="4-上下文是有价商品"&gt;4. 上下文是&amp;quot;有价商品&amp;quot;&lt;/h3&gt;
&lt;p&gt;Agent 的每一次 API 调用都在消耗 token，而上下文窗口是有限的。压缩机制把&amp;quot;长对话&amp;quot;变成&amp;quot;摘要 + 近期内容&amp;quot;，让 Agent 能处理远超窗口容量的任务。&lt;/p&gt;
&lt;h2 id="总结"&gt;总结&lt;/h2&gt;
&lt;p&gt;Agent Loop 不是一个高深的概念，它就是一个 while 循环。但这个循环内部的设计——迭代预算、中断处理、工具调度、错误恢复、上下文压缩——决定了 Agent 的可靠性、效率和智能程度。&lt;/p&gt;
&lt;p&gt;下一个系列文章，我们将深入工具系统：Hermes 是如何让 50+ 个工具自动注册、发现、分发和执行的。&lt;/p&gt;</description></item></channel></rss>