AIAgent 内核:同步编排器为何是系统心脏
本章核心源码:
run_agent.py(全文,重点 416-1140 行初始化、168-210 行 IterationBudget)
定位:本章完整拆解
AIAgent类——全书最重要的对象。从初始化决策链到回调体系、IterationBudget、fallback provider,理解这个 9431 行的同步编排器如何成为系统心脏。 前置依赖:第 3 章(请求旅程)。适用场景:想理解 AIAgent 的内部设计决策,或准备扩展/修改 agent 行为。
为什么 AIAgent 值得一整章
第 3 章追踪了一次请求穿过系统的旅程,AIAgent.run_conversation() 是旅程的核心。但第 3 章只展示了"消息怎么流动",没有回答"这个编排器为什么这么设计"。
AIAgent 类定义在 run_agent.py:416,它的 __init__ 方法有 45+ 个参数(run_agent.py:433-486),初始化代码超过 700 行(到第 1140 行)。这不是膨胀——每个参数和每段初始化代码都对应一个具体的工程问题。本章拆解这些问题和 Hermes 的解法。
run_agent.py 内部结构全景
在深入细节之前,先看这 9431 行代码的内部结构。下图按代码行号从上到下展示了 run_agent.py 的六大区域和它们之间的调用关系:
graph TB
subgraph "辅助类 (1-415 行)"
SW["_SafeWriter<br/>broken pipe 防护<br/>111-131"]
IB["IterationBudget<br/>线程安全迭代计数<br/>168-210"]
PAR["_should_parallelize_tool_batch<br/>并行安全判断<br/>265-330"]
end
subgraph "AIAgent.__init__ (416-1140 行)"
INIT1["阶段1-3: 配置 + API模式 + 回调"]
INIT2["阶段4: LLM 客户端构造<br/>Anthropic / OpenAI / Codex"]
INIT3["阶段5-6: 工具加载 + 记忆初始化"]
INIT4["阶段7: 压缩器初始化"]
INIT1 --> INIT2 --> INIT3 --> INIT4
end
subgraph "内部方法 (1140-6800 行)"
BG["_spawn_background_review<br/>后台记忆/技能审查<br/>1718"]
PERSIST["_persist_session / _flush_messages<br/>Session 持久化<br/>1842-1957"]
TRAJ["_save_trajectory<br/>轨迹保存<br/>2123"]
PROMPT["_build_system_prompt<br/>系统提示词拼装<br/>2582"]
SANITIZE["_sanitize_api_messages<br/>消息清洗与修复<br/>2757"]
STREAM["_interruptible_streaming_api_call<br/>流式 API 调用 + 健康检测<br/>4282"]
API_KW["_build_api_kwargs<br/>API 请求构建<br/>5229"]
FALLBACK["_restore_primary_runtime<br/>Fallback 恢复<br/>4928"]
COMPRESS["_compress_context<br/>上下文压缩<br/>5835"]
FLUSH_MEM["flush_memories<br/>记忆刷盘<br/>5677"]
end
subgraph "工具执行 (5930-6590 行)"
EXEC["_execute_tool_calls<br/>入口:并行/串行判断<br/>5930"]
INVOKE["_invoke_tool<br/>四路分发<br/>5953"]
CONC["_execute_tool_calls_concurrent<br/>ThreadPoolExecutor<br/>6027"]
SEQ["_execute_tool_calls_sequential<br/>逐个执行 + 显示<br/>6251"]
EXEC --> PAR
EXEC --> CONC
EXEC --> SEQ
CONC --> INVOKE
SEQ --> INVOKE
end
subgraph "run_conversation 主循环 (6800-9431 行)"
RC_PREP["准备阶段<br/>SafeWriter + 清洗 + prompt<br/>+ 预飞行压缩 + hooks<br/>+ memory prefetch"]
RC_LOOP["主循环<br/>while budget > 0:<br/> 组装消息 → API 调用<br/> → 解析响应 → 工具执行"]
RC_EXIT["退出与收尾<br/>persist + sync + hooks<br/>+ background review"]
RC_PREP --> RC_LOOP --> RC_EXIT
end
INIT4 -.-> RC_PREP
RC_PREP --> PROMPT
RC_PREP --> COMPRESS
RC_LOOP --> STREAM
RC_LOOP --> EXEC
RC_LOOP --> SANITIZE
RC_EXIT --> PERSIST
RC_EXIT --> FLUSH_MEM
RC_EXIT --> BG
style RC_LOOP fill:#f96,stroke:#333,stroke-width:3px
style INIT2 fill:#9cf,stroke:#333,stroke-width:2px
style EXEC fill:#fc9,stroke:#333,stroke-width:2px
图中高亮:主循环(橙色,2400 行)是系统心脏;LLM 客户端构造(蓝色)决定了与哪个 provider 通信;工具执行(黄色)是能力层的入口。
从图中可以看出 run_agent.py 的结构不是"一个大方法",而是围绕主循环的辐射状架构——主循环调用内部方法,内部方法调用辅助类。理解了这张图,就知道每一段代码在 9431 行中的位置和角色。
AIAgent 初始化的七个阶段
AIAgent.__init__() 的 700 行初始化代码可以分为七个阶段,每个阶段解决一类问题:
graph LR
A["1. 基础配置<br/>模型、参数、标志"] --> B["2. API 模式<br/>检测与路由"]
B --> C["3. 回调注册<br/>11 个适配接口"]
C --> D["4. LLM 客户端<br/>构造与认证"]
D --> E["5. 工具加载<br/>过滤与验证"]
E --> F["6. 记忆初始化<br/>builtin + provider"]
F --> G["7. 压缩器<br/>上下文管理"]
阶段 1:基础配置(run_agent.py:527-553)
# run_agent.py:529-533
self.model = model
self.max_iterations = max_iterations
self.iteration_budget = iteration_budget or IterationBudget(max_iterations)
self.tool_delay = tool_delay
self.quiet_mode = quiet_mode
第一阶段存储基础参数。值得注意的是 iteration_budget:如果调用方提供了 budget(子代理场景),使用传入的实例;否则创建新的。这让父子代理可以使用独立的 budget(详见"IterationBudget"节)。
阶段 2:API 模式检测与路由(run_agent.py:559-590)
Hermes 支持三种 API 模式,检测逻辑如下:
# run_agent.py:559-581(简化)
if api_mode in {"chat_completions", "codex_responses", "anthropic_messages"}:
self.api_mode = api_mode # 显式指定
elif self.provider == "openai-codex":
self.api_mode = "codex_responses" # Codex 专用
elif self.provider == "anthropic" or "api.anthropic.com" in base_url:
self.api_mode = "anthropic_messages" # Anthropic 原生
elif base_url.endswith("/anthropic"):
self.api_mode = "anthropic_messages" # 第三方 Anthropic 兼容
elif self._is_direct_openai_url():
self.api_mode = "codex_responses" # 直连 OpenAI 用 Responses API
else:
self.api_mode = "chat_completions" # 默认:OpenAI 兼容
| API 模式 | 使用场景 | 原因 |
|---|---|---|
chat_completions | OpenRouter、大多数第三方 | 最广泛兼容的标准 |
anthropic_messages | Anthropic 直连、MiniMax、DashScope | Anthropic 原生 API 支持 prompt caching |
codex_responses | OpenAI 直连、Codex | GPT-5.x 的 tool calling + reasoning 需要 Responses API |
这个检测必须在客户端构造之前完成,因为不同 API 模式需要不同的客户端类型(OpenAI SDK vs Anthropic SDK)。
阶段 3:回调注册(run_agent.py:592-602)
# run_agent.py:592-602
self.tool_progress_callback = tool_progress_callback
self.tool_start_callback = tool_start_callback
self.tool_complete_callback = tool_complete_callback
self.thinking_callback = thinking_callback
self.reasoning_callback = reasoning_callback
self.clarify_callback = clarify_callback
self.step_callback = step_callback
self.stream_delta_callback = stream_delta_callback
self.status_callback = status_callback
self.tool_gen_callback = tool_gen_callback
self.background_review_callback = None # line 546
11 个回调接口,每个服务于一个具体场景:
| 回调 | 触发时机 | CLI 用途 | Gateway 用途 |
|---|---|---|---|
stream_delta_callback | 模型输出每个 token | 流式渲染到终端 | 推送到平台 |
thinking_callback | API 调用等待时 | 显示思考动画 | 显示"正在输入..." |
tool_start_callback | 工具开始执行 | 显示工具名 | 发送状态 |
tool_complete_callback | 工具执行完成 | 显示结果预览 | 更新状态 |
clarify_callback | agent 需要用户确认 | 弹出输入框 | 发送消息等待回复 |
step_callback | 每轮 API 调用开始 | — | 触发 agent:step 事件 |
status_callback | 状态信息更新 | — | 推送到平台 |
tool_progress_callback | 工具执行中间状态 | 进度条 | — |
reasoning_callback | 模型 reasoning 输出 | 渲染 reasoning | — |
tool_gen_callback | 工具调用生成中 | 显示工具参数 | — |
background_review_callback | 后台记忆/技能保存完成 | — | 推送通知到平台 |
回调的设计原则:编排层决定"什么时候"调用,入口层决定"怎么处理"。这让同一个编排逻辑零修改适配所有入口。如果一个回调是 None,编排层跳过该通知——不会报错,也不会降级。
阶段 4:LLM 客户端构造(run_agent.py:708-828)
这是初始化中最复杂的阶段,分三条路径:
路径 A:Anthropic 原生(api_mode == "anthropic_messages")
# run_agent.py:716-730
from agent.anthropic_adapter import build_anthropic_client, resolve_anthropic_token
effective_key = api_key or resolve_anthropic_token() or ""
self._anthropic_client = build_anthropic_client(effective_key, base_url)
self.client = None # 不需要 OpenAI 客户端
路径 B:显式凭据(调用方直接提供 api_key + base_url)
# run_agent.py:737-744
client_kwargs = {"api_key": api_key, "base_url": base_url}
路径 C:Provider Router 自动解析(无显式凭据)
# run_agent.py:761-763
from agent.auxiliary_client import resolve_provider_client
_routed_client, _ = resolve_provider_client(self.provider or "auto", model=self.model)
三条路径最终都构造出可用的 LLM 客户端。一个重要的细节:如果是 Claude on OpenRouter,会自动注入 x-anthropic-beta: fine-grained-tool-streaming header(run_agent.py:802-812),让 Anthropic 逐 token 流式返回工具调用参数——否则 OpenRouter 的上游代理会在 Claude 沉默思考时超时断开。
阶段 5:工具加载与过滤(run_agent.py:855-883)
# run_agent.py:856-860
self.tools = get_tool_definitions(
enabled_toolsets=enabled_toolsets,
disabled_toolsets=disabled_toolsets,
quiet_mode=self.quiet_mode,
)
self.valid_tool_names = {tool["function"]["name"] for tool in self.tools}
通过 model_tools.py 的 get_tool_definitions() 获取工具 schema 列表。这个调用触发了完整的工具发现流程(导入所有工具模块 → 自注册 → 按 toolset 过滤)。valid_tool_names 集合后续用于验证模型返回的 tool call 是否合法。
阶段 6:记忆系统初始化(run_agent.py:969-1070)
记忆初始化分两层:
内置记忆(run_agent.py:969-992):
# run_agent.py:984-990
if self._memory_enabled or self._user_profile_enabled:
from tools.memory_tool import MemoryStore
self._memory_store = MemoryStore(
memory_char_limit=mem_config.get("memory_char_limit", 2200),
user_char_limit=mem_config.get("user_char_limit", 1375),
)
self._memory_store.load_from_disk() # 从 MEMORY.md / USER.md 加载
外部 Memory Provider(run_agent.py:996-1070):
# run_agent.py:1028-1054(简化)
if _mem_provider_name:
self._memory_manager = MemoryManager()
_mp = load_memory_provider(_mem_provider_name)
if _mp and _mp.is_available():
self._memory_manager.add_provider(_mp)
self._memory_manager.initialize_all(
session_id=self.session_id,
platform=platform or "cli",
user_id=self._user_id, # Gateway 场景的用户隔离
)
外部 provider 初始化后,它注册的工具会被追加到 self.tools 列表(run_agent.py:1064-1070)。这意味着 Honcho 或 Mem0 的自定义工具和内置工具以完全相同的方式暴露给模型。
详见第 11 章(Memory Provider 架构)。
阶段 7:上下文压缩器初始化(run_agent.py:1087-1140)
# run_agent.py:1132-1140(简化)
self.context_compressor = ContextCompressor(
model=self.model,
threshold_percent=compression_threshold, # 默认 0.50
protect_first_n=3, # 保护前 3 条消息
protect_last_n=compression_protect_last, # 默认保护最后 20 条
config_context_length=_config_context_length,
)
self.compression_enabled = compression_enabled
压缩器需要知道模型的上下文窗口长度来计算阈值。这个长度的获取有三个来源:config.yaml 显式配置 > custom_providers 配置 > model_metadata 自动探测。
详见第 12 章(上下文压缩)。
IterationBudget:防止 Agent 失控
# run_agent.py:168-209
class IterationBudget:
"""Thread-safe iteration counter for an agent."""
def __init__(self, max_total: int):
self.max_total = max_total
self._used = 0
self._lock = threading.Lock()
def consume(self) -> bool:
"""Try to consume one iteration. Returns True if allowed."""
with self._lock:
if self._used >= self.max_total:
return False
self._used += 1
return True
def refund(self) -> None:
"""Give back one iteration (e.g. for execute_code turns)."""
with self._lock:
if self._used > 0:
self._used -= 1
IterationBudget 看起来简单,但它解决了一个关键问题:如何防止 agent 在工具调用循环中无限运转?
设计要点:
- 线程安全:
threading.Lock()保护计数器,因为 Gateway 场景下多个会话可能在不同线程并发运行 - 独立预算:父代理默认 90 次(
max_iterations),子代理独立分配(delegation.max_iterations,默认 50 次),互不影响 - 退款机制:
execute_code工具的调用会被refund()——因为程序化工具调用是低优先级的,不应消耗面向用户的 budget - 压力预警:当 budget 使用达到 70% 时注入 caution,90% 时注入 warning,提醒模型尽快收敛(
run_agent.py:648-650)
# run_agent.py:648-650
self._budget_caution_threshold = 0.7 # 70% — nudge to start wrapping up
self._budget_warning_threshold = 0.9 # 90% — urgent, respond now
压力预警不是作为独立消息注入(那会破坏消息结构),而是嵌入到工具结果的 JSON 中——模型在读取工具结果时自然看到预警信息。
Fallback Provider 链
# run_agent.py:830-853
if isinstance(fallback_model, list):
self._fallback_chain = [
f for f in fallback_model
if isinstance(f, dict) and f.get("provider") and f.get("model")
]
elif isinstance(fallback_model, dict):
self._fallback_chain = [fallback_model]
else:
self._fallback_chain = []
Hermes 支持 fallback provider 链——当主模型不可用(限流、overload、连接失败)时,按顺序尝试备选模型。配置示例:
# config.yaml
fallback_providers:
- provider: anthropic
model: claude-sonnet-4-20250514
- provider: openai
model: gpt-4o
Fallback 的触发不在初始化阶段,而是在 run_conversation() 的 API 重试逻辑中(run_agent.py:7285+)。当所有重试耗尽后,编排器切换到 fallback chain 的下一个 provider。切换后的状态会在下一轮 run_conversation() 调用时通过 _restore_primary_runtime() 恢复。
背景自我改进:Nudge 与 Background Review
Hermes 的 "Learning Loop" 赌注不只是一个概念,而是编码在 AIAgent 中的具体机制:
# run_agent.py:973-976, 1072-1076
self._memory_nudge_interval = 10 # 每 10 个 user turn 检查一次记忆
self._skill_nudge_interval = 10 # 每 10 个 tool iteration 检查一次技能
self._turns_since_memory = 0
self._iters_since_skill = 0
当计数器达到阈值时,编排器在主循环结束后调用 _spawn_background_review()(run_agent.py:1718):
# run_agent.py:1718-1764(简化)
def _spawn_background_review(self, messages_snapshot, review_memory=False, review_skills=False):
def _run_review():
review_agent = AIAgent(model=self.model, max_iterations=8, quiet_mode=True)
review_agent._memory_store = self._memory_store # 共享记忆存储
review_agent.run_conversation(user_message=prompt, conversation_history=messages_snapshot)
threading.Thread(target=_run_review, daemon=True).start()
这个方法:
- 创建一个独立的
AIAgent实例(max_iterations=8,限制 budget) - 共享主 agent 的
_memory_store(让 review agent 可以直接写入记忆) - 在后台线程中运行,不阻塞当前响应
- 将 review agent 自己的 nudge 设为 0(避免递归触发)
- 完成后通过
background_review_callback通知平台(Gateway 场景下推送到消息平台)
这就是 Hermes 如何在不打断用户的情况下自动审查对话、提炼记忆和创建技能。
中断机制
# run_agent.py:609-611
self._interrupt_requested = False
self._interrupt_message = None
self._client_lock = threading.RLock()
中断机制解决一个交互问题:用户在 agent 执行工具时发送了新消息,或在 CLI 按了 Ctrl+C。编排器需要能安全地中断当前循环,而不是等所有工具执行完毕。
中断传播到子代理:
# run_agent.py:615-617
self._delegate_depth = 0 # 0 = 顶层 agent
self._active_children = [] # 运行中的子代理
self._active_children_lock = threading.Lock()
当顶层 agent 被中断时,它会遍历 _active_children 设置每个子代理的 _interrupt_requested = True,实现递归中断。
活动追踪
# run_agent.py:661-664
self._last_activity_ts: float = time.time()
self._last_activity_desc: str = "initializing"
self._current_tool: str | None = None
self._api_call_count: int = 0
活动追踪不是日志——它是 Gateway 超时处理器的数据源。当 Gateway 因超时杀死 agent 时,这些字段告诉运维人员"agent 被杀时正在做什么",以及"距离上次活动过了多久"。这在调试 agent 卡住问题时极为有用。
run_conversation() 主循环的控制流
初始化完成后,AIAgent 的核心能力通过 run_conversation()(run_agent.py:6800)暴露。第 3 章已追踪了消息在主循环中的流动路径,这里从控制流角度补充关键设计:
循环结构
# run_agent.py:7111(简化)
while api_call_count < self.max_iterations and self.iteration_budget.remaining > 0:
if self._interrupt_requested: break # 退出条件 1
if not self.iteration_budget.consume(): break # 退出条件 2
response = self._interruptible_streaming_api_call(...)
if assistant_message.tool_calls:
self._execute_tool_calls(...) # 有工具 → 继续循环
continue
else:
final_response = assistant_message.content # 无工具 → 退出条件 3
break
退出条件枚举
| 退出条件 | 代码位置 | 类型 |
|---|---|---|
| 模型无 tool_calls | run_agent.py:8876 | 正常完成 |
| 用户中断 | run_agent.py:7116 | 正常中断 |
| Budget 耗尽 | run_agent.py:7125 | 预算限制 |
| context_length_exceeded 压缩失败 | 主循环内 retry | 错误退出 |
| 不可重试 4xx 错误 | run_agent.py:8295+ | 错误退出 |
| 所有重试耗尽 | run_agent.py:8448+ | 错误退出 |
| finish_reason=length 续写耗尽 | run_agent.py:7638+ | 边界退出 |
收尾流程
循环退出后,编排器执行 9 步收尾(run_agent.py:9080+):trajectory 保存 → 资源清理 → post_llm_call hook → session 持久化 → memory_manager.sync_all() → prefetch 排队 → background review(如触发 nudge)→ on_session_end hook → 返回结果。
第 3 章已对主循环和收尾流程做了完整分析,此处仅补充控制流视角。
为什么 run_agent.py 有 9431 行没有拆分
这可能是读者翻开源码时的第一个疑问:一个 9431 行的 Python 文件,129 个方法,为什么不模块化?
先看这 9431 行到底装了什么:
| 职责 | 行数范围 | 约行数 | 占比 |
|---|---|---|---|
run_conversation() 主循环 | 6800-9200 | ~2400 | 25% |
__init__ 初始化链 | 433-1140 | ~700 | 7% |
| 工具执行(串行 + 并行) | 5930-6590 | ~660 | 7% |
| API 调用与流式处理 | 4170-5230 | ~1060 | 11% |
| 错误恢复(重试、fallback、压缩) | 分散在主循环内 | ~800 | 9% |
| Session/Trajectory 持久化 | 1840-2400 | ~560 | 6% |
| System prompt 构建 | 2580-2740 | ~160 | 2% |
| 消息清洗与安全检查 | 2750-3680 | ~930 | 10% |
| Streaming 解析(Codex/Anthropic/OpenAI) | 3690-4170 | ~480 | 5% |
| 辅助方法(日志、调试、格式化) | 散布 | ~680 | 7% |
| 内部类(_SafeWriter, IterationBudget 等) | 1-416 | ~415 | 4% |
不拆分的实际原因
1. 循环引用的工程成本
run_conversation() 的 2400 行主循环直接访问 self._memory_manager、self.context_compressor、self._session_db、self.tools、self.iteration_budget 等十余个实例属性。如果把主循环拆到 agent/conversation_loop.py,要么传入十几个参数,要么传入 self 本身——后者等于没拆。
Python 的模块系统在处理深度互引时远不如 Rust/Go 的包系统优雅。run_agent.py 的 129 个方法中,约 80% 需要访问 AIAgent 的实例状态。拆分后的每个模块仍然紧密耦合到 AIAgent,带来的不是解耦而是间接性。
2. 主循环的内聚性
run_conversation() 的 2400 行不是因为它做了太多不相关的事,而是因为 agent 对话循环本质上就是复杂的:
准备 → API 调用 → 流式解析 → 工具判断 → 并行/串行执行 → 错误恢复
↑ |
└────────── 重试 / 压缩 / fallback / interrupt ←──────────┘
这个循环中的每一步都可能触发错误恢复(API 限流 → 重试 → fallback → 压缩 → 重试),错误恢复的路径又可能回到循环的任何阶段。拆分成独立模块会把这个状态机的边打散,让控制流变得更难追踪。
3. 增量演化的代价
Hermes 从 v0.1 到 v0.8 的 9 个版本中,run_agent.py 持续增长。每个版本添加的功能(prompt caching、fallback 链、预飞行压缩、plugin hooks、Codex Responses API 支持等)都嵌入主循环的不同阶段。重构为模块化架构需要冻结功能开发——对一个快速迭代的开源项目来说,这个代价通常被推迟。
已经拆出去的模块
值得注意的是,Hermes 并非没有模块化意识。agent/ 目录下的 25 个模块就是从 run_agent.py 逐步剥离出来的:
agent/prompt_builder.py(983 行):system prompt 的无状态拼装函数agent/prompt_caching.py(72 行):Anthropic cache control 逻辑agent/context_compressor.py(696 行):上下文压缩算法agent/memory_manager.py(367 行):记忆编排agent/retry_utils.py:重试策略agent/model_metadata.py:模型能力探测
这些模块有一个共同特征:它们是无状态或弱状态的,不需要深度访问 AIAgent 实例。能拆的已经拆了,剩下的是确实需要 self 的有状态逻辑。
对读者的建议
不要试图线性阅读 run_agent.py 的 9431 行。推荐的阅读方式:
- 从
__init__(416 行)开始,理解初始化的七个阶段(本章已分析) - 跳到
run_conversation()(6800 行),只看主循环骨架(第 3 章已分析) - 按需深入具体的错误恢复路径或工具执行逻辑
这个文件的"正确心理模型"不是"一个巨大的类",而是"一个带有状态的有限状态机,包含了所有状态转换逻辑"。
设计启示
拆解 AIAgent 的初始化与主循环,可以提炼出四个设计原则:
- 初始化即决策:50+ 个参数不是配置的堆砌,而是在构造时就确定了 API 模式、prompt caching 策略、工具集合、记忆后端等关键决策。这些决策在整个
run_conversation()生命周期中不会改变(除非 fallback 触发),让编排逻辑可以安全地做出假设 - 回调是适配层的接口:11 个回调让编排层和入口层保持单向依赖——编排层不知道也不关心自己运行在 CLI 还是 Telegram 还是 Cron 中
- Budget 是安全网:IterationBudget + 压力预警 + background review 的组合让 agent 既有足够的自主性(90 次迭代),又有收敛机制(70%/90% 预警),还有自我改进能力(background review)——这三者的平衡是 Hermes 作为"长期协作代理"的核心
- 有状态内聚优于无状态拆分:9431 行的
run_agent.py不是设计缺陷而是权衡结果——当 80% 的方法需要深度访问实例状态时,强行模块化带来的不是解耦而是间接性。Hermes 的策略是"能拆的拆出去(agent/25 个模块),剩下的保持内聚"
第 5 章将深入 system prompt 的构建过程——_build_system_prompt() 如何将 identity、memory、skills、context files、platform hints 拼装成一个 prompt cache 友好的系统提示。
设计赌注回扣:本章回扣了全部四个赌注:CLI-First(回调体系让 CLI 成为第一等入口)、Run Anywhere(同一 AIAgent 适配所有平台)、Personal Long-Term(记忆初始化 + nudge 机制)、Learning Loop(
_spawn_background_review实现自动技能创建)。
版本演化说明
本章核心分析基于 Hermes Agent v0.8.0(2026 年 4 月)。
AIAgent.__init__的参数列表随功能持续扩展。独立的IterationBudget在 v0.3.0 发布窗口就已出现,_spawn_background_review的后台审查则在 v0.4.0 发布窗口进入主线;此后的变化主要是围绕 provider 路由、回调面和初始化职责继续堆叠。