Hermes Agent 源码解析(三):Agent 主循环 ------ run_agent.py 的核心秘密
run_agent.py4123 行,是AIAgent类的主场。构造器 60 个参数、同步循环、budget tracking、provider 无关设计------这篇把心脏拆开看。
AIAgent 构造
构造器签名有约 60 个参数 (通过 agent/agent_init.py 的 init_agent() 转发器实现),核心分类:
python
class AIAgent:
def __init__(self,
# 模型连接 / 模式选择 / 运行边界
base_url=None, provider=None, model="",
api_mode="chat_completions",
max_iterations=90,
enabled_toolsets=None,
session_id=None,
# 预算 / 回调 / credential_pool / checkpoints ...
iteration_budget=None,
tool_progress_callback=None,
): ...
构造器实际是代理模式 ------ __init__ 把所有参数转发给 agent.agent_init.init_agent(),这样 agent_init 可以在不干扰 AIAgent 类签名的情况下,自由重组初始化逻辑。
run_conversation() ------ 核心循环
这是 Agent 的主循环,完全同步(Synchronous),设计非常精炼:
python
while (api_call_count < self.max_iterations
and self.iteration_budget.remaining > 0) \
or self._budget_grace_call:
if self._interrupt_requested:
break
response = client.chat.completions.create(
model=model, messages=messages, tools=tool_schemas,
)
if response.tool_calls:
for tc in response.tool_calls:
messages.append(tool_result_message(
handle_function_call(tc.name, tc.args, task_id)))
api_call_count += 1
else:
return response.content
循环的三个关键机制
1. Budget Tracking :iteration_budget 跟踪已消耗的 token 和调用次数,防止无限循环。_budget_grace_call 留出一轮的"宽限" ------ 当 budget 刚好耗尽但模型正要输出有用结果时不强行中断。
这里有个反直觉的设计点:grace call 不加 budget。如果是无限循环,最后一步也会耗完 budget,不存在逃逸。这比在后处理阶段处理边界情况要干净得多。
2. Interrupt 机制 :_interrupt_requested 标志由外部线程设置(CLI 的 Ctrl+C 或 Gateway 的 /stop 命令),循环在下一次迭代开始时干净退出,不留僵尸进程。
3. Tool Call 批处理 :一次 LLM 响应可能包含多个 tool_calls(parallel tool calling),循环逐个执行并追加结果,再发回模型继续推理。
消息格式
遵循 OpenAI 标准消息格式:
python
# system → 系统提示词 / user → 用户输入
# assistant → 模型回复(可能含 tool_calls)
# tool → 工具执行结果
# reasoning → 推理内容存储在 assistant_msg["reasoning"]
三对接口
AIAgent 暴露三种使用方式:
python
# 1. 简单:response = agent.chat("你好")
# 2. 完整:result = agent.run_conversation("写个脚本")
# 3. 低层:agent.add_user_message("开始"); agent.run()
chat() 是 run_conversation() 的薄封装;run_conversation() 是核心循环;低层接口供 Gateway 等需要精细控制会话生命周期的调用者使用。
延迟导入优化
run_agent.py 顶部有一个值得注意的优化 ------ OpenAI SDK 不在模块顶层 import:
python
# NOTE: `from openai import OpenAI` 故意不在模块顶层
# SDK 导入耗时约 240ms,通过薄代理对象在首次调用时做真正 import
这个模式保证了 hermes --help 等不需要 LLM 的操作不会被 SDK 加载拖慢。
Provider 无关的设计
run_conversation() 不关心背后是哪个 provider。_create_openai_client() 根据运行时配置多态构造:
ini
provider → resolution → client
├─ OpenAI → openai.OpenAI(base_url=...)
├─ Anthropic → proxy-compatible client
├─ Gemini / Bedrock / Azure ...
└─ 10+ plugin providers → 统一接口
所有 provider 都暴露同一套 client.chat.completions.create() 接口 ------ 这是架构上最优雅的设计之一。
这个统一接口意味着:换 provider 对 run_conversation() 完全透明。逻辑层不关心你是走 OpenAI 还是 Anthropic,甚至不关心你是 REST 还是 gRPC------适配器层把差异消化干净了。
完整源码见 NousResearch/hermes-agent 下的 run_agent.py。
欢迎一起讨论! 60 个参数的构造器你觉得是必要之恶还是应该拆解?
📢 关注我,获取最新开源更新日志和开发者工具动态。如果觉得这篇文章有用,欢迎转发给身边的开发者朋友。
#源码解析 #HermesAgent #Agent循环 #开源项目
下一篇我们聚焦工具系统 ------ 70+ 工具如何自注册、自动发现、按需加载。