Hermes Agent:工具与技能的加载、执行与规模化策略

Hermes Agent:工具与技能的加载、执行与规模化策略

分析基于 hermes-agent-main 源码(2026-05),覆盖 tools/registry.pytools/schema_sanitizer.pytools/tool_result_storage.pymodel_tools.pyagent/agent_init.pyagent/system_prompt.pyagent/prompt_builder.pyagent/conversation_loop.pyagent/context_compressor.pyagent/agent_runtime_helpers.py 等核心模块。


一、整体流程概览

下图展示了 Hermes Agent 从收到用户请求到返回最终响应的完整主流程,包括初始化、系统提示构建、主循环(LLM 调用 → 工具执行 → 压缩 → 循环)以及持久化收尾:
#mermaid-svg-2IMsnUuBm2LNKu31{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-2IMsnUuBm2LNKu31 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-2IMsnUuBm2LNKu31 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-2IMsnUuBm2LNKu31 .error-icon{fill:#552222;}#mermaid-svg-2IMsnUuBm2LNKu31 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-2IMsnUuBm2LNKu31 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-2IMsnUuBm2LNKu31 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-2IMsnUuBm2LNKu31 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-2IMsnUuBm2LNKu31 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-2IMsnUuBm2LNKu31 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-2IMsnUuBm2LNKu31 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-2IMsnUuBm2LNKu31 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-2IMsnUuBm2LNKu31 .marker.cross{stroke:#333333;}#mermaid-svg-2IMsnUuBm2LNKu31 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-2IMsnUuBm2LNKu31 p{margin:0;}#mermaid-svg-2IMsnUuBm2LNKu31 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-2IMsnUuBm2LNKu31 .cluster-label text{fill:#333;}#mermaid-svg-2IMsnUuBm2LNKu31 .cluster-label span{color:#333;}#mermaid-svg-2IMsnUuBm2LNKu31 .cluster-label span p{background-color:transparent;}#mermaid-svg-2IMsnUuBm2LNKu31 .label text,#mermaid-svg-2IMsnUuBm2LNKu31 span{fill:#333;color:#333;}#mermaid-svg-2IMsnUuBm2LNKu31 .node rect,#mermaid-svg-2IMsnUuBm2LNKu31 .node circle,#mermaid-svg-2IMsnUuBm2LNKu31 .node ellipse,#mermaid-svg-2IMsnUuBm2LNKu31 .node polygon,#mermaid-svg-2IMsnUuBm2LNKu31 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-2IMsnUuBm2LNKu31 .rough-node .label text,#mermaid-svg-2IMsnUuBm2LNKu31 .node .label text,#mermaid-svg-2IMsnUuBm2LNKu31 .image-shape .label,#mermaid-svg-2IMsnUuBm2LNKu31 .icon-shape .label{text-anchor:middle;}#mermaid-svg-2IMsnUuBm2LNKu31 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-2IMsnUuBm2LNKu31 .rough-node .label,#mermaid-svg-2IMsnUuBm2LNKu31 .node .label,#mermaid-svg-2IMsnUuBm2LNKu31 .image-shape .label,#mermaid-svg-2IMsnUuBm2LNKu31 .icon-shape .label{text-align:center;}#mermaid-svg-2IMsnUuBm2LNKu31 .node.clickable{cursor:pointer;}#mermaid-svg-2IMsnUuBm2LNKu31 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-2IMsnUuBm2LNKu31 .arrowheadPath{fill:#333333;}#mermaid-svg-2IMsnUuBm2LNKu31 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-2IMsnUuBm2LNKu31 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-2IMsnUuBm2LNKu31 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2IMsnUuBm2LNKu31 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-2IMsnUuBm2LNKu31 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2IMsnUuBm2LNKu31 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-2IMsnUuBm2LNKu31 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-2IMsnUuBm2LNKu31 .cluster text{fill:#333;}#mermaid-svg-2IMsnUuBm2LNKu31 .cluster span{color:#333;}#mermaid-svg-2IMsnUuBm2LNKu31 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-2IMsnUuBm2LNKu31 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-2IMsnUuBm2LNKu31 rect.text{fill:none;stroke-width:0;}#mermaid-svg-2IMsnUuBm2LNKu31 .icon-shape,#mermaid-svg-2IMsnUuBm2LNKu31 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2IMsnUuBm2LNKu31 .icon-shape p,#mermaid-svg-2IMsnUuBm2LNKu31 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-2IMsnUuBm2LNKu31 .icon-shape .label rect,#mermaid-svg-2IMsnUuBm2LNKu31 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2IMsnUuBm2LNKu31 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-2IMsnUuBm2LNKu31 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-2IMsnUuBm2LNKu31 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 对话阶段:主循环(最多 max_iterations 次)
初始化阶段(每个会话执行一次)






修复失败

修复成功
否,无tool_calls
进程启动
import model_tools

模块顶层自动执行
discover_builtin_tools()

AST扫描 tools/*.py → importlib导入 → register()
discover_plugins()

扫描 ~/.hermes/plugins/ → register()
AIAgent.init()
get_tool_definitions(enabled_toolsets, disabled_toolsets)
memoize

cache 命中?
返回浅拷贝
_compute_tool_definitions()

resolve_toolset → check_fn过滤

动态Schema重建 → sanitize
agent.tools = ...

agent.valid_tool_names = {name,...}
build_system_prompt_parts()
stable tier

SOUL.md / 工具guidance / 技能索引 / 平台hints
context tier

AGENTS.md / .hermes.md

(injection扫描 + 20K截断)
volatile tier

记忆快照 + 日期/Session/Model
agent._cached_system_prompt(会话内不变)
run_conversation(user_message)
Preflight压缩

estimate_tokens ≥ threshold?
compress()

_prune_old_tool_results + LLM摘要
sanitize_api_messages()

清理孤儿 tool_call/result 对
LLM API 调用

携带 agent.tools + system_prompt
有 tool_calls?
名称验证

name in valid_tool_names?
repair_tool_call()

5种修复策略 + difflib模糊匹配(0.7)
返回错误+可用工具列表

最多重试3次
coerce_tool_args()

类型强制转换
handle_function_call()
pre_tool_call 插件钩子

可block执行
registry.dispatch()

O(1)字典查找 → handler()执行
maybe_persist_tool_result()

超100K chars → 落盘返回预览
post_tool_call / transform_tool_result 钩子
enforce_turn_budget()

本轮结果总量 > 200K → 溢出最大的
返回最终回复
_persist_session()

SQLite 持久化对话历史
结束

整体分为两大阶段:

  • 初始化阶段AIAgent.__init__):工具发现、注册、过滤、Schema 构建,以及系统提示(含技能索引)的组装。每个会话只执行一次。
  • 对话阶段run_conversation 主循环):每轮接收用户消息,调用 LLM,解析并执行工具,处理结果,循环直到 LLM 不再需要工具。

二、工具的加载与执行原理

2.1 工具自注册机制(tools/registry.py

Hermes 不维护工具列表文件。每个工具文件在模块顶层调用 registry.register()进程启动时自动完成注册

python 复制代码
# tools/terminal_tool.py(示例结构)
registry.register(
    name="terminal",
    toolset="terminal",
    schema={
        "name": "terminal",
        "description": "Run shell commands...",
        "parameters": { ... }   # JSON Schema
    },
    handler=lambda args, **kw: run_terminal(args, task_id=kw.get("task_id")),
    check_fn=check_terminal_requirements,  # 可选:运行时可用性检测
    requires_env=["DOCKER_HOST"],           # 可选:需要哪些环境变量
    emoji="💻",
    dynamic_schema_overrides=None,          # 可选:运行时动态覆盖 schema 描述
)

ToolEntry dataclass(__slots__ 优化内存)保存所有元数据:

复制代码
ToolEntry
  ├── name                   工具唯一名称
  ├── toolset                所属工具集
  ├── schema                 OpenAI function-calling 格式的 JSON Schema
  ├── handler                真实 Python 函数引用(O(1) 调用,无字符串查找)
  ├── check_fn               可用性探测函数(TTL 30s 缓存)
  ├── requires_env           需要的环境变量列表(仅供 UI 展示缺失提示)
  ├── is_async               是否是异步 handler
  ├── emoji                  CLI 显示用的 emoji
  ├── max_result_size_chars  该工具结果的 inline 上限(字节数)
  └── dynamic_schema_overrides  零参数 callable,返回运行时 schema 覆盖字段

_generation 计数器:每次 register() / deregister() 都自增,用于外层 memoization 的失效检测。

防重复注册规则

  • 同名同 toolset 注册:允许覆盖(MCP server 刷新常见场景)
  • 同名跨 toolset 注册:默认 拒绝 ,打印 ERROR 日志;需传 override=True 才允许(插件替换内建工具时的明确 opt-in)
  • MCP-to-MCP 跨名注册:允许(两个 MCP server 工具名冲突时后注册的覆盖)

2.2 工具自动发现(model_tools.pydiscover_builtin_tools()

model_tools.py 在模块顶层直接调用 discover_builtin_tools(),这是整个工具体系的启动入口:

python 复制代码
# model_tools.py(模块顶层,import 时自动执行)
discover_builtin_tools()   # 扫描 tools/*.py,触发所有 register() 调用
discover_plugins()         # 扫描 ~/.hermes/plugins/,触发插件工具注册

discover_builtin_tools() 的实现(tools/registry.py:62):

python 复制代码
def discover_builtin_tools(tools_dir=None):
    # 1. glob tools/*.py,排除 __init__.py, registry.py, mcp_tool.py
    # 2. 对每个 .py 文件做 AST 解析,检测 module.body 顶层是否有
    #    registry.register() 调用(不深入函数体,避免误导入辅助模块)
    # 3. importlib.import_module("tools.<stem>")
    #    → 模块顶层代码执行 → register() 被调用 → ToolEntry 写入 registry._tools

AST 扫描的意义:只 import 真正注册工具的文件,不触碰纯辅助模块(budget_config.pyschema_sanitizer.py 等),避免副作用。

2.3 工具 Schema 构建(model_tools.get_tool_definitions()

AIAgent.__init__agent/agent_init.py:910)调用 get_tool_definitions() 将注册表转换为 LLM 可用的工具列表,存入 agent.tools

python 复制代码
# agent/agent_init.py:910
agent.tools = _ra().get_tool_definitions(
    enabled_toolsets=enabled_toolsets,
    disabled_toolsets=disabled_toolsets,
    quiet_mode=agent.quiet_mode,
)
agent.valid_tool_names = {tool["function"]["name"] for tool in agent.tools}

get_tool_definitions() 内部完整流程(model_tools.py:270):

复制代码
①  memoization 快速返回(quiet_mode 下,cache_key 命中则直接返回浅拷贝)
②  _compute_tool_definitions():
    a. 解析 enabled_toolsets / disabled_toolsets
       ├── validate_toolset():检查 toolset 名称合法性
       ├── resolve_toolset():展开 toolset 名称为具体工具名集合
       ├── HERMES_KANBAN_TASK 环境变量:dispatcher 派生 worker 时自动补充 kanban toolset
       └── set 运算:enabled_union - disabled = tools_to_include
    b. registry.get_definitions(tools_to_include)
       ├── 按 tool_names 遍历注册表快照
       ├── 每个工具调用 _check_fn_cached(entry.check_fn)(TTL 30s + per-call 内层缓存)
       ├── check_fn 返回 False → 跳过,LLM 不可见
       ├── 应用 dynamic_schema_overrides(如 delegate_task 的并发数/深度描述)
       └── 包装为 {"type": "function", "function": schema_with_name}
    c. 动态 Schema 重建:
       ├── execute_code:只列出当前可用的 sandbox 工具子集(防止 LLM 被告知不可用工具)
       └── discord / discord_admin:根据 bot 权限和用户 action 白名单重建
    d. 跨工具引用清理:
       browser_navigate 描述中的 "prefer web_search" 提示
       → 当 web_search 不可用时删除(防止 LLM 幻觉调用不存在的工具)
    e. sanitize_tool_schemas():修复不兼容的 JSON Schema 格式
③  缓存结果并返回浅拷贝(共享 dict 引用,多 Agent 不会相互污染)

最终 agent.tools 是一个标准 OpenAI format 列表:

json 复制代码
[
  {
    "type": "function",
    "function": {
      "name": "terminal",
      "description": "Run shell commands...",
      "parameters": { "type": "object", "properties": { ... } }
    }
  }
]

2.4 工具执行完整调用链

LLM 返回工具调用请求后,执行链如下:

复制代码
LLM 返回 assistant_message.tool_calls
  │
  ▼
① 名称验证与自动修复(conversation_loop.py:3357)
  ├─ name in agent.valid_tool_names?
  │     ├─ 是 → 继续
  │     └─ 否 → repair_tool_call(name)(agent_runtime_helpers.py:1706)
  │               ├─ 小写直接匹配(lowercase)
  │               ├─ 连字符/空格 → 下划线(_norm)
  │               ├─ CamelCase → snake_case(_camel_snake)
  │               ├─ 剥离 _tool/-tool/tool 后缀,最多两轮
  │               │    TodoTool_tool → TodoTool → todo_tool
  │               └─ difflib.get_close_matches(cutoff=0.7) 模糊匹配兜底
  │           修复成功 → 打印 "🔧 Auto-repaired: 'X' -> 'Y'" 继续
  │           修复失败 → 向 LLM 返回错误消息 + 可用工具列表,最多重试 3 次
  │
  ▼
② JSON 参数解析与类型强制(model_tools.coerce_tool_args())
  ├─ "42" → 42(integer schema)
  ├─ "true" → True(boolean schema)
  ├─ "https://a.com" → ["https://a.com"](array schema 单值自动包装)
  ├─ '["a","b"]' 字符串 → ["a","b"](JSON 字符串解析为 array)
  └─ "null" → None(nullable schema)
  │
  ▼
③ handle_function_call()(model_tools.py:760)
  ├─ pre_tool_call 插件钩子(可返回 block 指令阻止执行)
  ├─ ACP 编辑审批(write_file / patch 在 VS Code/Zed 中需用户确认)
  ├─ file_tools 读取连续计数器重置(非读操作时重置计数,防 read-loop 检测误判)
  ├─ registry.dispatch(name, args, task_id=...)
  │     ├─ O(1) 字典查找:entry = _tools[name]
  │     ├─ async handler → _run_async()(主线程/worker线程/网关 event loop 三路桥接)
  │     ├─ entry.handler(args, **kwargs) 执行实际函数
  │     └─ 异常捕获 → _sanitize_tool_error()
  │             剥离 XML 角色标签(</tool_call> 等)、代码围栏、CDATA,截断至 2000 字
  ├─ maybe_persist_tool_result()(超大结果落盘,见第五章)
  ├─ post_tool_call 插件钩子(可观察结果,接收 duration_ms)
  └─ transform_tool_result 插件钩子(可替换结果字符串,first-wins)

三、技能的加载与执行原理

3.1 技能目录结构

技能存放于 ~/.hermes/skills/<category>/<skill_name>/SKILL.md

复制代码
~/.hermes/skills/
  qa/
    dogfood/
      SKILL.md         # 完整技能文档(通常几百行)
    DESCRIPTION.md     # category 一行描述,注入索引时作为 category 标题注释
  devops/
    kubernetes/
      SKILL.md
    terraform/
      SKILL.md
  ...

每个 SKILL.md 包含 YAML frontmatter(namedescriptionversionplatformsmetadata.hermes.*)和正文(目标、前提、操作步骤、注意事项、工具参考等)。

3.2 技能索引注入 System Prompt 的前提条件

build_system_prompt_parts()agent/system_prompt.py:169)在组装 stable tier 时,只有当 valid_tool_names 中包含 skill_viewskill_manage 时才注入技能索引。没有这两个工具时,Agent 不具备读取技能的能力,注入索引毫无意义:

python 复制代码
# agent/system_prompt.py:169
has_skills_tools = any(
    name in agent.valid_tool_names
    for name in ['skills_list', 'skill_view', 'skill_manage']
)
if has_skills_tools:
    avail_toolsets = {
        toolset for toolset in (
            get_toolset_for_tool(t) for t in agent.valid_tool_names
        ) if toolset
    }
    skills_prompt = build_skills_system_prompt(
        available_tools=agent.valid_tool_names,
        available_toolsets=avail_toolsets,
    )

3.3 技能索引的构建(build_skills_system_prompt()agent/prompt_builder.py:983

构建完成的索引格式:

复制代码
## Skills (mandatory)
Before replying, scan the skills below. If a skill matches or is even
partially relevant to your task, you MUST load it with skill_view(name)
and follow its instructions. Err on the side of loading...

<available_skills>
  qa: Quality assurance workflows
    - dogfood: Exploratory QA of web apps: find bugs, evidence, reports.
  devops:
    - kubernetes: Deploy and manage Kubernetes clusters.
    - terraform: Provision infrastructure as code.
</available_skills>

Only proceed without loading a skill if genuinely none are relevant.

每个技能只有 name: description(≤60字) 一行。100 个技能 ≈ 2000 token,500 个技能 ≈ 10000 token。

构建流程:

复制代码
build_skills_system_prompt(available_tools, available_toolsets)
  │
  ├─ L1 内存 LRU 命中?(cache_key 含 skills_dir / toolsets / platform / disabled)
  │     └─ 命中 → 直接返回,move_to_end(LRU)
  │
  ├─ L2 磁盘快照命中?(_load_skills_snapshot:mtime_ns + size 逐文件对比)
  │     ├─ 命中 → 从 snapshot["skills"] 读取 pre-parsed 元数据
  │     │         → 过滤 + 组装索引字符串
  │     └─ 未命中 → L3 冷路径
  │
  ├─ L3 冷路径(全量扫描)
  │     ├─ iter_skill_index_files(skills_dir, "SKILL.md") 遍历所有技能文件
  │     ├─ 每个文件:_parse_skill_file() → AST 解析 frontmatter + 提取 description
  │     ├─ iter_skill_index_files(skills_dir, "DESCRIPTION.md") 读取 category 描述
  │     └─ _write_skills_snapshot() 将结果写入磁盘(供下次冷启动跳过扫描)
  │
  ├─ 扫描外部技能目录(skills.external_dirs 配置,无快照缓存)
  │     └─ 本地技能名优先(seen_skill_names 去重,外部同名技能跳过)
  │
  ├─ 对每个技能条目执行 _skill_should_show() 条件过滤
  │
  └─ 按 category 分组 → sorted → 组装索引字符串 → 写入 L1 缓存

3.4 技能条件过滤(_skill_should_show()agent/prompt_builder.py:952

以下情况的技能完全不出现在索引中(任一条件满足即隐藏):

python 复制代码
# requires_tools:技能需要某工具,但该工具未在当前会话加载
for t in conditions.get("requires_tools", []):
    if t not in available_tools: return False

# requires_toolsets:技能需要某工具集,但未启用
for ts in conditions.get("requires_toolsets", []):
    if ts not in available_toolsets: return False

# fallback_for_tools:该技能是某工具的降级替代,但那个工具已可用 → 不需要降级
for t in conditions.get("fallback_for_tools", []):
    if t in available_tools: return False

# fallback_for_toolsets:同上,针对工具集粒度
# platforms:当前 OS 不在技能支持的平台列表中(frontmatter platforms 字段)

3.5 技能按需全文加载

LLM 扫描索引后,若判断某技能相关,主动调用 skill_view(name="dogfood"),此时才读取完整 SKILL.md 并注入当前 turn 的 context:

复制代码
System Prompt stable tier(永驻,每轮不变):
  "  qa:\n    - dogfood: Exploratory QA of web apps..."
         ↓
  LLM 判断任务与 QA 测试相关
         ↓
  tool_call: skill_view(name="dogfood")
         ↓
  读取 ~/.hermes/skills/qa/dogfood/SKILL.md(完整内容,可达几千行)
  作为 tool result 注入当前 turn 的 messages
         ↓
  LLM 根据完整 SKILL.md 中的 Phase 1-5 流程规划并执行

绝大多数技能的完整内容永远不进入 context window,只有被 LLM 主动调用时才加载。


四、系统提示的三层结构

build_system_prompt_parts()agent/system_prompt.py:60)将系统提示分为三个 tier,拼接顺序设计为最大化 Prefix Cache 命中:

复制代码
┌─────────────────────────────────────────────────────────────┐
│  stable tier(会话内不变,最大化 Prefix Cache 命中)         │
│                                                             │
│  ├─ SOUL.md(用户自定义身份)或 DEFAULT_AGENT_IDENTITY      │
│  ├─ HERMES_AGENT_HELP_GUIDANCE(关于 Hermes 本身的帮助指引)│
│  ├─ 工具感知行为指导:                                      │
│  │     memory/session_search/skill_manage 各有专属 guidance │
│  │     kanban worker/orchestrator 生命周期指导              │
│  │     computer_use 专项指导(macOS)                       │
│  ├─ TOOL_USE_ENFORCEMENT_GUIDANCE(按模型名称决定是否注入)  │
│  │     Gemini/Gemma → GOOGLE_MODEL_OPERATIONAL_GUIDANCE     │
│  │     GPT/Codex/Grok → OPENAI_MODEL_EXECUTION_GUIDANCE     │
│  ├─ 技能索引(build_skills_system_prompt())                │
│  ├─ 环境提示(WSL/Termux 路径提示、active profile 名称)    │
│  └─ 平台提示(telegram/discord/cli 各有不同行为指导)       │
│                                                             │
├─────────────────────────────────────────────────────────────┤
│  context tier(依赖 cwd,跨会话可变)                        │
│                                                             │
│  ├─ caller 传入的 system_message(可选)                    │
│  └─ build_context_files_prompt():                          │
│       ├─ SOUL.md(已在 stable 加载则跳过,避免重复)         │
│       ├─ AGENTS.md / agents.md(cwd 顶层,不递归)          │
│       ├─ .hermes.md / HERMES.md(向 git root 递归查找)     │
│       └─ .cursorrules / .cursor/rules/*.mdc                 │
│       所有文件先过 prompt injection 扫描,再硬截断 20K chars │
│                                                             │
├─────────────────────────────────────────────────────────────┤
│  volatile tier(每次会话可变,不参与 prefix cache)          │
│                                                             │
│  ├─ 内建记忆快照(memory store format_for_system_prompt)   │
│  ├─ USER.md 用户画像                                        │
│  ├─ 外部记忆 provider block(honcho/mem0 等)               │
│  └─ 日期(精确到天,不含分钟,避免每分钟失效缓存)            │
│       + Session ID + Model + Provider 行                    │
└─────────────────────────────────────────────────────────────┘

缓存设计约束 :系统提示在 __init__ 末尾一次性构建,缓存到 agent._cached_system_prompt,整个会话期间不重建(仅在上下文压缩后重建)。这是保证 Anthropic / OpenRouter prefix cache 命中的核心约束------系统提示字节不变,云端 KV cache 才能复用。


五、规模化带来的核心矛盾与对应策略

5.1 核心问题

问题 具体表现
Schema Token 膨胀 每个工具 Schema 约 200--800 token;100 个工具占 2--8 万 token;若全部注入则小 context 模型直接溢出
技能内容 Token 膨胀 每个 SKILL.md 约 500--3000 token;几百个技能全文注入不可行
工具结果 Token 累积 terminal 输出、文件内容随对话轮次累积,逐渐占满剩余 context window
模型幻觉与精度下降 LLM 面对 100+ 工具时幻觉不存在的名称(TodoTool_toolBrowserClick_tool),混淆参数类型

5.2 策略一:Toolset 白名单过滤

所有工具分组为 toolset,实例化时只传入当前场景需要的工具集:

python 复制代码
agent = AIAgent(
    enabled_toolsets=["messaging", "search", "file"],
    disabled_toolsets=["code_execution", "discord"],
)

disabled_toolsets最终减法 ,无论 enabled_toolsets 如何组合,被 disabled 的工具集严格剔除(set.difference_updatemodel_tools.py:355)。

已定义 20+ 个 toolset:browserclarifycode_executioncronjobdebuggingdelegationdiscordfilememorymessagingsearchterminaltodottsvisionweb 等。

5.3 策略二:check_fn 运行时可用性过滤

注册时附带的 check_fnget_definitions() 中自动调用,返回 False 的工具不出现在 LLM 的工具列表中:

  • 未安装 Playwright → browser 系 10 个工具全部不可见
  • DOCKER_HOST → Docker-backed terminal 不可见
  • Discord bot token 未配置 → discord 工具不可见

TTL 缓存_check_fn_cache dict(tools/registry.py:120)+ threading.Lock,30 秒有效。同一次 get_definitions() 调用内还有 per-call 内层缓存 check_results dict,同一个 check_fn 只运行一次。

通过 invalidate_check_fn_cache() 可立即清空缓存(hermes tools enable 命令会调用此函数)。

5.4 策略三:get_tool_definitions() 整体 memoization

model_tools.py 维护模块级 _tool_defs_cache dict(quiet_mode 下启用):

python 复制代码
cache_key = (
    frozenset(enabled_toolsets),       # toolset 参数
    frozenset(disabled_toolsets),
    registry._generation,              # 任何 register/deregister 后自增 → 自动失效
    (config_mtime_ns, config_size),    # config.yaml 变更后失效(dynamic schema 依赖配置)
    bool(HERMES_KANBAN_TASK),          # kanban worker 有额外工具
)

命中时返回浅拷贝列表(list(cached),共享 dict 引用),同时更新 _last_resolved_tool_names

为什么是浅拷贝而非直接返回原引用AIAgent.__init__ 可能向 agent.tools 追加额外工具(memory、LCM),若多个 Agent 共享同一 list 引用,工具名称会重复累积,导致 DeepSeek/Moonshot 等强制唯一工具名的 Provider 报 HTTP 400(issue #17335)。

5.5 策略四:Schema 体积压缩与修复

正常路径 --- sanitize_tool_schemas()tools/schema_sanitizer.py:40

每次 get_tool_definitions() 末尾执行,对深拷贝后的 Schema 修复:

问题 修复方式
{"type": "object"}properties 补全 "properties": {}
"type": ["string", "null"] 数组形式 折叠为单字符串 + nullable: true
顶层 anyOf/oneOf/allOf/enum/not 剥离(Codex 后端严格拒绝此格式)
MCP 服务器输出的 additionalProperties: "object" 字符串值 替换为合法 dict
Pydantic optional 字段的 anyOf: [{type:X},{type:null}] 折叠为 X + nullable: true

故障恢复路径 --- strip_pattern_and_format()tools/schema_sanitizer.py:308

仅在 llama.cpp 因 \d/\w/\s 等正则转义类报 HTTP 400 时按需触发(agent/conversation_loop.py:2358):

python 复制代码
if classified.reason == FailoverReason.llama_cpp_grammar_pattern:
    _, _stripped = strip_pattern_and_format(agent.tools)
    continue  # 删除 pattern/format 字段后自动重试

云端 Provider 保留 pattern/format 作为语义提示;本地 llama.cpp 报错时才删,最小化修改。

5.6 策略五:技能两级加载 + 三层缓存 + 条件过滤

两级加载 :System Prompt 只注入每条 ≤60 字的紧凑索引,完整 SKILL.md 由 LLM 主动调用 skill_view(name) 按需拉取。

三层缓存agent/prompt_builder.py:828):

层级 实现 cache key 失效条件
L1 内存 LRU _SKILLS_PROMPT_CACHE OrderedDict(最多 8 条) (skills_dir, external_dirs, available_tools, available_toolsets, platform_hint, disabled) key 不命中时淘汰最旧
L2 磁盘快照 ~/.hermes/.skills_prompt_snapshot.json mtime_ns + size 逐文件校验 任意 SKILL.mdDESCRIPTION.md 变更
L3 冷路径 全量 iter_skill_index_files() 扫描 --- 冷路径结束后写入 L2 快照

快照格式:{"version": 1, "manifest": {path: [mtime_ns, size]}, "skills": [...], "category_descriptions": {...}}。版本号 _SKILLS_SNAPSHOT_VERSION = 1,格式变更时快照全量失效。

条件过滤 _skill_should_show()agent/prompt_builder.py:952)------任一条件满足即隐藏:

python 复制代码
requires_tools     → 技能依赖某工具,但该工具未加载 → 隐藏
requires_toolsets  → 技能依赖某工具集,但未启用 → 隐藏
fallback_for_tools → 技能是某工具的降级替代,该工具已可用 → 隐藏
fallback_for_toolsets → 同上,工具集粒度
platforms          → 当前 OS 不在支持列表 → 隐藏

外部技能目录skills.external_dirs 配置支持额外目录,本地技能名冲突时优先(seen_skill_names 去重)。外部目录无快照缓存(只读、通常较小)。

5.7 策略六:工具结果三层大小控制(tools/tool_result_storage.py

当工具返回超大结果时,三层防御防止 context 爆炸:

第一层:工具自身 pre-truncation

各工具在返回前自行裁剪(如 search_files 限制行数),最轻量,工具作者控制。

第二层:maybe_persist_tool_result()(per-result,tool_result_storage.py:122

python 复制代码
# 阈值优先级:PINNED > tool_overrides > registry.max_result_size > default(100K chars)
effective_threshold = config.resolve_threshold(tool_name)

if len(content) > effective_threshold:
    # 通过 stdin pipe 写入 sandbox /tmp/hermes-results/{tool_use_id}.txt
    # (避免 MAX_ARG_STRLEN 128KB argv 限制,支持本地/Docker/SSH/Modal 后端)
    _write_to_sandbox(content, remote_path, env)
    # 返回 <persisted-output> 替换块
    return _build_persisted_message(preview=1500chars, file_path=remote_path)

LLM 收到的替换块:

复制代码
<persisted-output>
This tool result was too large (2,847,392 characters, 2.7 MB).
Full output saved to: /tmp/hermes-results/abc123.txt
Use the read_file tool with offset and limit to access specific sections.

Preview (first 1500 chars):
...
</persisted-output>

read_file 的阈值被 pin 为 ∞PINNED_THRESHOLDS),防止读取结果再次触发持久化造成无限循环。

第三层:enforce_turn_budget()(per-turn 聚合,tool_result_storage.py:186

同一轮次所有工具结果之和超过 DEFAULT_TURN_BUDGET_CHARS(200K chars)时,将最大的未持久化结果依次溢出到 sandbox,直到总量降至预算以下:

python 复制代码
if total_size > config.turn_budget:   # 默认 200K chars
    candidates.sort(key=size, reverse=True)
    for idx, size in candidates:
        # threshold=0 强制溢出,无论单个结果多小
        tool_messages[idx]["content"] = maybe_persist_tool_result(..., threshold=0)
        if total_size <= config.turn_budget: break

5.8 策略七:对话历史超限时分级压缩

ContextCompressoragent/context_compressor.py)在每次 LLM API 调用前检查 token 数,触发阈值为 context_length × threshold_percent(默认 50%)。

防抖机制_ineffective_compression_count >= 2(连续 2 次压缩效果 < 10%)时停止,避免无效循环,提示用户 /new/compress <topic>

两阶段压缩

复制代码
阶段一:廉价预处理,无 LLM 调用(_prune_old_tool_results())
  保护区:最新 protect_last_n(默认 20)轮 + tail_token_budget token 预算
  ├─ 保护区外旧工具结果 → 1 行摘要
  │     "terminal: ran `npm test` → exit 0, 47 lines"
  │     "read_file: read config.py from line 1 (3,400 chars)"
  ├─ 同一文件多次读取 → 去重,保留最新完整内容
  └─ assistant 消息中的大型 tool_call arguments → 截断

阶段二:LLM 摘要压缩(compress())
  ├─ protect_first_n(默认 3)轮头部不动(保留背景上下文)
  ├─ protect_last_n(默认 20)轮尾部不动(保留近期完整记忆)
  ├─ 中间窗口 → 辅助 LLM 生成摘要
  │     max_summary_tokens = min(context_length × 5%, _SUMMARY_TOKENS_CEILING)
  │     summary_target_ratio = 20%(目标保留当前体积的 20%)
  ├─ 新摘要与 _previous_summary 迭代合并(多次压缩不丢失历史)
  └─ 失败时(abort_on_summary_failure=False 默认行为):
       返回原始消息,设置 _last_compress_aborted=True,让调用方冻结会话

压缩后调用 sanitize_api_messages() 清理孤儿 tool_call/tool_result 对(压缩边界可能切断配对关系),保证 API 不收到 mismatched IDs。

5.9 策略八:上下文文件大小限制 + Prompt Injection 防御

AGENTS.mdSOUL.md.cursorrules.hermes.md 等注入前经过两道关卡:

Prompt Injection 扫描agent/prompt_builder.py:44):

python 复制代码
def _scan_context_content(content: str, filename: str) -> str:
    findings = _scan_for_threats(content, scope="context")
    if findings:
        return f"[BLOCKED: {filename} 包含疑似 prompt injection. 内容未加载.]"
    return content

检测 classic injection、promptware/C2 控制模式、角色劫持等。被污染的文件整段 BLOCK,不进入 system prompt。

硬截断CONTEXT_FILE_MAX_CHARS = 20_000):

python 复制代码
head_chars = int(max_chars * 0.7)   # 14,000 chars(保留文件头部,通常是最重要的部分)
tail_chars  = int(max_chars * 0.2)  # 4,000 chars(保留文件尾部,通常是最新配置)
marker = "[...truncated: kept head+tail. Use file tools to read full file.]"

头 70% + 尾 20%,中间插入截断标记。即使 AGENTS.md 写得很长也不会无限膨胀 system prompt。


六、现存的真实限制

场景 现状 建议
工具数 > 200 Schema token 总量可能超过小 context 模型上限,无自动裁剪工具数 按场景手动配置 toolset,只启用当前任务相关工具集
技能数 > 1000 索引 token 增大,LLM 凭名称/描述选技能精度下降;无语义向量检索 合理分 category,保持描述精准;必要时拆分为多 Agent
工具单次结果 > 100K chars 落盘后 LLM 需额外调一次 read_file 才能看到完整内容 工具层自行 pre-truncate;或提示 LLM 使用 offset/limit 分段读取
长会话压缩反复失效 连续 2 次压缩效率 < 10% 时停止 任务边界清晰时主动 /new 开启新会话
check_fn 状态变更延迟 TTL 30s,hermes tools enable 后最多 30s 才生效 手动调用 invalidate_check_fn_cache() 立即失效

七、完整初始化到执行流程(文字版)

复制代码
进程启动
  │
  ├─ import model_tools(模块顶层自动执行)
  │    ├─ discover_builtin_tools():AST 扫描 tools/*.py → importlib 动态导入 → register()
  │    └─ discover_plugins():扫描 ~/.hermes/plugins/ → register()
  │
  ▼
AIAgent.__init__(enabled_toolsets, disabled_toolsets, model, ...)
  │
  ├─ 工具加载
  │    └─ get_tool_definitions(enabled_toolsets, disabled_toolsets)
  │         ├─ memoize 命中 → 直接返回浅拷贝
  │         └─ _compute_tool_definitions()
  │              ├─ resolve_toolset() → tools_to_include(set 运算)
  │              ├─ registry.get_definitions()
  │              │    └─ _check_fn_cached() TTL 30s → 过滤不可用工具
  │              ├─ 动态 Schema 重建(execute_code / discord)
  │              ├─ 跨工具引用清理
  │              └─ sanitize_tool_schemas()(深拷贝 + 修复 JSON Schema)
  ├─ agent.tools = [...]
  ├─ agent.valid_tool_names = {name, ...}
  │
  ├─ 系统提示构建
  │    └─ build_system_prompt_parts()
  │         ├─ stable tier:
  │         │    ├─ SOUL.md / DEFAULT_AGENT_IDENTITY
  │         │    ├─ 工具感知 guidance(memory/skill_manage/kanban)
  │         │    ├─ build_skills_system_prompt()
  │         │    │    ├─ L1 内存 LRU → L2 磁盘快照 → L3 冷扫描
  │         │    │    └─ _skill_should_show() 过滤 → 紧凑索引
  │         │    └─ 环境/平台 hints
  │         ├─ context tier:AGENTS.md 等(injection 扫描 + 20K 截断)
  │         └─ volatile tier:记忆快照 + 日期/Session/Model 行
  ├─ agent._cached_system_prompt = 三段拼接(会话内不变)
  │
  ▼
run_conversation(user_message)
  │
  ├─ 系统提示恢复(从 SessionDB 加载或重建)
  ├─ Preflight 压缩(estimate_tokens ≥ threshold → compress())
  ├─ pre_llm_call 插件钩子(可追加 user context)
  │
  └─ 主循环(最多 max_iterations 次)
       │
       ├─ ContextCompressor.should_compress()
       │    ├─ < threshold → 跳过
       │    └─ ≥ threshold → _prune_old_tool_results()(廉价)→ compress()(LLM 摘要)
       │
       ├─ sanitize_api_messages()(清理孤儿 tool_call/result 对)
       │
       ├─ LLM API 调用(携带 agent.tools + active_system_prompt)
       │
       ├─ 有 tool_calls → 对每个 tool call:
       │    ├─ 名称验证 → repair_tool_call()(5 种修复策略 + difflib 模糊,最多重试 3 次)
       │    ├─ JSON 解析 + coerce_tool_args()(类型强制)
       │    ├─ handle_function_call()
       │    │    ├─ pre_tool_call 钩子(可 block)
       │    │    ├─ ACP 编辑审批(write_file/patch)
       │    │    ├─ registry.dispatch():O(1) 字典查找 → handler() 执行
       │    │    ├─ maybe_persist_tool_result()(超 100K chars → 落盘返回预览)
       │    │    └─ post_tool_call / transform_tool_result 钩子
       │    └─ enforce_turn_budget()(本轮所有结果 > 200K → 溢出最大的)
       │
       ├─ 无 tool_calls → 返回最终回复
       │
       └─ 收尾:_persist_session() → SQLite 持久化对话历史

文档生成时间:2026-05-31 | 源码版本:hermes-agent-main

相关推荐
冬奇Lab1 小时前
Agent 系列(9):多 Agent 架构设计模式——Supervisor 与 Pipeline
人工智能·源码·agent
冬奇Lab2 小时前
每日一个开源项目(第118篇):SkillOpt - 像训练神经网络一样优化 LLM Agent 的技能
人工智能·开源·agent
薛定谔的猫-菜鸟程序员3 小时前
2小时智能体开发一个智能体?我用CodeArts Agent 和 AtomCode 开发了一个适老化智能体。
人工智能·python·agent
HIT_Weston3 小时前
101、【Agent】【OpenCode】task 工具提示词(Usage Notes)
人工智能·agent·opencode
后端小肥肠4 小时前
效率狂飙9000%!Codex + HyperFrames 让一篇文章 5 分钟变视频
人工智能·aigc·agent
程序员小假4 小时前
我们来说说 Agent 的推理模式有哪些?说说 ReAct 模式,它和 CoT、ToT 等模式有什么区别?
agent
蛤密呱5 小时前
LangGraph: 状态图与状态转换
agent
LienJack5 小时前
我做了一个 AI Agent 学习站
github·agent
DigitalOcean6 小时前
AI推理成本砍半:DigitalOcean 批量推理服务正式上线
aigc·agent