AgentHarness 课程
Hermes 专题/13

第十三篇:性能调优与生产实践

瓶颈分析、Token优化、缓存策略、生产实践

1. 性能瓶颈分析

1.1 Agent 响应慢的常见原因

Hermes Agent 的响应延迟来自多个环节。理解每个环节的特征,才能精准定位瓶颈:

LLM API 延迟:这是最常见的瓶颈。一次完整的 Agent 循环包含至少一次 LLM 调用,而复杂任务可能涉及多轮工具调用 + 多次 LLM 推理。主要影响因素包括:

  • 模型本身的推理速度(如 Claude Sonnet 快于 Opus)
  • API 提供商的网络延迟(OpenRouter 中转 vs 直连 Anthropic)
  • Prompt 长度(上下文越大,首 Token 延迟越高)
  • 提供商的负载情况(高峰期排队)

工具执行超时:终端命令是最典型的耗时工具。一次 npm installdocker build 可能耗时数分钟,直接拉长整个 Agent 循环。Web 爬取、文件搜索等 I/O 密集型工具也可能成为瓶颈。

上下文过大:当对话历史膨胀到接近模型的 context window 上限时,每次 API 调用都需要传输大量 token,导致延迟线性增长。此时还会触发上下文压缩,而压缩本身也需要一次额外的 LLM 调用。

1.2 如何定位瓶颈

Hermes Agent 提供了完善的日志体系来辅助定位问题。日志文件位于 ~/.hermes/logs/ 目录下:

日志文件内容用途
agent.logINFO 及以上级别的所有活动日志追踪完整的 Agent 执行流程
errors.logWARNING 及以上级别的错误日志快速排查异常和告警
gateway.logGateway 模式专属日志平台接入层问题排查

日志时间戳分析法:在 agent.log 中搜索关键事件的时间戳,计算各阶段耗时:

# 典型的日志时间线
2026-04-18 10:00:01 INFO  API call started (model=claude-sonnet, prompt_tokens=12000)
2026-04-18 10:00:03 INFO  First stream chunk received   <-- TTFB 约 2s
2026-04-18 10:00:05 INFO  Tool call: execute_command(git status)
2026-04-18 10:00:06 INFO  Tool result received (duration=0.8s)
2026-04-18 10:00:06 INFO  API call started (prompt_tokens=12800)
2026-04-18 10:00:08 INFO  Final response delivered

API 响应时间监控:通过 verbose 模式或检查 HTTP 响应头中的 x-ratelimit-* 字段,可以获取速率限制的剩余配额。Hermes 内部的 _capture_rate_limits() 方法会自动解析这些头部并更新 rate_limit_state,通过 /usage 命令可查看。

1.3 关键性能指标

生产环境中需要关注的核心指标:

  • TTFB(Time to First Byte / 首 Token 时间):从发起 API 请求到收到第一个 Stream chunk 的耗时。正常范围应在 1-5 秒,超过 10 秒说明存在严重延迟。
  • 工具执行时间:每个工具调用的耗时。终端命令平均 1-30 秒,超过 60 秒应考虑 background 模式。日志中会记录每个工具的 duration
  • 上下文压缩触发频率:如果每次对话都频繁触发压缩("Context compression triggered" 日志),说明上下文管理策略需要调整。
  • 迭代预算消耗率budget_used / budget_max 比率。如果经常耗尽 90 次迭代上限,说明任务可能过于复杂或存在无效循环。

2. LLM 调用优化

2.1 Prompt Caching

Hermes Agent 对 Anthropic Claude 模型自动启用 Prompt Caching 功能,采用 system_and_3 策略。该策略在 prompt_caching.py 中实现,核心逻辑如下:

工作原理:在消息列表中放置最多 4 个 cache_control 断点:

  1. System prompt(跨所有轮次稳定不变) 2-4. 最后 3 条非 system 消息(滚动窗口)
# prompt_caching.py 核心逻辑
def apply_anthropic_cache_control(api_messages, cache_ttl="5m"):
    marker = {"type": "ephemeral"}
    # 断点 1:System prompt
    if messages[0].get("role") == "system":
        _apply_cache_marker(messages[0], marker)
    # 断点 2-4:最后 3 条非 system 消息
    non_sys = [i for i in range(len(messages)) if messages[i].get("role") != "system"]
    for idx in non_sys[-remaining:]:
        _apply_cache_marker(messages[idx], marker)

自动启用条件(在 run_agent.py 第 747 行判断):

  • 通过 OpenRouter 使用 Claude 模型
  • 或使用原生 Anthropic Messages API(api_mode == "anthropic_messages"

配置方法:默认 TTL 为 5 分钟(写入成本为正常的 1.25 倍)。可通过配置设置为 1 小时(写入成本为正常的 2 倍)以获得更长的缓存有效期。日志中会显示 Prompt caching: ENABLED (source, 5m TTL)

效果评估:Prompt Caching 可将多轮对话的输入 token 成本降低约 75%。在 CLI 启动信息中可以看到是否成功启用,建议在 Anthropic 控制台检查 cache hit rate。

2.2 智能模型路由

smart_model_routing.py 实现了基于消息复杂度的自动模型路由,将简单的闲聊类消息路由到更便宜的模型,将复杂任务保留给主模型。

工作原理choose_cheap_model_route() 函数对用户消息进行多维检测:

长度检测

  • max_simple_chars(默认 160):消息字符数上限
  • max_simple_words(默认 28):消息单词数上限
  • 超过 1 个换行符则视为复杂

代码/URL 检测

  • 包含反引号(````)→ 复杂
  • 包含 URL(http:// / www.)→ 复杂

复杂度关键词检测_COMPLEX_KEYWORDS 集合,共 26 个关键词):

_COMPLEX_KEYWORDS = {
    "debug", "debugging", "implement", "implementation",
    "refactor", "patch", "traceback", "stacktrace",
    "exception", "error", "analyze", "analysis",
    "investigate", "architecture", "design", "compare",
    "benchmark", "optimize", "review", "terminal",
    "shell", "tool", "pytest", "test", "plan", ...
}

只要消息中任意单词命中上述集合,就直接使用主模型。

配置示例config.yaml):

smart_model_routing:
  enabled: true
  max_simple_chars: 160
  max_simple_words: 28
  cheap_model:
    provider: openrouter
    model: google/gemini-3-flash-preview
    api_key_env: OPENROUTER_API_KEY

调优建议:如果发现简单问题使用了昂贵的模型,可以适当放宽 max_simple_charsmax_simple_words。如果发现复杂任务被误路由到廉价模型,检查是否有关键词遗漏。

2.3 Fallback 链

Hermes Agent 支持配置 Fallback 模型链,当主模型不可用时自动切换到备用模型:

配置方式:在 config.yaml 中通过 fallback_providers 列表指定:

fallback_providers:
  - provider: openrouter
    model: anthropic/claude-sonnet-4-20250514
  - provider: openrouter
    model: google/gemini-3-flash-preview

避免 Fallback 浪费时间的策略

  • Fallback 链不要配置太多层级(建议不超过 2-3 个)
  • 每个 Fallback 模型的能力应逐级递减,避免在能力不足的模型上浪费迭代
  • 当遇到 rate limit 错误时,系统会自动尝试 Fallback;对于 context length 错误,会先尝试降级再 Fallback
  • delegation 配置可以让子 Agent 使用不同的模型,避免所有任务都争抢主模型

2.4 max_tokens 调优

max_tokens 参数控制模型单次响应的最大输出 token 数。合理配置可以避免浪费和截断:

任务类型推荐 max_tokens说明
闲聊 / 简单问答500-1000短回复为主
代码生成2000-4000函数级别的代码
文件编辑 + 工具调用4000-8000包含工具调用参数
复杂分析 / 长文写作8000-16000需要长文本输出

如果不设置(默认 None),则使用模型默认值。注意上下文压缩后会自动缩减 max_output_tokens 以适配剩余空间。


3. 工具执行优化

3.1 并行工具执行

当 LLM 在一个响应中返回多个工具调用时,Hermes Agent 会自动判断是否可以并行执行。这一机制在 run_agent.py 中实现。

并行安全工具列表_PARALLEL_SAFE_TOOLS,共 11 个):

_PARALLEL_SAFE_TOOLS = frozenset({
    "ha_get_state", "ha_list_entities", "ha_list_services",
    "read_file", "search_files", "session_search",
    "skill_view", "skills_list", "vision_analyze",
    "web_extract", "web_search",
})

路径作用域工具_PATH_SCOPED_TOOLS):read_filewrite_filepatch 这类工具在操作不同文件路径时也可以并行。

永不并行的工具_NEVER_PARALLEL_TOOLS):clarify(交互式确认工具)。

并行判断逻辑_should_parallelize_tool_batch()):

  1. 只有 1 个工具调用 → 不并行
  2. 包含 _NEVER_PARALLEL_TOOLS 中的工具 → 全部串行
  3. 路径作用域工具之间路径重叠(如同时写同一目录)→ 串行
  4. 其他不在 _PARALLEL_SAFE_TOOLS 中的工具 → 串行
  5. 所有工具都在安全列表中且路径不重叠 → 并行

并发控制:最大并发线程数为 _MAX_TOOL_WORKERS = 8。使用 ThreadPoolExecutor 实现:

max_workers = min(num_tools, _MAX_TOOL_WORKERS)
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
    futures = [executor.submit(_run_tool, i, tc, name, args) for i, (tc, name, args) in enumerate(parsed_calls)]
    concurrent.futures.wait(futures)

如何让自定义工具可并行:如果你的自定义工具是只读的且无共享可变状态,可以将其名称加入 _PARALLEL_SAFE_TOOLS。如果是路径作用域的,加入 _PATH_SCOPED_TOOLS 并实现路径提取逻辑。

3.2 终端超时配置

终端工具(terminal_tool.py)是耗时最长的工具之一,合理配置超时至关重要:

  • 前台超时硬上限FOREGROUND_MAX_TIMEOUT = 600 秒(10 分钟),可通过环境变量 TERMINAL_MAX_FOREGROUND_TIMEOUT 覆盖
  • 默认超时TERMINAL_TIMEOUT 环境变量,默认 180 秒
  • 后台模式:超过 600 秒的命令会自动建议使用 background=true

配置建议

# 在 ~/.hermes/.env 中设置
TERMINAL_TIMEOUT=180                  # 默认前台超时
TERMINAL_MAX_FOREGROUND_TIMEOUT=600   # 前台超时上限

对于长时间运行的任务(编译、部署、训练),应在工具参数中指定 background=true + notify_on_complete=true,这样 Agent 可以继续执行其他操作。

3.3 结果截断策略

大型工具输出会显著增加上下文体积。Hermes Agent 通过多层截断机制控制输出大小:

  • 文件读取上限file_read_max_chars = 100_000(约 25K-35K tokens),超过时建议使用 offset/limit 分段读取
  • 上下文文件上限CONTEXT_FILE_MAX_CHARS = 20_000,超出时按 70% 头部 + 20% 尾部截断
  • 压缩时的工具输出截断:每条消息最多保留 _CONTENT_MAX = 6000 字符(_CONTENT_HEAD = 4000 + _CONTENT_TAIL = 1500

4. 上下文管理优化

4.1 压缩配置详解

Hermes Agent 的上下文压缩由 ContextCompressorcontext_compressor.py)实现,采用多阶段压缩算法:

第一阶段:工具输出剪枝(无 LLM 调用,零成本)

  • 将旧的工具结果替换为短占位符 [Old tool output cleared to save context space]
  • 保留最近 protect_last_n(默认 20)条消息和 tail_token_budget 范围内的工具输出
  • 只有超过 200 字符的内容才会被剪枝

第二阶段:确定压缩边界

  • 保护头部:前 protect_first_n(默认 3)条消息(system prompt + 首次交换)
  • 保护尾部:基于 token 预算而非固定消息数,确保最近的高价值上下文不被压缩

第三阶段:LLM 摘要生成

  • 使用结构化模板:Goal / Progress / Decisions / Resolved Questions / Pending Asks / Relevant Files / Remaining Work
  • 支持迭代更新:后续压缩不是从零开始,而是在上一次摘要基础上增量更新
  • 摘要 token 预算按比例缩放:min(被压缩内容的 20%, max_summary_tokens)

关键配置参数config.yaml):

compression:
  enabled: true
  threshold: 0.50           # 上下文使用率达到 50% 时触发压缩
  target_ratio: 0.20        # 压缩后保留为目标大小的 20%
  protect_last_n: 20        # 最少保护最近 20 条消息不被压缩
  summary_model: ""         # 空 = 使用主模型;建议设为廉价快速模型
  summary_provider: "auto"  # 摘要生成使用的 provider
  timeout: 120              # 摘要生成的超时时间(秒)

调优建议

  • threshold: 0.50 是平衡点:过早压缩会丢失信息,过晚会影响 API 性能
  • summary_model 配置一个廉价模型(如 gemini-3-flash-preview)可以显著降低压缩成本
  • 如果对话经常丢失重要信息,可提高 target_ratio 到 0.30
  • 压缩失败后会有 600 秒冷却期(_SUMMARY_FAILURE_COOLDOWN_SECONDS),避免连续失败浪费资源

4.2 会话重置策略

长时间运行的会话会积累大量上下文,定期重置可以保持性能。在 config.yaml 中通过 session_reset 配置:

session_reset:
  mode: "both"          # both | idle | daily | none
  idle_minutes: 1440    # 空闲 24 小时后重置
  at_hour: 4            # 每天凌晨 4 点重置
  • both:同时启用空闲超时和定时重置
  • idle:仅在空闲超过指定时间后重置
  • daily:仅在每天指定时间重置
  • none:禁用自动重置

4.3 工具结果剪枝机制

_prune_old_tool_results() 是压缩算法中最重要的零成本优化。它在 LLM 摘要之前执行,将旧的工具输出替换为占位符:

_PRUNED_TOOL_PLACEHOLDER = "[Old tool output cleared to save context space]"

保护逻辑

  • 使用 token 预算(tail_token_budget)从末尾向前累积
  • 至少保护最近 protect_last_n 条消息
  • 只有超过 200 字符的工具结果才会被剪枝
  • 低于 200 字符的短结果保留原样,因为替换的占位符本身就接近这个长度

4.4 CONTEXT_FILE_MAX_CHARS

系统提示中注入的上下文文件(通过 context_files 配置)有大小上限控制。CONTEXT_FILE_MAX_CHARS = 20_000 确保 Agent 的系统提示不会被过大的文件撑爆。超出时采用截断策略:保留前 70% 和后 20% 的内容,中间插入截断标记。


5. 内存与资源管理

5.1 Python 内存消耗

Hermes Agent 的内存消耗主要来自以下几个方面:

大模型响应缓存:每次 API 调用都会在内存中累积消息历史。长对话可能产生数百条消息,每条包含大量工具输出。压缩虽然减少了发送给 API 的内容,但原始历史仍可能在内存中短暂保留。

多子 Agent 并发:通过 delegate_task 创建的子 Agent 各自维护独立的对话历史和上下文引擎。默认最大并发子 Agent 数为 _DEFAULT_MAX_CONCURRENT_CHILDREN = 3,可通过 delegation.max_concurrent_children 配置:

delegation:
  max_concurrent_children: 3    # 默认值,最大可设为 CPU 核心数
  max_iterations: 50            # 每个子 Agent 的迭代上限

优化建议

  • 监控 RSS 内存(ps aux | grep hermes),超过 2GB 应考虑减少并发
  • 子 Agent 使用更小的模型(通过 delegation.model 配置),减少内存占用
  • 长时间运行的 Gateway 服务建议配置定时重启(如 systemd 的 RuntimeMaxSec

5.2 磁盘空间管理

会话日志:每次会话生成一个 JSON 文件(session_YYYYMMDD_HHMMSS_XXXXXX.json),位于 ~/.hermes/logs/ 目录。长期运行会积累大量日志。

检查点快照:启用 checkpoints_enabled 后,Agent 会在关键节点保存状态快照,方便恢复。

日志轮转配置:Hermes 使用 RotatingFileHandler 管理日志文件。日志文件达到一定大小会自动轮转。建议定期清理旧日志:

# 清理 30 天前的会话日志
find ~/.hermes/logs/ -name "session_*.json" -mtime +30 -delete

5.3 并发控制

Hermes Agent 的并发控制涉及多个层面:

工具并发ThreadPoolExecutor + _MAX_TOOL_WORKERS = 8 控制工具执行并发度。

子 Agent 并发_get_max_concurrent_children() 从配置中读取上限(默认 3),超过的委派任务会被截断并提示用户调整。

Credential Pool:多 API Key 轮换可以在 rate limit 范围内提高吞吐量。credential_pool 参数支持配置多个 API Key 进行负载均衡。


6. 监控与可观测性

6.1 关键日志位置

日志文件路径内容
agent.log~/.hermes/logs/agent.log完整的 Agent 活动日志(INFO+)
errors.log~/.hermes/logs/errors.log错误和告警(WARNING+)
gateway.log~/.hermes/logs/gateway.logGateway 平台接入层日志
session_*.json~/.hermes/logs/session_*.json每次会话的详细 JSON 记录

日志中的关键事件

  • "API call started" — LLM 调用开始,包含模型名和 token 数
  • "Context compression triggered" — 压缩触发,包含 token 使用详情
  • "Compressed: X -> Y messages" — 压缩结果摘要
  • "Tool X returned error" — 工具执行错误
  • "Smart route → model" — 智能路由生效

6.2 上下文压力通知

Hermes Agent 实现了分层上下文压力通知机制(_emit_context_pressure()):

  • 85% 阈值:首次警告,提示用户上下文即将耗尽
  • 95% 阈值:严重警告,下一次 API 调用大概率触发压缩

这些通知仅面向用户(CLI 输出或 Gateway 状态回调),不会注入到 LLM 的消息中,避免干扰模型推理。

6.3 API 用量监控

成本估算usage_pricing.py 中的 estimate_usage_cost() 函数根据模型和 token 用量估算美元成本。支持多种计费模式:

  • 按 token 计费的标准模型
  • Subscription 包含的模型(标记为 "included"
  • 未知定价模型(返回 "unknown" 状态)

Rate Limit 跟踪_capture_rate_limits() 方法从 HTTP 响应头中解析 x-ratelimit-* 字段,通过 /usage 命令查看剩余配额。

显示成本:在 config.yaml 中启用 display.show_cost: true 可以在状态栏中实时显示成本。

6.4 自定义监控

通过 Hook 机制可以收集自定义指标:

  • on_tool_call / on_tool_result:收集工具执行的耗时和成功率
  • on_api_call:收集 LLM 调用延迟和 token 用量
  • on_session_end:收集会话级别的汇总指标

7. 生产环境配置模板

7.1 小型团队(1-5 人)推荐配置

# config.yaml — 小型团队配置
model: "anthropic/claude-sonnet-4-20250514"
provider: "openrouter"

compression:
  enabled: true
  threshold: 0.50
  target_ratio: 0.20
  protect_last_n: 20
  summary_model: "google/gemini-3-flash-preview"
  summary_provider: "openrouter"
  timeout: 120

smart_model_routing:
  enabled: true
  max_simple_chars: 160
  max_simple_words: 28
  cheap_model:
    provider: "openrouter"
    model: "google/gemini-3-flash-preview"

session_reset:
  mode: "idle"
  idle_minutes: 1440

delegation:
  max_concurrent_children: 3
  max_iterations: 50
  model: "google/gemini-3-flash-preview"
  provider: "openrouter"

display:
  tool_progress: "all"
  show_cost: true

核心思路:主模型处理复杂任务,廉价模型处理简单消息和子任务,最大化性价比。

7.2 中型团队(5-20 人)推荐配置

# config.yaml — 中型团队配置
model: "anthropic/claude-sonnet-4-20250514"
provider: "openrouter"

fallback_providers:
  - provider: "openrouter"
    model: "anthropic/claude-haiku-4-20250514"

compression:
  enabled: true
  threshold: 0.45           # 更早触发压缩,避免多人共享时上下文溢出
  target_ratio: 0.20
  protect_last_n: 25        # 保护更多近期上下文
  summary_model: "google/gemini-3-flash-preview"
  summary_provider: "openrouter"
  timeout: 90               # 更短的超时,快速失败

smart_model_routing:
  enabled: true
  max_simple_chars: 200     # 稍宽松,更多消息可走廉价模型
  max_simple_words: 35
  cheap_model:
    provider: "openrouter"
    model: "google/gemini-3-flash-preview"

session_reset:
  mode: "both"
  idle_minutes: 720         # 12 小时空闲重置
  at_hour: 3                # 每天凌晨 3 点定时重置

delegation:
  max_concurrent_children: 5
  max_iterations: 50
  model: "google/gemini-3-flash-preview"
  provider: "openrouter"

display:
  tool_progress: "new"      # 减少噪音
  show_cost: true

agent:
  max_turns: 120            # 更高迭代上限,支持更复杂的任务

核心思路:增加 Fallback 容错,提前压缩防止多人场景下上下文膨胀,提高并发子 Agent 数以处理更多并行任务。

7.3 关键配置参数速查表

参数默认值位置说明
compression.threshold0.50config.yaml上下文使用率触发压缩的阈值
compression.target_ratio0.20config.yaml压缩后保留为目标大小的比例
compression.protect_last_n20config.yaml最少保护的最近消息数
compression.timeout120config.yaml摘要生成超时(秒)
smart_model_routing.enabledfalseconfig.yaml是否启用智能路由
smart_model_routing.max_simple_chars160config.yaml简单消息的字符上限
smart_model_routing.max_simple_words28config.yaml简单消息的单词上限
session_reset.mode"both"config.yaml重置模式:both/idle/daily/none
session_reset.idle_minutes1440config.yaml空闲重置的分钟数
session_reset.at_hour4config.yaml定时重置的小时
delegation.max_concurrent_children3config.yaml最大并发子 Agent 数
delegation.max_iterations50config.yaml每个子 Agent 的迭代上限
agent.max_turns90环境变量主 Agent 迭代上限
FOREGROUND_MAX_TIMEOUT600环境变量前台终端超时上限(秒)
TERMINAL_TIMEOUT180环境变量默认终端超时(秒)
CONTEXT_FILE_MAX_CHARS20,000代码常量上下文文件大小上限
file_read_max_chars100,000config.yaml单次文件读取上限
_MAX_TOOL_WORKERS8代码常量工具执行最大并发线程数
_cache_ttl"5m"运行时Prompt Cache 的 TTL

思考题

  1. 压缩阈值权衡:假设你负责一个频繁使用 Hermes Agent 进行大型代码重构的团队。将 compression.threshold 从 0.50 提高到 0.75 会带来什么好处和风险?在什么场景下这个调整是合理的?

  2. 智能路由的边界smart_model_routing 通过关键词列表判断复杂度。如果一个用户发送了 "帮我用 shell 脚本实现一个简单的 hello world",这条消息会被路由到哪个模型?为什么?你如何调整配置来优化这种边界情况?

  3. 并行工具执行安全性_PARALLEL_SAFE_TOOLS 中包含了 web_searchweb_extract。假设这两个工具共享一个内部的全局 HTTP 连接池,将其标记为并行安全是否还合适?请从线程安全和资源竞争的角度分析。

  4. Fallback 链设计:你的主模型是 Claude Opus,Fallback 是 Gemini Flash。在一次复杂的代码调试任务中,主模型触发了 rate limit,系统自动切换到 Fallback。这可能导致什么问题?你如何设计 Fallback 链来缓解这些风险?

  5. 生产环境成本优化:一个 10 人团队每天产生约 500 次 Agent 对话,当前使用 Claude Sonnet 作为唯一模型,月度 API 成本约 $2000。请基于本章所学,设计一个综合方案(利用智能路由、压缩摘要模型、子 Agent 委派、Prompt Caching 等)将成本降低 50% 以上,同时保持核心任务质量。