作者:互联网 时间: 2026-07-01 09:24:52
上篇我们拆了核心循环。Agent 的核心决策逻辑是:LLM 决定"调用什么工具",然后执行它。

那问题来了——这些工具是怎么注册的?LLM 怎么知道有哪些工具可用?执行工具的时候发生了什么?
这篇就来拆 Hermes 的工具系统。
这个问题其实是整个 AI Agent 系统的基石。
当你跟一个普通的 LLM(比如 ChatGPT)聊天时,你发给它的消息格式是:
messages = [{"role": "user", "content": "今天天气怎么样?"}]
但对于支持 Function Calling / Tool Calling 的 LLM,你可以额外传入工具定义:
response = client.chat.completions.create(model="...",messages=[...],tools=[# ← 这是关键!{"type": "function","function": {"name": "get_weather","description": "获取指定城市的天气","parameters": {"type": "object","properties": {"city": {"type": "string", "description": "城市名"}},"required": ["city"]}}}])
LLM 看到 tools 参数后,会做判断:
tool_calls,告诉你要调什么函数、传什么参数所以 Agent 的工具系统,本质上就是把一组函数定义转换成 LLM 能理解的 Schema 格式,然后执行 LLM 选择的函数。
Hermes 的工具系统由三部分组成:
tools/registry.py← 注册中心(核心枢纽)tools/*.py ← 各个工具的实现model_tools.py ← 对外接口(给 run_agent.py 用)
registry.py 是一个全局单例的工具注册表。核心数据结构很简单:
class ToolRegistry:def __init__(self):self._tools: Dict[str, ToolDef] = {}# name -> ToolDefself._initialized = Falsedef register(self, name, toolset, schema, handler,check_fn=None, requires_env=None):self._tools[name] = ToolDef(name=name,toolset=toolset,schema=schema,# OpenAI function calling schemahandler=handler,# 实际的 Python 函数check_fn=check_fn,# 可用性检查(比如依赖是否安装)requires_env=requires_env)# 全局单例registry = ToolRegistry()
就这么简单——一个字典,以工具名为 key,存着工具的定义和处理函数。
discover_builtin_tools()Hermes 不需要手动注册每个工具。它会在启动时自动扫描 tools/ 目录下所有的 .py 文件:
def discover_builtin_tools(tools_dir=None):"""导入 tools/ 目录下所有自注册工具模块"""tools_path = tools_dir or Path(__file__).resolve().parentfor f in sorted(tools_path.iterdir()):if f.suffix == '.py' and f.name != '__init__.py':if _module_registers_tools(f):# 导入这个模块,模块内的 registry.register() 会被执行importlib.import_module(f"tools.{f.stem}")
这里用了个巧妙的方法:静态分析。它先扫描文件,检查文件里是不是有 registry.register(...) 调用,有才导入。这样避免了导入那些不需要的工具模块。
def _module_registers_tools(module_path: Path) -> bool:"""检查模块文件是否有 registry.register() 调用"""try:source = module_path.read_text(encoding="utf-8")tree = ast.parse(source, filename=str(module_path))except (OSError, SyntaxError):return Falsereturn any(_is_registry_register_call(stmt) for stmt in tree.body)
使用 AST(抽象语法树)静态分析,而不是简单的字符串搜索,更精确。
每个工具是一个独立的 .py 文件。以 tools/web_search_tool.py 为例:
import json, osfrom tools.registry import registrydef check_requirements() -> bool:"""检查依赖是否可用"""return bool(os.getenv("OPENROUTER_API_KEY") or os.getenv("GOOGLE_API_KEY"))def web_search(query: str, max_results: int = 5, task_id: str = None) -> str:"""执行网络搜索"""# ... 实际搜索逻辑 ...return json.dumps({"results": [...]})# 在全局注册registry.register(name="web_search",toolset="web", # 属于哪个工具集schema={ # OpenAI Function Calling Schema"name": "web_search","description": "搜索互联网获取最新信息","parameters": {"type": "object","properties": {"query": {"type": "string", "description": "搜索关键词"},"max_results": {"type": "integer","description": "返回结果数量(默认5)"}},"required": ["query"]}},handler=lambda args, **kw: web_search(query=args.get("query", ""),max_results=args.get("max_results", 5),task_id=kw.get("task_id")),check_fn=check_requirements,requires_env=["OPENROUTER_API_KEY"])
每个工具只需要:
添加一个新工具,只需要创建一个文件,写一个函数,一个注册调用。三件事。 下一篇我可以用这个写个小例子。
model_tools.py 是对外暴露的接口层:
# 获取所有已启用工具的定义(用于传给LLM)def get_tool_definitions(enabled_toolsets, disabled_toolsets, quiet_mode):"""根据启用的工具集,返回工具Schema列表"""tools = []for name, tool in registry._tools.items():if tool.toolset in enabled_toolsets and tool.toolset not in disabled_toolsets:if tool.check_fn is None or tool.check_fn():tools.append(tool.schema)return tools# 执行工具调用def handle_function_call(function_name, function_args, task_id):"""根据函数名查找并执行工具"""if function_name not in registry._tools:return json.dumps({"error": f"未知工具: {function_name}"})tool = registry._tools[function_name]try:return tool.handler(function_args, task_id=task_id)except Exception as e:return json.dumps({"error": str(e)})
Hermes 有 20+ 工具集,每个工具集是一组相关工具的集合:
# toolsets.py_HERMES_CORE_TOOLS = ["web", # 网络搜索、内容提取"browser", # 浏览器自动化"terminal",# 终端命令执行"file",# 文件读写搜索"code_execution",# Python 沙盒执行"vision",# 图像分析"memory",# 持久化记忆"delegation",# 子袋里"cronjob", # 定时任务"skills",# 技能管理"messaging", # 消息发送"session_search", # 历史会话搜索...]
用户可以在配置里开启或关闭某些工具集:
hermes tools # 交互式管理hermes tools enable web # 开启网络工具hermes tools disable browser# 关闭浏览器工具
每个工具集可能有对应的环境依赖:
TOOLSET_REQUIREMENTS = {"browser": ["PLAYWRIGHT_BROWSERS_PATH", "BROWSERBASE_API_KEY"],"terminal": [],# 始终可用"image_gen": ["OPENAI_API_KEY"],}
只有满足依赖条件的工具集才会被加载。
启动时│▼discover_builtin_tools()│ 扫描 tools/*.py▼各模块执行 registry.register()│ 工具信息写入全局 registry▼用户提问│▼get_tool_definitions(enabled_toolsets)│ 从 registry 筛选启用的工具▼传给 LLM (tools=...)│▼LLM 决定调用某个工具│ 返回 tool_calls▼handle_function_call(name, args)│ 从 registry 查找 handler▼执行工具 → 返回结果 → 结果追加到对话
作为一个老程序员,拆完这套工具系统后有几个感受:
每个工具是一个独立文件,互不依赖。加一个新工具不影响现有工具。这是插件化架构的精髓。
不需要在配置里列"我有哪些工具",代码自描述。这是 Java SPI、Python entry_points 那套思想,但更轻量。
工具定义跟 OpenAI Function Calling Schema 直接对应。这套 Schema 已经成为事实标准——Anthropic、Google、Mistral 都兼容它。Hermes 基于标准做,天然兼容性好。
check_fn 机制让工具可以在依赖不满足时自动隐藏。比如没有安装浏览器驱动,Browser 工具就不显示给 LLM。这让系统在弱环境下也能优雅运行。
说了这么多,不如实战一下。假如我要给 Hermes 加一个"计算器"工具:
新建 tools/calculator.py:
import jsonfrom tools.registry import registrydef calculate(expression: str, task_id: str = None) -> str:"""安全执行数学表达式"""# 只允许数字和运算符的白名单allowed = set("0123456789+-*/()., ")if not all(c in allowed for c in expression):return json.dumps({"error": "表达式包含非法字符"})try:result = eval(expression, {"__builtins__": {}}, {})return json.dumps({"result": result})except Exception as e:return json.dumps({"error": str(e)})registry.register(name="calculate",toolset="web", # 归到 web 工具集(或者新建一个)schema={"name": "calculate","description": "计算数学表达式,支持 +-*/ 和括号","parameters": {"type": "object","properties": {"expression": {"type": "string","description": "数学表达式,例如 '1+2*3'"}},"required": ["expression"]}},handler=lambda args, **kw: calculate(expression=args.get("expression", ""),task_id=kw.get("task_id")),)
重启 Hermes,LLM 就会知道有 calculate 这个工具可用。
现在 Agent 会思考(核心循环)了,会干活(工具系统)了。但还有一个关键问题没解决:它怎么知道自己是谁?该怎么表现?
第五篇我们拆 System Prompt 的组装过程——agent/prompt_builder.py。
你会看到:
元思未来 · 行稳致远,进而有为