数据截至 (上游 commit 61962296c38a)
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_content与content两段,塞进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_client → register_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, Field又from 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.py | get_llm_model |
| 推理模型适配 | src/utils/llm_provider.py | DeepSeekR1ChatOpenAI, DeepSeekR1ChatOllama |
| 可选模型清单 | src/utils/config.py | model_names, PROVIDER_DISPLAY_NAMES |
| MCP 连接 | src/utils/mcp_client.py | setup_mcp_client_and_tools |
| Schema→Pydantic | src/utils/mcp_client.py | create_tool_param_model, resolve_type |
| MCP 动作注册 | src/controller/custom_controller.py | register_mcp_tools, act |