跳到主要内容

模型路由:正则注册表 + 优先级

这一章解释:为什么 model="claude-..."model="GTA1-7B+gemini/..." 会选中完全不同的“大脑”,而你的代码一行没变。

它要解决的小问题

有 20+ 种模型,每种的 API 格式都不同。框架需要一个查表机制:给一个模型名字符串,找到对应的处理逻辑(loop)。而且要支持“一个 loop 管一类模型”(所有 claude-* 走同一个 Anthropic loop)。

思路/直觉:每个 loop 自己声明“我管哪些模型”

不是中心化的 if-else,而是去中心化的自注册:每个 loop 类头上挂一个装饰器,用正则声明它认领的模型名模式 + 一个优先级数字。导入时它们把自己登记进全局列表。匹配时按优先级从高到低试,第一个正则命中的胜出。

图示:注册与匹配

导入 loops/ 包时,每个 @register_agent 把自己塞进全局表:

_agent_configs (按 priority 降序排列)
┌──────────────────────────────────────────────┐
│ priority 2 : r"omniparser\+.*|omni\+.*" →OmniparserConfig │
│ priority 2 : r"moondream3\+.*" →Moondream3 │
│ priority 1 : r".*\+.*" →ComposedGrounded │ ← 含 '+' 的组合模型
│ priority 1 : r"(?i).*qwen35.*" →Qwen35 │
│ priority 0 : r".*claude-.*" →AnthropicConfig │
│ priority 0 : r".*(computer-use-preview..)→OpenAIConfig │
│ priority 0 : r".*GTA1.*" →GTA1 (只 click) │
│ ... │
│ priority -1: r"(?i).*ui-?tars.*" →UITARS │
│ priority -100: r"(?i).*" →GenericVLM (兜底) │
└──────────────────────────────────────────────┘

find_agent_config("claude-sonnet-4-5")
│ 从上往下试 re.match,第一个命中即返回

AnthropicConfig

怎么读: 优先级是“谁先被试”。组合模型(含 +)优先级高,所以 GTA1-7B+claude-... 会先命中 .*\+.*ComposedGrounded,而不会被 .*GTA1.* 抢走;兜底的 GenericVLM 优先级 -100,只有谁都不匹配时才轮到它。

真实实现

装饰器(agent/cua_agent/decorators.py:13-55,register_agent)。它先验证这个类实现了协议三件套(predict_step/predict_click/get_capabilities),再把 (类, 正则, 优先级, tool_type) 包成 AgentConfigInfo 存进全局 _agent_configs,并按优先级降序排序:

# agent/cua_agent/decorators.py:42-51 (真实源码,节选)
config_info = AgentConfigInfo(
agent_class=agent_class, models_regex=models,
priority=priority, tool_type=tool_type,
)
_agent_configs.append(config_info)
_agent_configs.sort(key=lambda x: x.priority, reverse=True) # 高优先级在前

匹配(agent/cua_agent/decorators.py:78-93,find_agent_config):遍历(已排好序的)表,re.match 第一个命中即返回。

# agent/cua_agent/types.py:43-45 (真实源码)
def matches_model(self, model: str) -> bool:
"""Check if this agent config matches the given model"""
return bool(re.match(self.models_regex, model))

注册靠“导入即副作用”: loops/__init__.py 把每个 loop 模块 import 一遍(from . import anthropic, composed_grounded, ...,agent/cua_agent/loops/__init__.py:6-26),装饰器在导入时执行,自注册就完成了。cua_agent/__init__.py:9from . import loops 触发这一切。

几个真实的注册声明

loop 类正则prioritytool_type
AnthropicHostedToolsConfig.*claude-.*0None
OpenAIComputerUseConfig.*(computer-use-preview|gpt-?5\.?4)0None
ComposedGroundedConfig.*\+.*1None
GTA1Config.*GTA1.*0None(只 click)
FaraVlmConfig(?i).*fara-7b.*0browser
GenericVLMConfig(?i).*(兜底)-100None

(分别见 loops/anthropic.py:1753loops/openai.py:180loops/composed_grounded.py:123loops/gta1.py:75loops/fara/config.py:178loops/generic_vlm.py:237。)

关键细节/坑

  • cua/<provider>/ 路由前缀会被剥掉再匹配。 find_agent_config 先试原串,不中再用 _strip_cua_prefix 去掉 cua/google/ 这类前缀重试(agent/cua_agent/decorators.py:63-91),让云端路由的模型解析到和裸名一样的 loop。
  • tool_type 决定要不要自动包工具。 声明 tool_type="browser" 的 loop(如 FARA),框架会把传进来的 Computer 自动包成 BrowserTool(agent/cua_agent/agent.py:455-531,_resolve_tools)。一般模型 tool_type=None,工具原样透传。
  • 本地模型靠 liteLLM 自定义 provider。 构造 agent 时注册了 huggingface-local/human/mlx/cua/azure_ml 五个自定义 provider(agent/cua_agent/agent.py:367-373),所以 huggingface-local/HelloKKMe/GTA1-7B 这种模型名能被 liteLLM 路由到本地适配器。
  • 协议是结构鸭子类型(Protocol)。 loop 不必继承基类,只要有那三个方法即可;装饰器用 hasattr 校验(agent/cua_agent/decorators.py:28-39)。基类定义见 loops/base.py:11(AsyncAgentConfig)。

代码地图

主题文件路径符号名
注册装饰器agent/cua_agent/decorators.pyregister_agent, get_agent_configs
匹配/前缀剥离agent/cua_agent/decorators.pyfind_agent_config, _strip_cua_prefix
注册元数据agent/cua_agent/types.pyAgentConfigInfo, matches_model
触发注册agent/cua_agent/loops/__init__.py(导入副作用)
自定义 provideragent/cua_agent/agent.pylitellm.custom_provider_map
工具自动包装agent/cua_agent/agent.pyComputerAgent._resolve_tools