Skip to content

AIAgent / init_agent

源码版本v2026.7.20

职责

init_agent 是 agent 的「装配车间」:把 provider、toolsets、压缩阈值、自定义 provider extra_body、记忆与学习组件等配置注入到 agent 实例,使其具备运行一轮对话所需的全部依赖。AIAgent 类本身只是它产物的薄壳。

设计动机

配置和运行时分开,是为了让「跑一轮对话」的代码路径尽可能无状态。init_agent 一次性把所有启动期决策(provider 选择、压缩策略、toolsets 开关、extra_body 合并)落定,之后 run_conversation 进入「喂消息→跑循环」的节奏,不必回头改配置。这样做的直接好处是:run_conversation 可以放心并发,同一 agent 实例同时被多条入站消息驱动时,没有「配置正在被改」的竞态。

压缩阈值放在启动期解析、而不是在压缩触发时再算,另一个动机:Codex gpt-5.x 家族的 272K 窗口需要把阈值「自动抬上去」用满窗口,这个抬升要通知用户一次。运行时每次算要么反复弹通知,要么漏弹。启动期算一次、写 marker 文件,后续直接读 marker 判定。

关键文件

数据流

  1. AIAgent.__init__(run_agent.py:423) 把构造参数原样收集。
  2. init_agent:276,依次:
  3. 装配完成的 agent 暴露 run_conversation 方法(run_agent.py:6350),转发到模块级 agent/conversation_loop.py:588
  4. 网关 GatewayRunner(gateway/run.py:3029)持有装配好的 agent,把入站消息交给它。

provider 名在 init_agent 里被规整为小写,空白被剥离;provider 既用作路由提示也用作 api_mode 推断的依据:

python
agent.base_url = base_url or ""
provider_name = provider.strip().lower() if isinstance(provider, str) and provider.strip() else None
agent.provider = provider_name or ""
...
if api_mode in {"chat_completions", "codex_responses", "anthropic_messages", "bedrock_converse", "codex_app_server"}:
    agent.api_mode = api_mode
elif agent.provider == "openai-codex":
    agent.api_mode = "codex_responses"
elif agent.provider in {"xai", "xai-oauth"}:
    agent.api_mode = "codex_responses"

压缩阈值的解析在 _resolve_compression_threshold 里。Codex 的 autoraise 是单向的——只能抬高,不能压低用户已经设的更高阈值:

python
if model_cthresh is None:
    return global_threshold, None
if is_codex_autoraise:
    if model_cthresh <= global_threshold + 1e-9:
        # Autoraise never lowers; keep the user's higher/equal threshold.
        return global_threshold, None
    return model_cthresh, {
        "model": model,
        "from": global_threshold,
        "to": model_cthresh,
    }
return model_cthresh, None

自定义 provider 的 extra_body 合并,核心是按 base_url + model 匹配配置项,找到后把 extra_body 并进 request_overrides,已有键优先保留用户自己设的值:

python
merged_extra_body = dict(extra_body)
existing_extra_body = overrides.get("extra_body")
if isinstance(existing_extra_body, dict):
    merged_extra_body.update(existing_extra_body)
overrides["extra_body"] = merged_extra_body
agent.request_overrides = overrides

update 的方向是「配置里的 extra_body 当底,用户已有的 extra_body 覆盖上面」,所以运行时塞进 request_overrides["extra_body"] 的字段不会被启动配置覆盖。

边界与失败

  • provider 名无法解析到 api_mode:用户给了一个未知的 provider="foobar",api_mode 不会被推断,会落到默认的 chat_completions 路径。如果 foobar 实际只支持 codex_responses,运行时第一条消息就会 400。init_agent 不校验 provider 是否在白名单里,只规整大小写。
  • custom extra_body 合并冲突:同一个 base_url 下配了多个 entry,且都匹配同一个 model,_custom_provider_extra_body_for_agent 走「第一个 model 显式匹配的赢」,后面被忽略。entry 里没有 model 字段时作为 fallback,只有当没有显式 model 匹配项时才用。用户改了配置但没重启 agent,以为新 extra_body 生效了,实际还是旧 fallback。
  • 压缩阈值非法值:global_threshold 如果是 None 或负数,_resolve_compression_threshold 不做范围校验,直接透传。负数会让压缩永远不触发,None 在比较时会抛 TypeError。配置层(hermes config)有范围检查,但 init_agent 不重复校验,直接信任 config 产物。
  • autoraise notice 写 marker 失败:$HERMES_HOME 只读或磁盘满时,_record_codex_gpt55_autoraise_notice 静默跳过,代价是下次 init 再弹一次通知。这是有意的 trade-off:写 marker 失败不应该让 agent 起不来。
  • _ra() 的惰性 import:init_agent 里辅助函数通过 _ra()run_agent 模块,而不是顶部 import,目的是让测试可以 monkeypatch run_agent.OpenAI 等属性后再调 init_agent。代价是首次调用多一次 import,patch 时机晚了的话已经绑定的属性不会被新 patch 覆盖。

小结

init_agent 是配置→运行时的翻译层,所有启动期决策(provider 选择、压缩策略、toolsets 开关)都在这里一次性落定。之后 agent 进入无状态的「喂消息→跑循环」节奏,不再回头改配置。

非官方社区学习站,内容以 MIT 许可的 NousResearch/hermes-agent 源码为依据。