模型管理:怎么拿到一个"能调的模型"、还带多 key 轮询
Day04 第④站 Runner 里那句"调模型",今天正式展开。主角是两个文件——core/model_manager.py 和 core/provider_manager.py。弄清三件事:①怎么从"租户 + 供应商 + 模型名"拿到一个能 invoke_llm() 的 ModelInstance;②这个 invoke 底层怎么走;③配了多把 API Key 时,"负载均衡"是怎么用 Redis 做轮询、失败还能自动冷却切换的。这是所有 LLM 调用的总闸门。
ModelManager 像翻译中介公司,ModelInstance 是派给你的那位翻译。你说"我要一位懂法语的翻译"(要 OpenAI 的 gpt-4o),中介查档案、核对你的付费凭证,派来一位随时能开工的翻译(ModelInstance)。你只管对翻译说话(invoke_llm),至于他背后属于哪家、怎么结算,你不用操心。类比二:负载均衡多 key 轮询像银行多个窗口叫号——你配了 3 把 API Key,每次请求发一个递增号,取号 % 3 决定用哪把;某把 key 触发限流就"暂时关闭这个窗口"(冷却),叫号自动跳过它。这样单 key 的额度上限就被摊薄了。痛点:OpenAI、Claude、通义……几十家怎么统一调
if provider == "openai": openai.chat(...) elif provider == "anthropic": ...,那简直是灾难。而且还有现实需求:同一个模型我配了多把 API Key 想分摊额度;某把 key 挂了要能自动切换;租户配置改了缓存要能失效。业务层(Day04 的 Runner)显然不该操心这些。invoke_llm() 调)+ ProviderManager(配置中心:管租户配了哪些供应商、凭据、默认模型)+ LBModelManager(轮询器:多 key 的负载均衡与故障冷却)。业务层只跟 ModelInstance 打交道,脏活累活全被这三层挡住。这是"门面模式 + 策略解耦"的教科书用法。三个主角:谁管什么
core/model_manager.py 里有三个关键类,各管一摊:
| 类 | 位置 | 职责 |
|---|---|---|
ModelInstance | model_manager.py:35 | 一个"随时能开工的模型",对外提供 invoke_llm/invoke_text_embedding/... |
ModelManager | model_manager.py:445 | 工厂/中介:按"租户+供应商+模型名"产出 ModelInstance |
LBModelManager | model_manager.py:566 | 负载均衡:多个凭据配置轮流用、失败冷却 |
还有一个跨文件的搭档 ProviderManager(provider_manager.py:557)——它是"配置中心",ModelManager 靠它拿到"某供应商在某租户下的完整配置"。
ModelManager(中介)→ 借助 ProviderManager(查档案)→ 产出 ModelInstance(翻译)→ 你调它的 invoke_llm → 如果配了多 key,内部再交给 LBModelManager(叫号)挑一把。后面几节就是把这条链一环环拆开。ModelInstance:怎么拿到一个能调的模型
入口是 ModelManager.get_model_instance(api/core/model_manager.py:475):
# api/core/model_manager.py:475
def get_model_instance(self, tenant_id, provider, model_type, model) -> ModelInstance:
if not provider:
return self.get_default_model_instance(tenant_id, model_type) # 没指定就用默认模型
provider_model_bundle = self._provider_manager.get_provider_model_bundle( # ★向配置中心要"包"
tenant_id=tenant_id, provider=provider, model_type=model_type
)
return ModelInstance(provider_model_bundle, model) # ★用这个包造实例
造实例时,ModelInstance.__init__(api/core/model_manager.py:40)做了几件关键准备:
# api/core/model_manager.py:40
def __init__(self, provider_model_bundle, model, credentials=None):
self.provider_model_bundle = provider_model_bundle
self.model_name = model
self.provider = provider_model_bundle.configuration.provider.provider
if credentials is None:
credentials = self._fetch_credentials_from_bundle(provider_model_bundle, model) # ① 取凭据
self.credentials = credentials
self.model_type_instance = self.provider_model_bundle.model_type_instance # ② 拿到"模型类型实例"
self.load_balancing_manager = self._get_load_balancing_manager( # ③ 若配了多 key,建轮询器
configuration=provider_model_bundle.configuration,
model_type=provider_model_bundle.model_type_instance.model_type,
model=model, credentials=self.credentials,
)
provider_model_bundle一个"打包好的配置束":包含"这个供应商在这个租户下的完整配置"和"模型类型实例"。由 ProviderManager.get_provider_model_bundle 产出(L05 细看)。_fetch_credentials_from_bundle从配置束里取出当前该用的凭据(API Key 等)。取不到就抛 ProviderTokenNotInitError——所以"没配 key"的报错就是这里来的。model_type_instance★真正"会调某类模型"的对象(如 LargeLanguageModel)。invoke_llm 最终就是调它的 .invoke()。不同模型类型(LLM/Embedding/Rerank/TTS)有各自的实例。_get_load_balancing_manager★检查这个模型有没有配"负载均衡"(多组凭据)。配了就建一个 LBModelManager(api/core/model_manager.py:83),没配就是 None(单 key,直调)。L06 展开。ModelManager 还有个便捷入口 for_tenant(tenant_id, user_id)(api/core/model_manager.py:467),一行就能给某租户造出管理器。Day04 的 Runner 拿模型走的就是类似路径。invoke_llm:一次真正的模型调用
拿到 ModelInstance 后,业务层调 invoke_llm(api/core/model_manager.py:154):
# api/core/model_manager.py:154
def invoke_llm(self, prompt_messages, model_parameters=None, tools=None,
stop=None, stream=True, callbacks=None, request_metadata=None):
if not isinstance(self.model_type_instance, LargeLanguageModel):
raise Exception("Model type instance is not LargeLanguageModel") # 类型防呆
return cast(Union[LLMResult, Generator], self._round_robin_invoke( # ★统一走轮询包装
self.model_type_instance.invoke, # 真正要调的函数
model=self.model_name, credentials=self.credentials,
prompt_messages=list(prompt_messages), model_parameters=model_parameters,
tools=list(tools) if tools else None, stop=list(stop) if stop else None,
stream=stream, callbacks=callbacks, request_metadata=request_metadata,
))
类型防呆先确认 model_type_instance 真是个 LLM(不是 embedding/tts 之类)。防止把嵌入模型当聊天模型误用。prompt_messages入参是 Day04 里 Runner organize_prompt_messages 拼出来的 PromptMessage 列表——这就是 Day04 和 Day05 的接头处。self._round_robin_invoke(func, ...)★不直接调 model_type_instance.invoke,而是把它当参数传给 _round_robin_invoke 包一层。无论单 key 还是多 key,统一从这个包装走——这样负载均衡逻辑只写一份,被 invoke_llm/embedding/rerank 等所有方法复用(源码里十来处都调它)。stream=True默认流式:返回一个 Generator,模型每吐一段就产出一块——正好喂给 Day04 第⑤站的 task_pipeline 转成 SSE。包装函数 _round_robin_invoke(api/core/model_manager.py:378)的骨架——注意"没配负载均衡就直调"这个快路径:
# api/core/model_manager.py:378
def _round_robin_invoke(self, function, *args, **kwargs):
if not self.load_balancing_manager:
return function(*args, **kwargs) # ★单 key:直接调,零开销
while True:
lb_config = self.load_balancing_manager.fetch_next() # ① 轮询取下一个凭据
if not lb_config:
raise last_exception or ProviderTokenNotInitError(...) # 全冷却了 → 报错
try:
kwargs["credentials"] = lb_config.credentials # ② 换上这把凭据
return function(*args, **kwargs) # ③ 调!成功就返回
except InvokeRateLimitError as e:
self.load_balancing_manager.cooldown(lb_config, expire=60) # 限流 → 冷却 60s
last_exception = e; continue # 换下一把重试
except (InvokeAuthorizationError, InvokeConnectionError) as e:
self.load_balancing_manager.cooldown(lb_config, expire=10) # 认证/连接错 → 冷却 10s
last_exception = e; continue
load_balancing_manager is None)就直调、零额外开销;配了才进 while 轮询循环。常见情况(单 key)不为不常见情况(多 key)付代价——这是好 API 设计的普遍原则。而多 key 时,不同错误给不同冷却时长(限流冷 60s、认证/连接冷 10s),体现了对失败类型的精细处理。ProviderManager:配置和凭据从哪来
L03 那个"配置束"由 ProviderManager.get_provider_model_bundle(api/core/provider_manager.py:814)产出:
# api/core/provider_manager.py:814
def get_provider_model_bundle(self, tenant_id, provider, model_type) -> ProviderModelBundle:
provider_configurations = self.get_configurations(tenant_id) # ① 取该租户所有供应商配置
provider_configuration = provider_configurations.get(provider) # ② 挑出目标供应商
if not provider_configuration:
raise ValueError(f"Provider {provider} does not exist.")
model_type_instance = provider_configuration.get_model_type_instance(model_type) # ③ 取该类型实例
return ProviderModelBundle(
configuration=provider_configuration,
model_type_instance=model_type_instance,
)
get_configurations(tenant_id)核心方法(api/core/provider_manager.py:601):把一个租户配置过的所有供应商(系统内置的 + 自定义的)、凭据、模型设置、负载均衡配置,全部从数据库+缓存里加载、解密、组装成 ProviderConfigurations。这是整个模型体系的"数据源头"。provider_configurations.get(provider)从这一大堆里挑出你要的那家(如 "openai")。没配过就报"provider does not exist"。get_model_type_instance(model_type)拿到"会调这类模型的实例"(LLM/Embedding/…),装进 bundle 一起返回给 ModelInstance 用。ProviderManager 还管默认模型get_default_model(api/core/provider_manager.py:836):没指定用哪个模型时,返回租户设的默认模型;连默认都没有就自动挑第一个可用的并记下来。这就是 L03 里 get_default_model_instance 的底气。get_configurations 每次都查库+解密会很慢(一次对话可能调好几次模型)。所以 ProviderManager 里大量用缓存(文件顶部一堆 _XxxCacheEntry 缓存条目类)。但缓存有代价:改了 API Key 后可能读到旧凭据。源码的应对是提供 invalidate_configurations_cache 主动失效,并且 ModelManager 的凭据缓存默认关闭(enable_credentials_cache=False,见 api/core/model_manager.py:445 的类注释),推荐"每个请求/每次运行用新的 manager"。用缓存换速度,用失效机制兜正确性——这是所有配置中心都要做的权衡。负载均衡:多 key 怎么用 Redis 轮询
L04 里 fetch_next() 是怎么"叫号"的?看 LBModelManager.fetch_next(api/core/model_manager.py:599):
# api/core/model_manager.py:599
def fetch_next(self):
"""Strategy: Round Robin"""
cache_key = "model_lb_index:{}:{}:{}:{}".format( # ① 每个"租户+供应商+类型+模型"一个计数器
self._tenant_id, self._provider, self._model_type.value, self._model)
max_index = len(self._load_balancing_configs)
while True:
current_index = redis_client.incr(cache_key) # ② ★Redis 自增:拿一个全局递增号
current_index = cast(int, current_index)
if current_index >= 10000000: # 防止无限增大,到顶归 1
current_index = 1; redis_client.set(cache_key, current_index)
redis_client.expire(cache_key, 3600)
if current_index > max_index:
current_index = current_index % max_index # ③ ★对配置数取模 → 轮询
real_index = current_index - 1
config = self._load_balancing_configs[real_index] # ④ 选中这把凭据
if self.in_cooldown(config): # ⑤ 若正在冷却 → 跳过换下一个
...
continue
return config # ⑥ 返回可用凭据
redis_client.incr(cache_key)★为什么用 Redis?因为 Dify 是多进程(多个 gunicorn worker)部署的。若用进程内变量计数,每个进程各转各的、无法真正均衡。Redis 的 incr 是原子操作、全局共享——所有进程共用一个递增号,才能真正雨露均沾。current_index % max_index递增号对"配了几把 key"取模,得到 0/1/2/0/1/2… 的循环——这就是 Round Robin(轮流)的核心。in_cooldown(config)L04 里某把 key 触发限流会被 cooldown 打上冷却标记(也存 Redis,见 api/core/model_manager.py:674 的 cooldown 与 api/core/model_manager.py:687 的 in_cooldown)。轮询时遇到冷却中的就跳过,换下一把。全部冷却 → 返回 None如果所有 key 都在冷却,fetch_next 返回 None,回到 L04 就会抛出最后一次的异常——诚实报错,而不是死循环。串起来 + 今日小结
ChatAppRunner 要调模型 → ModelManager.get_model_instance(tenant_id, "openai", LLM, "gpt-4o") → 内部 ProviderManager.get_provider_model_bundle 加载该租户 openai 配置(含你填的 API Key)→ 造出 ModelInstance(发现你配了 2 把 key,建了 LBModelManager)→ Runner 调 instance.invoke_llm(prompt_messages, stream=True) → _round_robin_invoke 发现有负载均衡 → fetch_next() 用 Redis 递增号 % 2 选中 key#0 → 用 key#0 的凭据真正请求 OpenAI → 返回流式 Generator → 一段段喂回 Day04 第⑤站的 task_pipeline → SSE 流给前端。一句"调模型",背后是这一整条链。👶 小白:ModelInstance、model_type_instance、provider_model_bundle 名字好像,到底谁是谁?
👨🏫 老师:一句话分清——provider_model_bundle 是"原料包"(供应商配置 + 类型实例,由 ProviderManager 产出);ModelInstance 是"成品翻译"(业务层拿它调 invoke_llm);model_type_instance 是翻译的"专业技能对象"(真正实现"怎么调某类模型",如 LargeLanguageModel)。ModelInstance 内部持有后两者:造它时用原料包,调它时委托给技能对象。分清"谁被谁持有"就不乱了。
🧠 今天你应该能回答
- 业务层怎么拿到一个能调的模型?(
ModelManager.get_model_instance→ModelInstance) ProviderManager负责什么?(配置中心:加载/解密租户的供应商配置、凭据、默认模型)- 为什么 invoke_llm 都走
_round_robin_invoke?(统一负载均衡逻辑,单 key 走直调快路径) - 负载均衡用什么做轮询计数?为什么?(Redis
incr,因为多进程要全局共享) - 某把 key 限流了会怎样?(
cooldown冷却 60s,轮询自动跳过换下一把) - 改了 API Key 却没生效,可能什么原因?(配置/凭据缓存,需失效;建议每请求用新 manager)
✋ 10 分钟动手
cd /Users/bitmart/work/codes/github/AI_WORK/dify
# 1. 三个主角
sed -n '35,60p' api/core/model_manager.py # ModelInstance
sed -n '445,490p' api/core/model_manager.py # ModelManager.get_model_instance
sed -n '154,178p' api/core/model_manager.py # invoke_llm
# 2. 负载均衡轮询
sed -n '378,428p' api/core/model_manager.py # _round_robin_invoke
sed -n '599,650p' api/core/model_manager.py # fetch_next(Redis incr 轮询)
# 3. 配置中心
sed -n '814,835p' api/core/provider_manager.py # get_provider_model_bundle
grep -n "def get_configurations\|def invalidate" api/core/provider_manager.py
provider_model_bundle 当"黑箱原料包"用了。明天拆开它——ProviderConfiguration 到底装了什么、凭据是怎么加解密存库的、以及 LLM / Embedding / Rerank / TTS 这些"模型类型实体"的类型体系。继续深入模型运行时。