先问一个问题:为什么接入新 provider 这么容易?
如果你去看 core/llm.py,会发现一件有趣的事:代码里已经支持了 OpenAI、DeepSeek、Qwen、Kimi、Zhipu、SiliconFlow、Ollama、vLLM 等十家不同的服务,但整个文件里没有十段风格迥异的适配代码。
秘密在 PROVIDER_PROFILES 这张表里。
结论先说
MyCodeAgent 的 LLM 层用**表驱动(table-driven)**的方式管理 provider。接入一个新 provider 只需要三步:
| 步骤 | 做什么 |
|---|---|
| 1. 在表里加一行 | PROVIDER_PROFILES 里加一个 profile |
| 2. 扩展类型注解 | SUPPORTED_PROVIDERS Literal 里加名字 |
| 3. 配置环境变量 | .env.example 里加新 provider 的变量说明 |
不需要改调用链,不需要写新类,不需要 if-else 分支。
一、PROVIDER_PROFILES:provider 路由的核心
先看 DeepSeek 的 profile,它是所有 profile 里最典型的:
python
# core/llm.py
PROVIDER_PROFILES = {
"deepseek": {
"key_envs": ("DEEPSEEK_API_KEY", "LLM_API_KEY"), # 按优先级查 API key
"detect_envs": ("DEEPSEEK_API_KEY",), # 自动探测时检查哪些变量
"base_url_envs": ("LLM_BASE_URL",), # 从哪个环境变量读端点
"base_url": "https://api.deepseek.com", # 未配置时的默认端点
"model": "deepseek-chat", # 默认模型名
"url_markers": ("api.deepseek.com",), # 从 base_url 反推 provider
},
...
}
这五个字段控制了 provider 路由的全部逻辑:
key_envs :查 API key 的优先级列表。框架从左到右依次查环境变量,第一个有值的就用。这让用户既可以用专用的 DEEPSEEK_API_KEY,也可以用通用的 LLM_API_KEY。
detect_envs :自动探测时用哪些变量。当用户没有指定 provider,框架会扫描所有 profile 的 detect_envs,看哪家的变量在环境里设置了值,就自动选那家。如果有多家同时命中,会报错要求用户显式指定(避免歧义)。
base_url :默认端点。用户没有配置 LLM_BASE_URL 时用这个值。
url_markers :从用户配置的 base_url 字符串里反推 provider。比如用户设置了 LLM_BASE_URL=https://api.deepseek.com/v1,框架检查这个 URL 包含 "api.deepseek.com",就自动把 provider 识别为 deepseek。
model :默认模型。用户没有指定 LLM_MODEL_ID 时用这个值。
二、provider 解析的完整优先级
搞清楚表结构之后,来看框架是怎么确定最终使用哪个 provider 的:
python
def _resolve_provider(self, provider, api_key, base_url):
# 优先级 1:代码里显式传入的 provider 参数
if provider:
return self._normalize_provider(provider)
# 优先级 2:环境变量 LLM_PROVIDER
env_provider = self._get_env("LLM_PROVIDER")
if env_provider:
return self._normalize_provider(env_provider)
# 优先级 3:自动探测(扫描 detect_envs,或从 base_url 字符串反推)
return self._auto_detect_provider(api_key, base_url)
三级优先级,层层兜底。对于用户来说,最常见的配置方式是在 .env 里设置:
bash
LLM_PROVIDER=deepseek
DEEPSEEK_API_KEY=sk-xxxxx
LLM_MODEL_ID=deepseek-chat
或者更省事,让框架自动探测:
bash
DEEPSEEK_API_KEY=sk-xxxxx # 只设这一个,框架会自动识别是 DeepSeek
三、接入一个新 provider:以"SomeNewAI"为例
假设市场上出现了一个叫 "SomeNewAI" 的新服务,它的 API 接口 OpenAI 兼容(这是现在几乎所有新模型服务商的标准),端点是 https://api.someneai.com/v1。
第一步:在 PROVIDER_PROFILES 里加一行
python
# core/llm.py --- PROVIDER_PROFILES 里加入:
"someneai": {
"key_envs": ("SOMENEAI_API_KEY", "LLM_API_KEY"),
"detect_envs": ("SOMENEAI_API_KEY",),
"base_url_envs": ("LLM_BASE_URL",),
"base_url": "https://api.someneai.com/v1",
"model": "someneai-pro",
"url_markers": ("api.someneai.com",),
},
第二步:扩展类型注解
python
# core/llm.py --- SUPPORTED_PROVIDERS Literal 里加上新名字
SUPPORTED_PROVIDERS = Literal[
"openai",
"deepseek",
...
"someneai", # 新增
"auto",
]
第三步:更新 .env.example
bash
# .env.example --- 在 LLM 配置区加上新 provider 的说明
# SomeNewAI
# SOMENEAI_API_KEY=your-key-here
完成。现在用户可以这样使用:
bash
LLM_PROVIDER=someneai
SOMENEAI_API_KEY=sk-xxxxx
LLM_MODEL_ID=someneai-pro
或者在启动命令里覆盖:
bash
uv run python main.py --provider someneai --api-key sk-xxxxx --model someneai-pro
四、如果新 provider 有特殊怪癖怎么办
OpenAI 兼容接口是现在的主流,但不同服务商在实现上多少有些差异。core/llm.py 里已经有一些针对特定服务的"怪癖处理":
python
# core/llm.py --- 构建请求时的怪癖处理
def _build_request(self, messages, tools, ...):
# 有些 provider 不支持 temperature=0,会报错
if self.provider in ("zhipu",):
temperature = max(temperature, 0.01)
# 有些 provider 不支持多个 system message,需要合并
if self.provider in ("kimi", "moonshot"):
messages = self._merge_system_messages(messages)
# 有些 provider 不支持 tool_choice="auto"
if self.provider in ("some_provider",):
request.pop("tool_choice", None)
如果新 provider 也有类似的怪癖,在 _build_request() 里加一个针对它的条件分支即可。
框架把"路由"(去哪家?用什么 key?什么端点?)和"怪癖处理"(请求格式要不要调整?)分开了:路由在表里,怪癖在 _build_request() 里。
五、本地模型:Ollama 的接入方式
对于完全本地运行的模型(比如 Ollama),接入方式一样,只是 profile 里的 base_url 指向本地:
python
"ollama": {
"key_envs": ("OLLAMA_API_KEY", "LLM_API_KEY"),
"detect_envs": ("OLLAMA_API_KEY", "OLLAMA_HOST"),
"base_url_envs": ("OLLAMA_HOST", "LLM_BASE_URL"),
"base_url": "http://localhost:11434/v1",
"model": "llama3.2",
"default_key": "ollama", # API key 默认值(Ollama 不验证 key,随便填)
"url_markers": ("ollama",),
},
用户只需要:
bash
# 先启动 Ollama 服务
ollama serve
# 然后在 .env 里配置
LLM_PROVIDER=ollama
LLM_MODEL_ID=llama3.2
# OLLAMA_API_KEY 可以不填,框架会用 "ollama" 作为默认值
设计亮点
1. 表驱动,不是继承
每家 provider 的差异只在数据层(那张表),而不是代码层(一个 class per provider)。新增 provider 是加数据行,不是加代码类。这让维护成本非常低------所有 provider 的路由逻辑集中在一处,一眼能看出区别。
2. 三级 API key 查找
key_envs 的列表设计允许"专用 key 优先,通用 key 兜底"。DEEPSEEK_API_KEY 比 LLM_API_KEY 优先级高,这让有多家 provider 的用户可以为每家分别配密钥,而不是每次切换 provider 都要改 LLM_API_KEY。
3. 自动探测省去显式声明
扫描 detect_envs 来自动识别 provider,让用户只需要设置 API key 而不用同时设置 LLM_PROVIDER。对新手友好,减少了必填配置项的数量。
小结
| 设计选择 | 方案 | 工程价值 |
|---|---|---|
| provider 路由 | 表驱动(PROVIDER_PROFILES) | 加 provider 加数据行,不改代码逻辑 |
| API key 查找 | 多级 key_envs | 专用 key 优先,通用 key 兜底 |
| provider 识别 | 三级优先级(参数 → 环境变量 → 自动探测) | 灵活,最少配置可用 |
| 怪癖处理 | _build_request() 里按 provider 分支 | 路由逻辑与怪癖逻辑分离,各自演化 |
下一篇讲 Skill------用 Markdown 定义一段可复用的"专家行为",这是 MyCodeAgent 里最轻量的扩展方式。
关于本系列的源码
本系列所有分析均基于开源项目 MyCodeAgent。
源码里已经按照本系列文章的讲解顺序,在关键位置加入了配套注释------读文章时可以对照代码,也可以直接克隆下来自己跑、改、扩展,基于它开发你自己的 agent。
bash
git clone https://github.com/chendongqi/MyCodeAgent
cd MyCodeAgent
cp .env.example .env # 填入你的 LLM API key
uv sync
uv run python main.py
欢迎访问 PrimeSkills ------ 一个精心策划的 AI Agent 与技能市场,所有内容均经过真实企业级工作流验证。没有噱头,只有真正有效的东西。
更多实用知识和有趣产品,欢迎访问我的个人主页