大家好!我们今天继续拆解 browser-use 项目,今天我们将分析它的Controller 层。随着大型语言模型(LLM)能力的飞速发展,构建能够与外部世界交互、执行复杂任务的智能 Agent 成为了一个热门方向。无论是网页自动化、数据提取,还是与其他 API 交互,Agent 都需要一个可靠的机制来理解和执行“行动”(Actions)。

今天,我们将深入探讨 browser-use 项目中实现的高度灵活、可扩展且类型安全的行动控制器(Action Controller)模式。我们将通过分析代码(包含 views.py, service.py, controller.py),揭示其设计思想、核心组件以及如何巧妙地利用 Pydantic 和 Python 的特性来构建一个既方便开发者使用,又适合 LLM 集成的系统。

一、 核心概念与数据结构(views.py

在构建任何系统之前,清晰的数据结构定义是关键。views.py 文件(第一个实例)为我们定义了行动注册与执行所需的核心 Pydantic 模型。

  1. RegisteredAction:行动的“身份证”

    class RegisteredAction(BaseModel):
        name: str
        description: str
        function: Callable
        param_model: Type[BaseModel]
        model_config = ConfigDict(arbitrary_types_allowed=True)
        # ... prompt_description method ...
    

    RegisteredAction 是我们系统中每个行动的详细描述。它包含了:

    • name: 行动的唯一标识符,通常是其对应的函数名。
    • description: 对行动功能的自然语言描述。这个描述至关重要,因为它将展示给 LLM,帮助 LLM 理解何时应该调用此行动。
    • function: 实际执行该行动的可调用对象(函数或方法)。注意这里允许存储 Callable 类型,arbitrary_types_allowed=True 配置项使其成为可能。
    • param_model: 一个 Pydantic 模型类,定义了执行此行动所需的参数及其类型。这为参数验证提供了坚实的基础。

    prompt_description 方法特别有趣,它将行动信息格式化为适合 LLM Prompt 的字符串,清晰地告诉 LLM 这个行动叫什么、需要哪些参数以及参数的结构。

  2. ActionModel:动态行动调用的载体

    class ActionModel(BaseModel):
        model_config = ConfigDict(arbitrary_types_allowed=True)
        # ... get_index / set_index methods ...
    

    ActionModel 是一个基类,后续我们会看到它被用来动态创建具体的行动调用模型。想象一下,LLM 决定要执行 click_element 这个动作,并且需要参数 index=5。那么系统会生成一个类似这样的动态模型实例:

    # 概念性示例,非实际代码
    ChosenAction(click_element=ClickElementParams(index=5))
    

    这个 ChosenAction 就会继承自 ActionModel。这种设计的巧妙之处在于,它将 LLM 的“选择”和“参数”封装在一个结构化的 Pydantic 对象中。get_indexset_index 方法则提供了方便的操作接口,特别是针对网页自动化中常见的元素索引操作。

  3. ActionRegistry:行动的“注册中心”

    class ActionRegistry(BaseModel):
        actions: Dict[str, RegisteredAction] = {}
        # ... get_prompt_description method ...
    

    这是一个简单的容器,核心是一个字典 actions,存储了所有已注册的 RegisteredAction 实例,以行动名称作为键。它提供了一个集中的地方来管理所有可用的行动,并能方便地生成所有行动的描述列表(get_prompt_description)给 LLM 参考。

你可以把 ActionRegistry 想象成一家餐厅的菜单。
RegisteredAction 就是菜单上的每一道菜品(比如“宫保鸡丁”)。
name 是菜名("gongbao_jiding")。
description 是菜品的描述(“鲜香麻辣,鸡丁嫩滑”)。
param_model 是点这道菜需要的选项(比如辣度 la_du: str = 'medium',是否加花生 jia_huasheng: bool = True)。
function 是厨师制作这道菜的方法。
ActionRegistry.actions 就是整个菜单(所有菜品的列表)。
get_prompt_description 就是把菜单打印出来给顾客(LLM)看。

二、 注册与执行引擎(service.py - Registry 类)

service.py 中的 Registry 类是整个行动控制器的核心引擎,负责行动的注册和执行逻辑。

  1. 行动注册:@registry.action 装饰器的魔力

    class Registry(Generic[Context]):
        # ... __init__ ...
    
        def _create_param_model(self, function: Callable) -> Type[BaseModel]:
            # ... dynamically creates Pydantic model from function signature ...
    
        def action(
            self,
            description: str,
            param_model: Optional[Type[BaseModel]] = None,
        ):
            def decorator(func: Callable):
                # ... skip excluded actions ...
                actual_param_model = param_model or self._create_param_model(func)
                wrapped_func = # ... wrap sync func to async ... or use original async func
                action = RegisteredAction(...)
                self.registry.actions[func.__name__] = action
                return func
            return decorator
        # ...
    

    Registry 类提供了一个 @registry.action 装饰器,这是注册新行动的主要方式。它的工作流程非常优雅:

    • 描述与参数模型:开发者在装饰器中提供行动的 description。可以选择性地提供一个预定义的 param_model (比如我们稍后会看到的 ClickElementAction)。
    • 自动参数模型创建:如果开发者没有提供 param_model_create_param_model 方法会介入。它会检查被装饰函数的签名(参数及其类型注解),并动态地创建一个 Pydantic 模型来匹配这些参数!这极大地简化了注册过程,开发者只需写好函数和类型提示即可。它还会智能地排除像 browserpage_extraction_llm 这样的上下文参数。
    • 异步兼容性:装饰器会检查函数是否是异步 (async def)。如果不是,它会自动使用 asyncio.to_thread 将其包装成一个异步函数。这确保了所有注册的行动最终都可以通过 await 来调用,统一了执行接口。
    • 注册到中心:最后,它创建一个 RegisteredAction 实例,包含所有信息(名称、描述、处理过的函数、参数模型),并将其存储在内部的 ActionRegistry 中。

怎么理解如上逻辑呢,想象一下餐厅的总厨(Registry)制定了一个新菜品提交流程(@registry.action 装饰器):
1. 厨师(开发者)研发了一道新菜(func)。
2. 厨师需要写明菜品描述(description)。
3. 厨师可以提供一个详细的配料表(param_model),说明需要哪些特殊调料和规格。
4. 如果厨师比较懒,没有提供详细配料表,总厨的助手(_create_param_model)会根据菜谱(函数签名)自动生成一个标准的配料需求单。
5. 为了保证出餐流程统一(异步执行),如果这道菜是慢炖的(同步函数),助手会把它标记为需要在特定炉子上慢炖(asyncio.to_thread 包装)。
6. 最后,这道新菜品的信息(RegisteredAction)被添加到正式菜单(ActionRegistry)上。

  1. 行动执行:execute_action 方法

    async def execute_action(
        self,
        action_name: str,
        params: dict,
        # ... optional context: browser, page_extraction_llm, sensitive_data, etc. ...
        context: Context | None = None,
    ) -> Any:
        # ... find action ...
        action = self.registry.actions[action_name]
        try:
            # 1. Validate params using the action's param_model
            validated_params = action.param_model(**params)
    
            # 2. Check signature & prepare arguments
            sig = signature(action.function)
            # ... check if first param is Pydantic model, get parameter names ...
    
            # 3. Handle sensitive data replacement
            if sensitive_data:
                validated_params = self._replace_sensitive_data(validated_params, sensitive_data)
    
            # 4. Check & Inject context dependencies
            if 'browser' in parameter_names and not browser: # ... raise error ...
            # ... similar checks for page_extraction_llm, available_file_paths, context ...
    
            extra_args = {} # ... populate with browser, llm, context etc. if needed ...
    
            # 5. Call the actual function (async)
            if is_pydantic: # Check if function expects the validated model directly
                return await action.function(validated_params, **extra_args)
            else: # Or if it expects unpacked arguments
                return await action.function(**validated_params.model_dump(), **extra_args)
        except Exception as e:
            # ... error handling ...
    

    execute_action 是行动的执行引擎。当接收到行动名称 (action_name) 和原始参数字典 (params) 后,它执行以下关键步骤:

    • 查找行动:从 ActionRegistry 中找到对应的 RegisteredAction
    • 参数验证:使用该行动关联的 param_model 来验证传入的 params 字典。如果参数无效或缺失,Pydantic 会自动抛出错误,保证了类型安全和数据完整性。
    • 敏感数据处理:调用 _replace_sensitive_data 处理参数中可能存在的敏感信息占位符(如 <secret>password_key</secret>)。
    • 上下文依赖检查与注入:检查行动函数是否需要特定的上下文(如 browser 实例、page_extraction_llm 模型等)。如果需要但未提供,则抛出错误。如果提供,则准备好注入。
    • 调用行动函数:最后,异步调用 (await) 实际的行动函数。它能智能地判断函数是期望接收一个 Pydantic 模型实例作为参数,还是期望接收解包后的参数字典。同时,它将必要的上下文对象(browser, llm 等)作为关键字参数传递给函数。

    这种设计将参数验证、上下文管理和函数调用逻辑清晰地分离,使得行动函数本身可以专注于核心业务逻辑。

  2. 动态模型创建(LLM 交互):create_action_model

    def create_action_model(self, include_actions: Optional[list[str]] = None) -> Type[ActionModel]:
        fields = {
            name: (
                Optional[action.param_model], # Parameter model is optional
                Field(default=None, description=action.description), # Use action description
            )
            for name, action in self.registry.actions.items()
            # ... filter by include_actions if provided ...
        }
        # ... telemetry ...
        return create_model('ActionModel', __base__=ActionModel, **fields) # Dynamically create the model
    

    这是另一个利用 pydantic.create_model 的精彩示例,但目的不同。create_action_model 动态地构建一个全新的 Pydantic 模型,这个模型的每个字段都代表一个已注册的行动

    • 字段名:行动的名称 (name)。
    • 字段类型Optional[action.param_model]。这意味着该字段的值要么是对应行动的参数模型实例,要么是 None
    • 字段描述:行动的 description

    为什么需要这个? 这个动态创建的模型通常用作 LLM 的输出模式(Output Schema)。当 LLM 需要决定执行哪个行动时,你可以要求它返回一个符合这个动态模型结构的 JSON。例如,如果 LLM 决定点击索引为 5 的元素,它应该返回:

    {
      "click_element": {
        "index": 5
      },
      "search_google": null, // 其他行动为 null 或省略
      "input_text": null,
      // ... etc.
    }
    

    当你用这个动态创建的 ActionModel 类来解析这个 JSON 时,你会得到一个 Pydantic 对象,其中只有一个字段(click_element)被设置了值,其他都为 None。这清晰地表明了 LLM 选择的行动及其参数。

想象一下顾客(LLM)点餐。餐厅(系统)不只是给一张菜单(get_prompt_description),还会给一张特殊的点餐单(动态创建的 ActionModel)。这张点餐单上列出了所有菜品,但要求顾客只在一个菜品后面填写具体的口味要求(参数),其他的留空。
顾客(LLM)选择了“宫保鸡丁”,并在对应的栏目填写了 {"la_du": "extra_hot", "jia_huasheng": false}
服务员(代码)收到这张点餐单(解析 LLM 输出),一眼就能看出顾客点了宫保鸡丁,并且知道了具体要求。

三、 控制器与默认行动(controller.py - Controller 类)

Controller 类是对 Registry 的封装和应用,它初始化了一个 Registry 实例,并预先注册了一系列常用的浏览器自动化行动。

  1. 初始化与默认行动注册

    class Controller(Generic[Context]):
        def __init__(
            self,
            exclude_actions: list[str] = [],
            output_model: Optional[Type[BaseModel]] = None,
        ):
            self.registry = Registry[Context](exclude_actions)
    
            # Register default actions using @self.registry.action
            @self.registry.action('Search the query in Google...', param_model=SearchGoogleAction)
            async def search_google(params: SearchGoogleAction, browser: BrowserContext): ...
    
            @self.registry.action('Click element', param_model=ClickElementAction)
            async def click_element(params: ClickElementAction, browser: BrowserContext): ...
    
            @self.registry.action('Input text...', param_model=InputTextAction)
            async def input_text(params: InputTextAction, browser: BrowserContext, has_sensitive_data: bool = False): ...
    
            # Action without specific param model (uses auto-creation)
            @self.registry.action('Extract page content...')
            async def extract_content(goal: str, browser: BrowserContext, page_extraction_llm: BaseChatModel): ...
    
            # Action using NoParamsAction for actions without input args
            @self.registry.action('Go back', param_model=NoParamsAction)
            async def go_back(_: NoParamsAction, browser: BrowserContext): ...
    
            # Conditional 'done' action based on output_model
            if output_model is not None:
                # ... register done action with ExtendedOutputModel ...
            else:
                # ... register done action with DoneAction ...
        # ...
    

    Controller 在初始化时:

    • 创建了一个 Registry 实例。
    • 使用 @self.registry.action 装饰器注册了大量实用的浏览器操作,如:search_google, go_to_url, click_element, input_text, scroll_down, switch_tab, extract_content 等。
    • 它展示了如何使用预定义的参数模型(如 views.py 第二部分定义的 SearchGoogleAction, ClickElementAction)和自动创建的参数模型(如 extract_content,其参数 goal, browser, page_extraction_llm 会被自动处理)。
    • 还展示了 NoParamsAction 的用法,对于不需要任何输入参数的行动(如 go_back)非常方便。
    • 特别地,done 行动的注册是条件性的,根据是否提供了 output_model,它会注册不同参数模型的 done 函数,增加了最终结果输出的灵活性。
  2. 行动执行入口:act 方法

    async def act(
        self,
        action: ActionModel, # Input is an instance of the dynamically created model
        browser_context: BrowserContext,
        # ... other context: page_extraction_llm, sensitive_data, etc. ...
        context: Context | None = None,
    ) -> ActionResult:
        try:
            # Iterate through the fields of the input action model
            for action_name, params in action.model_dump(exclude_unset=True).items():
                if params is not None: # Find the action chosen by the LLM (the one not None)
                    # Call the registry's execution engine
                    result = await self.registry.execute_action(
                        action_name,
                        params, # Pass the parameters dictionary
                        browser=browser_context,
                        page_extraction_llm=page_extraction_llm,
                        sensitive_data=sensitive_data,
                        # ... pass other context ...
                        context=context,
                    )
                    # Standardize return type using ActionResult
                    if isinstance(result, str): return ActionResult(extracted_content=result)
                    elif isinstance(result, ActionResult): return result
                    # ... handle other potential return types or raise error ...
            return ActionResult() # No action was chosen?
        except Exception as e:
            raise e
    

    act 方法是外部调用者(通常是 Agent 的主循环)与行动控制器交互的入口点。

    • 它接收一个 ActionModel 的实例(这是 LLM 输出被解析后的结果)。
    • 它遍历这个实例的字段,找到那个值不是 None 的字段,这个字段的名称就是被选中的行动 (action_name),就是对应的参数 (params)。
    • 然后,它调用 self.registry.execute_action,将行动名称、参数以及所有必要的上下文(browser_context, page_extraction_llm 等)传递过去。
    • 最后,它将 execute_action 的结果包装成一个标准的 ActionResult 对象(定义在 browser_use.agent.views,代码未提供但可以推断其结构),方便上层调用者处理。

四、 整体工作流程

现在我们把所有部分串联起来,看看一个典型的 LLM Agent 使用这个控制器的完整流程:

  1. 初始化: 创建 Controller 实例,内部 Registry 会自动注册所有默认行动。开发者也可以使用 @controller.action 注册自定义行动。
  2. 生成 Prompt: 调用 controller.registry.get_prompt_description() 获取所有可用行动的描述,将其包含在给 LLM 的 Prompt 中,告知 LLM 它有哪些“工具”可用。
  3. 生成输出模式: 调用 controller.registry.create_action_model() 动态创建一个 Pydantic 模型,作为 LLM 输出的 JSON 模式。
  4. LLM 推理: 将包含当前状态、任务目标和行动描述的 Prompt 发送给 LLM,并要求 LLM 根据 create_action_model 生成的模式返回一个 JSON 结果,指定要执行的行动及其参数。
  5. 解析 LLM 输出: 使用步骤 3 中创建的动态模型类来解析 LLM 返回的 JSON 字符串,得到一个 ActionModel 的实例 chosen_action
  6. 执行行动: 调用 controller.act(action=chosen_action, browser_context=..., ...),传入解析后的行动对象和必要的上下文。
  7. 处理结果: act 方法内部调用 registry.execute_action,执行实际的函数,进行参数验证、上下文注入等,最终返回一个 ActionResult。Agent 的主循环接收这个结果,更新状态,并决定下一步行动(可能再次调用 LLM)。

五、总结

这种基于 Registry 和动态 Pydantic 模型的行动控制器设计带来了诸多好处:

  • 类型安全: Pydantic 在行动注册和执行时都提供了强大的类型检查和数据验证。
  • 高可扩展性: 使用装饰器可以非常容易地添加新的行动,无需修改核心 RegistryController 代码。
  • LLM 友好: 自动生成行动描述 (prompt_description) 和结构化的输出模式 (create_action_model),极大地方便了与 LLM 的集成。
  • 代码简洁: 自动参数模型创建和异步函数包装减少了样板代码。
  • 上下文管理: 清晰地定义和注入行动所需的依赖(如浏览器实例、LLM 模型)。
  • 灵活性: 支持预定义参数模型和自动生成参数模型,适应不同复杂度的行动。
  • 健壮性: 集中的错误处理和依赖检查。

我们通过深入分析 views.py, service.py, 和 controller.py 中的代码,详细了解了如何构建一个强大、灵活且类型安全的 LLM Agent 行动控制器。Action Registry 模式,结合 Pydantic 的数据验证和动态模型创建能力,以及 Python 装饰器和异步特性,为开发可靠的 Agent 工具使用(Tool Use)功能提供了一个优秀的范例。

希望这次分享能帮助大家理解这种设计模式的精髓,并在自己的项目中应用这些思想,构建出更智能、更强大的 AI 应用!我们下篇文章见。