AIAgent / init_agent
职责
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 判定。
关键文件
class AIAgent:400-497— 类定义,字段即配置__init__ 转发:497-560—from agent.agent_init import init_agent; init_agent(...)def init_agent:276— 装配主函数入口自定义 provider extra_body 辅助:183-275—_normalized_custom_base_url/_custom_provider_model_matches/_merge_custom_provider_extra_body_resolve_compression_threshold:93-127— 压缩阈值解析辅助函数区:62-91—_ra、autoraise notice 等
数据流
AIAgent.__init__(run_agent.py:423) 把构造参数原样收集。- 调
init_agent:276,依次:- 解析 provider 名与 base_url(
agent/agent_init.py:183) - 合并自定义 provider 的
extra_body(agent/agent_init.py:257) - 解析压缩阈值(
agent/agent_init.py:93) - 注入 toolsets、记忆管理器、学习图等
- 解析 provider 名与 base_url(
- 装配完成的 agent 暴露
run_conversation方法(run_agent.py:6350),转发到模块级agent/conversation_loop.py:588。 - 网关
GatewayRunner(gateway/run.py:3029)持有装配好的 agent,把入站消息交给它。
provider 名在 init_agent 里被规整为小写,空白被剥离;provider 既用作路由提示也用作 api_mode 推断的依据:
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 是单向的——只能抬高,不能压低用户已经设的更高阈值:
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,已有键优先保留用户自己设的值:
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 = overridesupdate 的方向是「配置里的 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,目的是让测试可以 monkeypatchrun_agent.OpenAI等属性后再调init_agent。代价是首次调用多一次 import,patch 时机晚了的话已经绑定的属性不会被新 patch 覆盖。
小结
init_agent 是配置→运行时的翻译层,所有启动期决策(provider 选择、压缩策略、toolsets 开关)都在这里一次性落定。之后 agent 进入无状态的「喂消息→跑循环」节奏,不再回头改配置。