技术文档:模型工厂 `llm_provider` / `llm_use` 分层设计

技术文档:模型工厂 llm_provider / llm_use 分层设计

适用口径:Python 3.11+ / langchain-core 0.3.x。 本文在原始设计稿基础上做了 4 处修正 ,其中第 1 处会让 llm_use 通道的伪代码直接 AttributeError,详见第 6 节。


1 概述

模型工厂提供两条模型实例构建通道:

通道 定位
llm_provider 主干道,预注册适配器
llm_use 逃生门,反射动态加载

绝大多数场景使用 llm_provider;llm_use 作为兜底扩展入口,仅用于标准化体系无法覆盖的特殊场景。两条通道可以共存,遵循统一的显式意图优先原则。


2 字段定义

字段 类型 说明
llm_provider 字符串枚举 预注册供应商标识(如 openai、vllm、ollama)。系统启动时,provider 名称与适配器实例预先注册到全局注册表 ADAPTER_REGISTRY
llm_use 类全路径字符串 格式 module.path:ClassName,用于动态反射导入指定适配器 / 模型类

3 优先级规则

显式意图优先级高于默认主干道:llm_use > llm_provider

  1. 配置中 llm_use 非空:优先走反射通道,忽略 llm_provider。使用者显式声明要加载指定类,主动承担反射带来的代价。
  2. 配置中 llm_use 为空:自动回落至主干道,使用 llm_provider 查表获取预注册适配器。

顶层设计原则:优先级给显式意图,默认值给护栏。


4 两条通道定位与适用场景

4.1 llm_provider:主干道(推荐默认使用)

底层实现:适配器 + 静态注册表,无 if-else 分支。

适用场景:

  1. 模型兼容 OpenAI HTTP 协议(vLLM、Ollama、各类兼容网关),仅需修改 base_url、model_name、密钥
  2. 该供应商为系统长期维护的标准模型,需要统一做限流、配额、日志标签、权限、前端下拉选择
  3. 希望报错信息具备业务语义,故障栈干净,便于运维排查

新增模型接入方式:

  1. 编写适配器(大多场景直接复用 ChatOpenAI,仅覆盖基础参数)
  2. 将 provider 名称与适配器实例注册进全局注册表

仅新增适配器代码,无需修改工厂核心路由逻辑。

优点 局限
报错友好、可审计、可管控 必须提前注册,仅支持预定义白名单内的供应商

4.2 llm_use:逃生门(兜底扩展通道)

底层实现:动态反射导入模块与类。

适用场景(仅当 provider 体系覆盖不到时才启用):

  1. 模型接口不兼容 OpenAI 协议 ,使用原生 SDK / gRPC 等异构接口,无法用 ChatOpenAI 封装
  2. 需要深度自定义模型行为:增加特殊前置 / 后置逻辑、自定义 Agent 封装
  3. 临时实验原型,不想新增 provider、不想加入系统白名单,快速验证自定义适配器

重要 :兼容 OpenAI 协议的自定义模型,不需要 llm_use ,直接走 llm_provider 主干道。

新增模型接入方式:

  1. 编写继承 BaseChatModel 的自定义适配器类
  2. 在配置中填写 llm_use = "module:ClassName",无需修改注册表
优点 代价
不受预注册白名单约束,扩展自由度高 反射失败抛出底层 ModuleNotFoundError / AttributeError,堆栈深、丢失业务语义;难以按 provider 做配额、权限分组管控

⚠️ 关于"重写 invoke / stream / bind_tools":不应覆写 invoke / stream 。 LangChain 要求子类实现的是 _generate(必需)、_llm_type(必需), 可选 _stream / _agenerate / _astream;invoke / stream 是基类公开入口,覆写会破坏回调与重试。 另外 bind_tools 在 BaseChatModel 中默认抛 NotImplementedError,契约校验保不了它,需另行能力探测。


5 判定规则:什么时候走反射层(llm_use)

默认走主干道 llm_provider;反射层 llm_use 仅在加载不在预注册白名单内 ,且无法通过 OpenAI 协议适配器封装的自定义类时显式启用。 一旦配置 llm_use,它以更高优先级覆盖主干道;使用者换取动态扩展能力,同时承担反射带来的诊断成本。


6 执行逻辑伪代码(修正版)

【修正 1】原伪代码两条通道的 adapter 类型不一致,llm_use 分支会直接崩

原稿:

python 复制代码
def build_chat_model(cfg):
    if cfg.llm_use:
        adapter_cls = dynamic_import(cfg.llm_use)
        assert issubclass(adapter_cls, BaseChatModel)   # ← 断言:是 BaseChatModel 子类
        adapter = adapter_cls()                          # ← 于是 adapter 是【模型实例】
    else:
        adapter = ADAPTER_REGISTRY[cfg.llm_provider]    # ← 这里是【适配器实例】
    llm_model = adapter.build_model(cfg.params)          # ← llm_use 分支:AttributeError
    return llm_model, adapter

问题 :issubclass(adapter_cls, BaseChatModel) 之后 adapter_cls() 得到的是模型对象 (如 ChatOpenAI 实例), 而 ChatOpenAI 没有 build_model() 方法 。下一行 adapter.build_model(cfg.params) 在 llm_use 分支必然抛 AttributeError: 'ChatOpenAI' object has no attribute 'build_model'。

根因 :两条通道对 llm_use 的目标类型没有统一契约------它到底指向"适配器类"还是"模型类"?

修正 :显式区分两种情况,并把返回值统一成 (model, adapter):

python 复制代码
def build_chat_model(cfg):
    if cfg.llm_use:
        cls = dynamic_import(cfg.llm_use)

        if isinstance(cls, type) and issubclass(cls, BaseLlmAdapter):
            # 情况 A:llm_use 指向【适配器类】→ 与主干道同构
            adapter = cls()
        elif isinstance(cls, type) and issubclass(cls, BaseChatModel):
            # 情况 B:llm_use 指向【模型类】→ 用通用适配器包一层
            adapter = GenericAdapter(model_cls=cls)
        else:
            raise TypeError(
                f"llm_use={cfg.llm_use!r} 必须是 BaseLlmAdapter 或 BaseChatModel 子类,"
                f"实际为 {cls!r}"
            )
    else:
        adapter = _lookup_provider(cfg.llm_provider)

    llm_model = adapter.build_model(cfg.params)
    return llm_model, adapter

更推荐的收口方式 :让 llm_use 只接受适配器类 (BaseLlmAdapter 子类), 与 llm_provider 主干道完全同构 ------两条通道最终都返回一个"能 build_model() 的适配器"。 这样 build_chat_model 后半段只有一条路径,分支数从 2×2 降到 2×1,也避免"返回的 adapter 到底是什么"的长期歧义。

【修正 2】assert 不能用于生产环境校验

Python 官方语义:assert expr 等价于

python 复制代码
if __debug__:
    if not expr:
        raise AssertionError

而 __debug__ 在 -O / PYTHONOPTIMIZE=1 下为 False,编译器此时根本不为 assert 生成任何代码 ------ 校验被静默跳过。官方与主流实践一致:断言只用于检查程序内部不变量,绝不用于校验外部输入/配置。

python 复制代码
# ❌
assert issubclass(cls, BaseChatModel)

# ✅
if not (isinstance(cls, type) and issubclass(cls, BaseChatModel)):
    raise TypeError(f"{cfg.llm_use} 不是 BaseChatModel 子类")

【修正 3】ADAPTER_REGISTRY[...] 直接下标会抛无语义的 KeyError

两个字段都为空时 cfg.llm_provider 为 None,直接 KeyError: None;provider 拼错时也只是 KeyError: 'vlllm',运维看不出该怎么办。

python 复制代码
def _lookup_provider(provider: str | None):
    if not provider:
        raise ValueError("未配置 llm_provider,且未指定 llm_use,无法构建模型")
    try:
        return ADAPTER_REGISTRY[provider]
    except KeyError as e:
        raise ValueError(
            f"未知 llm_provider={provider!r};可用值:{sorted(ADAPTER_REGISTRY)}"
        ) from e

【修正 4】"反射堆栈深、丢失业务语义"这个代价,其实可以消掉

原稿把"堆栈深、丢语义"列为使用者必须知情承担的代价。这个代价不是必然的 ------用异常链就能同时拿到业务语义和完整技术栈。

Python 官方文档:raise NewError(...) from e 会把原异常挂到新异常的 __cause__ 上, traceback 中两段都会打印,并标注 The above exception was the direct cause of the following exception。

python 复制代码
def dynamic_import(path: str):
    module_path, _, cls_name = path.partition(":")
    if not module_path or not cls_name:
        raise ValueError(f"llm_use 格式应为 'module.path:ClassName',实际为 {path!r}")
    try:
        module = importlib.import_module(module_path)
    except ModuleNotFoundError as e:
        raise LlmProviderError(
            f"llm_use 模块未安装:{module_path}(请先 pip install,反射不会自动装包)"
        ) from e
    try:
        return getattr(module, cls_name)
    except AttributeError as e:
        raise LlmProviderError(
            f"模块 {module_path} 中不存在类 {cls_name}"
        ) from e
写法 结果
raise e ❌ 截断堆栈,丢底层上下文------Code Review 应直接打回
raise NewError(msg) ⚠️ 隐式链(__context__),语义弱
raise NewError(msg) from e ✅ 业务语义 + 完整技术栈,两者兼得

这样处理后,llm_use 的"代价"就只剩无法按 provider 做配额 / 权限分组管控这一条(因为它天然不在白名单里), 而"堆栈难读"这条可以从文档里划掉。


7 边界总结

# 结论
1 llm_provider 负责标准化接入:绝大多数 OpenAI 兼容模型全走这个通道,是系统主力扩展方式
2 llm_use 不是用来接入普通 OpenAI 兼容模型的,它是逃出标准化体系的逃生口------异构接口、深度定制、临时原型,日常业务使用频次很低
3 二者不是互相替代,是主通道 + 兜底逃逸通道的互补关系

8 常见误区澄清

误区 1 :llm_use 是接入自定义模型的唯一方式。 纠正 :兼容 OpenAI 协议的自定义模型,走 llm_provider 即可;llm_use 仅用于超出 OpenAI 协议封装能力边界的场景。
误区 2 :llm_use 优先级更高,所以优先推荐使用 llm_use。 纠正 :优先级代表显式配置的意图权重 ,不等于推荐选型。日常开发不填 llm_use,默认落在 provider 主干道,享受护栏保护。
补充误区 3 :工厂里用 assert 做契约校验就够了。 纠正 :-O 下 assert 被完全移除,必须 raise TypeError。
补充误区 4 :反射的错误堆栈只能忍。 纠正 :用 raise ... from e 包装成业务异常,语义和栈可以兼得。


9 一句话背诵版

主干道注册、逃生门反射;显式意图压默认,护栏给默认值。 契约校验要 raise 不要 assert,反射报错要 from e 不要裸抛。

相关推荐
2302_805771072 小时前
Day23:POM设计模式(封装)
java·开发语言·设计模式
熊出没3 小时前
高并发高可用系统设计:限流、熔断、降级与多级缓存实战指南
缓存·高并发·架构设计·高可用
风123456789~3 小时前
【架构设计】习题1 数据库题目 2/2
架构设计·数据库习题
半个落月19 小时前
LangGraph.js 实战:从状态图到分支、循环、检查点与人工中断
javascript·人工智能·langchain
大连好光景21 小时前
LangChain的核心架构
langchain·langsmith·langserver
染指11101 天前
142.Agent-多Agent框架-通过Skills实现多Agent
开发语言·人工智能·python·langchain·agents
憧憬成为java架构高手的小白1 天前
langchain4j手敲理解
java·langchain
M--Y1 天前
LangChain 提示工程:少样本提示与输出解析器
python·langchain·大模型·llm·输出解析器·少样本提示
打工仔折腾 AI1 天前
极空间用 Docker 部署 Photopea:网页版 PS 的私有化实践
运维·后端·python·docker·容器·langchain·ai agent 实战
guslegend1 天前
工程地基、Provider 与 ReAct 循环:Agent 核心能力的设计、实现与验收
人工智能·agent·react·架构设计·spring ai