从源码看 Hermes 事件循环策略设计:主线程、工作线程、异步上下文怎么玩?
1. 主线程,无运行中的循环 → 持久事件循环 (_tool_loop)
原理: 当主线程运行时,如果没有现成的事件循环,就创建一个持久的循环并长期持有。所有缓存的异步 HTTP 客户端(httpx.AsyncClient / AsyncOpenAI)都会绑定到这个循环,避免每次调用都重新创建。
典型场景: CLI 模式下,用户在终端进行对话。每次需要调用辅助任务(如 vision 分析、web extraction)时,使用的是同一个循环,避免重复初始化开销。
# _get_cached_client 中的关键逻辑
if async_mode:
current_loop = _aio.get_event_loop()
loop_id = id(current_loop) # 循环身份作为缓存键的一部分
如果循环不存在(RuntimeError),_get_event_loop() 会抛异常,此时可以创建一个持久循环(例如 _tool_loop)并缓存起来。
缓存键中包含 loop_id:
cache_key = (provider, async_mode, base_url or"", api_key or"", loop_id)
这保证了不同循环不会共享客户端,防止跨循环使用导致的崩溃。
2. 工作线程(例如 delegate_task 池)→ 每线程持久循环 (_worker_loop)
原理: 子 Agent 在独立线程中运行(通过 delegate_tool 的线程池),每个线程创建并持有自己的持久事件循环。这样避免与主线程竞争,并且当该线程结束时,即使主线程的循环还活着,也不会影响该线程的客户端清理。
典型场景: 主 Agent 使用 delegate_task 工具调用子 Agent。子 Agent 的运行过程完全独立,有自己的 loop 和客户端缓存。
def_run_single_child(task_index: int, goal: str, child=None, parent_agent=None, **_kwargs) -> Dict[str, Any]:
"""
Run a pre-built child agent. Called from within a thread.
Returns a structured result dict.
"""
# 线程内部运行子 Agent
result = child.run_conversation(user_message=goal)
# ...
子 Agent 在自己的线程中执行,线程池管理生命周期。当线程结束时,它绑定的所有异步客户端也会随之清理,避免主线程 GC 时误以为循环还活着。
为什么要这样做?
-
• 主线程的 _tool_loop可能还活着,但工作线程已经结束。 -
• 如果工作线程使用了主线程的循环,会导致循环引用和清理顺序问题。 -
• 每线程独立循环保证了线程隔离和生命周期清晰。
3. 在异步上下文内部(网关、RL 环境)→ 带 asyncio.run() 的一次性线程
原理: 当代码已经在一个运行中的事件循环内(例如 Gateway 的 asyncio 服务器),需要调用同步或阻塞操作时,创建一个新线程并在该线程内启动一个新的事件循环(使用 asyncio.run())。这样避免了与已有循环冲突。
典型场景: Gateway 处理多条消息,所有消息在同一个事件循环中处理。如果某步需要调用阻塞的工具(如同步的 file_operations),可以将其放入新线程,并在该线程中启动新循环,避免阻塞主循环。
# 伪代码示意
asyncdefhandle_message(message):
# 当前已经在 Gateway 的事件循环中
# 需要调用一个阻塞操作
result = await asyncio.to_thread(some_blocking_operation)
# some_blocking_operation 可能在其内部创建新循环(例如调用需要 asyncio 的库)
另一种直接做法是:
defrun_in_fresh_loop():
asyncio.run(blocking_async_task())
# 从已有循环中调用
await asyncio.to_thread(run_in_fresh_loop)
为什么需要一次性循环?
-
• 避免在已有循环中再创建子循环(asyncio 不允许嵌套循环) -
• 阻塞操作不会影响主循环的性能 -
• 生命周期简单,用完即销毁
总结对比
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
这种分层策略确保了不同执行环境下的异步安全性,同时最大程度地复用了客户端实例,减少开销。
夜雨聆风