跳到主要内容

04 · 多 LLM 接入与 MCP 工具

本项目的两个“接入层”巧思都在 src/utils/ 里:用一个工厂函数兼容十几家 LLM、用一段递归把任意 MCP 工具变成 agent 能调的动作。两者独立,分两半讲。

A. 多 LLM:一个工厂函数,一打厂商

A.1 要解决的小问题

UI 上能下拉切 OpenAI / Anthropic / Google / DeepSeek / Ollama / Grok / Mistral / Azure / 阿里 / 月之暗面 / IBM / SiliconFlow / ModelScope / Unbound 等。每家的 LangChain 类、默认 base_url、key 环境变量名都不同。需要一个统一入口把这些差异抹平。

A.2 思路:provider 名 → 分支 → 对应 ChatModel

get_llm_model(provider, **kwargs) 就是这个统一入口(src/utils/llm_provider.py:152)。它先做统一的 key 处理,再按 provider 进 if/elif 造对象。

统一 key 处理(除 ollama/bedrock 外都要 key): 优先用 UI 传入,否则从 {PROVIDER}_API_KEY 环境变量取,都没有就抛带 emoji 的友好错误(llm_provider.py:159-166):

# 真实逻辑(节选),llm_provider.py:159-166
if provider not in ["ollama", "bedrock"]:
env_var = f"{provider.upper()}_API_KEY"
api_key = kwargs.get("api_key", "") or os.getenv(env_var, "")
if not api_key:
raise ValueError(f"💥 {provider_display} API key not found! ...")
kwargs["api_key"] = api_key

分支造对象(以 anthropic 为例): 多数厂商其实是 OpenAI 兼容,直接复用 ChatOpenAI 只换 base_url;少数有专用类。anthropic 用专用 ChatAnthropic,带默认模型与 base_url(llm_provider.py:168-179):

# 真实逻辑(节选),llm_provider.py:174-179
return ChatAnthropic(
model=kwargs.get("model_name", "claude-3-5-sonnet-20241022"),
temperature=kwargs.get("temperature", 0.0),
base_url=base_url,
api_key=api_key,
)

“OpenAI 兼容”大家族:grok、deepseek(普通)、alibaba、moonshot、unbound、siliconflow、modelscope 全是 ChatOpenAI + 各自的 base_url/默认模型(llm_provider.py:208-353)。可选模型清单集中在 src/utils/config.py:model_names

A.3 巧妙处:为 DeepSeek-R1 这类“带思维链”模型做适配

推理型模型会额外吐一段 reasoning_content(思考过程),标准 LangChain 类接不住。本项目造了两个子类专门处理:

  • DeepSeekR1ChatOpenAI:绕过 LangChain,直接用 OpenAI SDK 取 reasoning_contentcontent 两段,塞进 AIMessage(llm_provider.py:55-114)。仅当模型是 deepseek-reasoner 时启用(llm_provider.py:226-232)。
  • DeepSeekR1ChatOllama:本地 Ollama 版,从输出里</think>出思考与正文两段(llm_provider.py:117-149):
# 真实逻辑(节选),llm_provider.py:128-133
org_content = org_ai_message.content
reasoning_content = org_content.split("</think>")[0].replace("<think>", "") # 思考段
content = org_content.split("</think>")[1] # 正文段

这解释了为什么 02 章 _set_tool_calling_method 要为“无工具支持模型”兜底——这些推理模型常没有原生 function calling。

B. MCP:把任意外部工具变成 agent 的动作

MCP(Model Context Protocol,模型上下文协议)是一种让 agent 接外部工具的标准。本项目允许在 UI 里贴一段 MCP 服务器配置,把外部工具拉进来当动作用。

B.1 连接:一个 LangChain 适配器

setup_mcp_client_and_tools 很薄:把配置(支持 mcpServers 包一层)喂给 MultiServerMCPClient,__aenter__ 连上即可(src/utils/mcp_client.py:17-43)。控制器侧由 setup_mcp_clientregister_mcp_tools 接手(回看 02 章 custom_controller.py:154-178)。

B.2 难点:工具的参数 Schema ≠ agent 动作要的 Pydantic 模型

browser-use 的动作注册表要求每个动作配一个 Pydantic 参数模型(继承 ActionModel)。但 MCP 工具给的是 JSON Schema。所以要做一次“JSON Schema → Pydantic 模型”的转换,这就是 create_tool_param_model + resolve_type 干的事。

MCP 工具(带 args_schema JSON)
│ create_tool_param_model mcp_client.py:46
│ 遍历 properties → 每个字段调 resolve_type 求 Python 类型
│ 带上 required / default / 约束(ge/le/min_length/pattern...)
▼ resolve_type 递归处理 mcp_client.py:134
│ string/int/.. 基础映射;format→datetime/uuid;enum→动态 Enum
│ array→List[子类型];object→嵌套 model;oneOf/anyOf→Union;allOf→合并

create_model(__base__=ActionModel, **字段) → 一个新的 Pydantic 动作参数模型

核心一句:用 Pydantic 的 create_model 动态造类,基类指定为上游的 ActionModel(mcp_client.py:94-98):

# 真实逻辑(节选),mcp_client.py:94-98
return create_model(
f'{tool_name}_parameters',
__base__=ActionModel, # 让它符合 browser-use 动作注册表的要求
**params,
)

resolve_type 几乎覆盖了 JSON Schema 全谱(mcp_client.py:134-254):$ref 退化为 Any;format 映射到 datetime/date/uuid/bytes 等;enum 现造一个安全命名的 Enum;array/object/oneOf/anyOf/allOf 递归处理;["string","null"] 这种联合类型转 Optional

B.3 注册:让 MCP 工具进动作表

转好的模型在 register_mcp_tools 里以 mcp.{server}.{tool} 为名塞进注册表(custom_controller.py:160-174)。执行时再由 02 章那条 act 里的 if action_name.startswith("mcp") 旁路调用——闭环。

巧妙之处

  • OpenAI 兼容当公分母:大部分厂商复用 ChatOpenAI 只换 base_url,工厂因此短小(get_llm_model 多个 elif)。
  • 推理模型显式拆思维链:用专用子类按 </think>reasoning_content 分离思考与答案(DeepSeekR1ChatOllama/DeepSeekR1ChatOpenAI)。
  • Schema 反推 Pydantic:一段递归把任意 MCP 工具变成合规动作模型,无需为每个工具手写 schema(create_tool_param_model, resolve_type)。
  • 动态基类 create_model:用 __base__=ActionModel 让生成类天然满足上游注册表契约(mcp_client.py:96)。

边界与坑

  • mcp_client.py 同时 from pydantic import BaseModel, Fieldfrom pydantic.v1 import BaseModel, Field(mcp_client.py:11-12),后者覆盖前者——v1/v2 混用是脆点。
  • $ref 直接退化成 Any(mcp_client.py:138-140),引用型复杂 schema 会丢类型信息。
  • 工厂里硬编码了一堆默认模型名(如 anthropic 默认 claude-3-5-sonnet-20241022),会随时间过时;真实可选项以 UI 下拉(config.model_names)为准。

代码地图

主题文件符号
LLM 工厂src/utils/llm_provider.pyget_llm_model
推理模型适配src/utils/llm_provider.pyDeepSeekR1ChatOpenAI, DeepSeekR1ChatOllama
可选模型清单src/utils/config.pymodel_names, PROVIDER_DISPLAY_NAMES
MCP 连接src/utils/mcp_client.pysetup_mcp_client_and_tools
Schema→Pydanticsrc/utils/mcp_client.pycreate_tool_param_model, resolve_type
MCP 动作注册src/controller/custom_controller.pyregister_mcp_tools, act