← Back to Lab
// Backend2026.07.133 min read

多模型 Key 管理与路由:统一 resolve 与 Fernet 加密

多 LLM Key 与多模型路由的统一 resolve + ModelFactory 设计,以及用 Fernet 加密密钥、掩码返回的密钥安全实践。

如何优雅管理多个 LLM Key 与多模型路由

适用场景:需要支持多 provider、用户自带 Key、项目级/用户级模型覆盖的 AI 应用。

一、背景

项目要支持多个 provider(Anthropic / OpenAI 兼容 / 自托管 MaaS),还要让用户自带 Key、项目级覆盖用户默认。早期散落 4 处的 ChatOpenAI(...) 构造极难维护、也难审计"到底用了哪个模型"。

二、统一为 resolve + ModelFactory

所有 LLM 调用点先 resolve(user_id, project_id, scenario) 拿到具体配置,再 ModelFactory.build() 构造:

# 优先级:项目精确 → 项目全局(*) → 用户精确 → 用户全局(*) → yaml 兜底
def resolve(db, user_id, project_id, scenario):
    if (o := project_override精确命中(scenario)): return o, "project_override"
    if (o := project_global.get("*")):           return o, "project_global_override"
    if (o := user_default精确命中(scenario)):       return o, "user_default"
    if (o := user_global.get("*")):              return o, "user_global_default"
    return yaml_fallback, "yaml_fallback"

class ModelFactory:
    def build(self, cfg, temperature=None, max_tokens=None):
        if cfg.provider == "anthropic":
            return ChatAnthropic(model=cfg.model, ...)
        return ChatOpenAI(base_url=cfg.base_url, model=cfg.model, ...)

source 字段让每一步命中来源可观测——出问题时一眼知道模型从哪来。

三、踩坑:非章节任务静默落兜底

最初只有"精确匹配",导致 suggestion / outline_validate 等非章节场景无法命中用户的全局默认,悄悄用了系统兜底模型。修复就是加两级 * 通配(见上方优先级链)。

结论:所有 LLM 调用点统一纳管、零硬编码,这本身比"能不能调通"更重要。问题往往出在 fallback 策略层,而不是漏纳管。

四、密钥安全

用户 Key 用 Fernet 对称加密存 DB,明文只在内存瞬时解密;GET 接口只返回 sk-****xxxx 掩码,绝不返回明文 / 密文:

from cryptography.fernet import Fernet

key = Fernet(MASTER_KEY).encrypt(api_key.encode())      # 存储密文
# 返回时
return "sk-****" + api_key[-4:]

MASTER_ENCRYPTION_KEY 只在服务端环境变量里,日志永不打印。

五、复盘

  • 所有 LLM 调用点统一纳管,零硬编码模型名;
  • fallback 策略比"能不能调通"更该被重视,记得给"全局默认"留通配;
  • 密钥永远只进不出(掩码),加密存储、瞬时解密。

这套架构让"换模型 / 加 provider / 用户自带 Key"都变成配置级操作,而不是改代码。