大家好!我们今天继续拆解 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 模型。
-
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 这个行动叫什么、需要哪些参数以及参数的结构。 -
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_index和set_index方法则提供了方便的操作接口,特别是针对网页自动化中常见的元素索引操作。 -
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 类是整个行动控制器的核心引擎,负责行动的注册和执行逻辑。
-
行动注册:
@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 模型来匹配这些参数!这极大地简化了注册过程,开发者只需写好函数和类型提示即可。它还会智能地排除像browser、page_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)上。
-
行动执行:
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等)作为关键字参数传递给函数。
这种设计将参数验证、上下文管理和函数调用逻辑清晰地分离,使得行动函数本身可以专注于核心业务逻辑。
- 查找行动:从
-
动态模型创建(LLM 交互):
create_action_modeldef 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 实例,并预先注册了一系列常用的浏览器自动化行动。
-
初始化与默认行动注册
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函数,增加了最终结果输出的灵活性。
- 创建了一个
-
行动执行入口:
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 eact方法是外部调用者(通常是 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 使用这个控制器的完整流程:
- 初始化: 创建
Controller实例,内部Registry会自动注册所有默认行动。开发者也可以使用@controller.action注册自定义行动。 - 生成 Prompt: 调用
controller.registry.get_prompt_description()获取所有可用行动的描述,将其包含在给 LLM 的 Prompt 中,告知 LLM 它有哪些“工具”可用。 - 生成输出模式: 调用
controller.registry.create_action_model()动态创建一个 Pydantic 模型,作为 LLM 输出的 JSON 模式。 - LLM 推理: 将包含当前状态、任务目标和行动描述的 Prompt 发送给 LLM,并要求 LLM 根据
create_action_model生成的模式返回一个 JSON 结果,指定要执行的行动及其参数。 - 解析 LLM 输出: 使用步骤 3 中创建的动态模型类来解析 LLM 返回的 JSON 字符串,得到一个
ActionModel的实例chosen_action。 - 执行行动: 调用
controller.act(action=chosen_action, browser_context=..., ...),传入解析后的行动对象和必要的上下文。 - 处理结果:
act方法内部调用registry.execute_action,执行实际的函数,进行参数验证、上下文注入等,最终返回一个ActionResult。Agent 的主循环接收这个结果,更新状态,并决定下一步行动(可能再次调用 LLM)。
五、总结
这种基于 Registry 和动态 Pydantic 模型的行动控制器设计带来了诸多好处:
- 类型安全: Pydantic 在行动注册和执行时都提供了强大的类型检查和数据验证。
- 高可扩展性: 使用装饰器可以非常容易地添加新的行动,无需修改核心
Registry或Controller代码。 - LLM 友好: 自动生成行动描述 (
prompt_description) 和结构化的输出模式 (create_action_model),极大地方便了与 LLM 的集成。 - 代码简洁: 自动参数模型创建和异步函数包装减少了样板代码。
- 上下文管理: 清晰地定义和注入行动所需的依赖(如浏览器实例、LLM 模型)。
- 灵活性: 支持预定义参数模型和自动生成参数模型,适应不同复杂度的行动。
- 健壮性: 集中的错误处理和依赖检查。
我们通过深入分析 views.py, service.py, 和 controller.py 中的代码,详细了解了如何构建一个强大、灵活且类型安全的 LLM Agent 行动控制器。Action Registry 模式,结合 Pydantic 的数据验证和动态模型创建能力,以及 Python 装饰器和异步特性,为开发可靠的 Agent 工具使用(Tool Use)功能提供了一个优秀的范例。
希望这次分享能帮助大家理解这种设计模式的精髓,并在自己的项目中应用这些思想,构建出更智能、更强大的 AI 应用!我们下篇文章见。