适配器模式(Adapter Pattern):原理、场景与 LLM 多厂商工程落地

适配器模式(Adapter Pattern):原理、场景与 LLM 多厂商工程落地

适用口径:GoF 设计模式原义 + langchain-core 0.3.x / langchain-openai / langchain-anthropic。 本文在原始复盘稿基础上做了 5 处修正 ,其中 3 处是照原稿写会直接 AttributeError 的硬伤,见第 4 节各子小节。


一、概述

1.1 定义

适配器模式(Adapter Pattern)是一种结构型设计模式,核心目的是:

对多个接口不统一、行为不一致的异构第三方组件,通过包装适配,对外暴露一套统一、稳定、标准化的接口,彻底解耦上层业务与底层异构实现。

通俗理解:插头转换器。不同国家插头不一样(各家大模型),电器(Agent 业务)不改代码,只换转换器(适配器)。

原义补充 :GoF 里适配器有两种实现形态------ 类适配器 (多继承,同时继承 Target 与 Adaptee)与对象适配器 (组合,Adapter 内部持有 Adaptee 实例)。 Python / Java 工程几乎一律用对象适配器(组合优于继承),本文讨论的也是这一种。

1.2 解决的核心痛点

在多厂商、多异构 SDK 场景下,系统天然存在以下问题:

# 问题
1 不同厂商入参结构不统一(思考参数、温度、最大生成长度格式各异)
2 不同厂商返回结构不统一(思考内容、token 消耗、停止原因字段不同)
3 新增厂商需要大量修改业务代码,if-else 爆炸
4 上层业务必须感知底层模型差异,严重耦合、极难维护

适配器模式的终极目标:上层业务 0 感知底层厂商差异。


二、模式核心组成(标准三角色)

角色 定义 本项目对应
Target(目标抽象接口) 全系统统一的行为规范,所有适配器必须实现 BaseLlmAdapter
Adaptee(被适配者) 原生异构、无法直接给业务使用的底层组件 OpenAI / Anthropic / DeepSeek / 通义千问 各家 SDK
Adapter(具体适配器) 每个厂商一个,把"非标行为"翻译成"标准契约" OpenAIAdapter / AnthropicAdapter / ...

Target 的作用:统一入参、出参、行为定义;作为上层业务依赖的唯一标准;强制所有厂商抹平差异。

Adapter 内部只做一件事 :把厂商的"非标行为"翻译成系统的"标准契约"------ 包含参数适配、结构转换、字段映射、异常兼容、元数据归一化。


三、核心设计思想

3.1 单一职责

  • 适配器 :只处理当前厂商独有的差异化逻辑
  • 公共工厂 / 业务层 :只处理所有厂商共有的通用逻辑

3.2 开闭原则(最重要)

新增模型厂商,只新增适配器,不改动原有任何业务代码。 彻底解决传统 if-else 架构「加一个需求改一堆代码」的问题。

3.3 解耦思想

上层 Agent、对话链路、记忆链路、流式输出完全不依赖具体模型,只依赖统一适配器契约。


四、LLM 项目完整可运行落地示例

4.1 第一步:定义统一适配器契约(基类)

python 复制代码
from abc import ABC, abstractmethod
from langchain_core.language_models import BaseChatModel
from langchain_core.messages import AIMessage   # ← 修正点:invoke() 返回的是 AIMessage


class BaseLlmAdapter(ABC):
    """LLM 多厂商适配器统一契约。所有厂商必须实现,保证上层业务完全统一。"""

    @abstractmethod
    def build_model(self, raw_params: dict) -> BaseChatModel:
        """根据原始配置构建模型实例,内部完成厂商独有参数转换、嵌套适配。"""
        raise NotImplementedError

    @abstractmethod
    def extract_metadata(self, message: AIMessage) -> dict:
        """统一元数据归一化:抹平各家返回字段差异。
        统一输出:thinking / usage / finish_reason。"""
        raise NotImplementedError

4.2 【修正 1】llm.invoke() 返回的是 AIMessage,不是 ChatResult

这是原稿最根本的一处类型错误,且会连锁导致后两个字段全部取不到。

调用方式 返回类型 说明
model.invoke(...) AIMessage 单条对话,最常用
model.generate([messages]) ChatResult 批量 / 多候选,含 generations: List[ChatGeneration]

原稿 4.3 的调用是 resp = llm.invoke("你好") 然后 adapter.extract_metadata(resp), 但 extract_metadata 的签名标注的是 result: ChatResult------类型标注与实际调用对不上 。 invoke() 的返回值没有 .generations,后面 result.generations[0] 会直接炸。

4.3 【修正 2】usage_metadata 在 AIMessage 上,不在 ChatResult 上

LangChain 官方 AIMessage 参考明确列出:

属性 位置
usage_metadata AIMessage (UsageMetadata,跨厂商统一的 token 统计)
response_metadata AIMessage(厂商原始响应:model_name、finish_reason 等)
additional_kwargs AIMessage(厂商私有载荷)
content / tool_calls AIMessage

而 ChatResult 只有 generations: List[ChatGeneration] 与 llm_output: Optional[dict]------它没有 usage_metadata。

所以原稿的 "usage": result.usage_metadata 会抛 AttributeError。正确写法是 message.usage_metadata。

usage_metadata 的结构(官方口径):

python 复制代码
{
    "input_tokens": 111,
    "output_tokens": 544,
    "total_tokens": 655,
    "input_token_details": {"cache_read": 0},
    "output_token_details": {"reasoning": 460},   # 思考模型的推理 token 单独计
}

4.4 【修正 3】finish_reason 在 response_metadata 里,不在 ChatGeneration 上

ChatGeneration 的字段是 text / message / generation_info------没有 finish_reason 属性 。 原稿的 result.generations[0].finish_reason 同样是 AttributeError。

正确来源是 AIMessage.response_metadata["finish_reason"],常见取值:

值 含义
stop 正常生成完毕
length 达到 max_tokens 或上下文上限,被截断
tool_calls 模型暂停文本生成,转为请求调用工具
content_filter 被安全策略过滤

4.5 第二步:不同厂商实现各自适配器(修正后的可运行版)

OpenAI 兼容模型适配器
python 复制代码
from langchain_openai import ChatOpenAI
from langchain_core.messages import AIMessage


class OpenAIAdapter(BaseLlmAdapter):
    def build_model(self, raw_params: dict) -> BaseChatModel:
        return ChatOpenAI(**raw_params)          # OpenAI 协议标准,无需特殊转换

    def extract_metadata(self, message: AIMessage) -> dict:
        return {
            # DeepSeek / 通义千问等把思考内容放在 reasoning_content
            "thinking": message.additional_kwargs.get("reasoning_content", ""),
            "usage": message.usage_metadata,                          # ✅ 修正
            "finish_reason": message.response_metadata.get("finish_reason"),  # ✅ 修正
        }
Anthropic Claude 适配器(非标结构适配)
python 复制代码
from langchain_anthropic import ChatAnthropic


class AnthropicAdapter(BaseLlmAdapter):
    def build_model(self, raw_params: dict) -> BaseChatModel:
        """Claude 独有:平铺的 reasoning_effort 转成嵌套 thinking 结构。"""
        params = raw_params.copy()
        if "reasoning_effort" in params:
            budget = params.pop("reasoning_effort")
            params["thinking"] = {"type": "enabled", "budget_tokens": budget}
        return ChatAnthropic(**params)

    def extract_metadata(self, message: AIMessage) -> dict:
        return {
            # Claude 的思考以 content block 形式返回,content 可能是 list[dict]
            "thinking": self._pick_thinking(message),
            "usage": message.usage_metadata,
            "finish_reason": message.response_metadata.get("finish_reason"),
        }

    @staticmethod
    def _pick_thinking(message: AIMessage) -> str:
        content = message.content
        if isinstance(content, list):
            return "".join(
                b.get("thinking", "") for b in content
                if isinstance(b, dict) and b.get("type") == "thinking"
            )
        return message.additional_kwargs.get("thinking", "")

4.6 【修正 4】Claude budget_tokens 有三条硬约束,原稿没写

Anthropic 官方《Extended thinking》文档给出的约束:

# 约束 后果
1 budget_tokens 最小 1024 更小的值 API 直接拒绝
2 budget_tokens 必须严格小于 max_tokens 思考 token 计入 max_tokens,必须给最终回答留空间
3 不能与 max_tokens: 0 同用 因为违反第 2 条,extended thinking 无法配合 cache pre-warming

工程建议:max_tokens >= budget_tokens + 4000(给最终输出留缓冲), 常见配法是 50/50:max_tokens=8192 配 budget_tokens=4096。

多轮会话还有一个坑 :开启 thinking 后,Claude 返回的 thinking block 带加密签名 。 下一轮必须原样回传 (顺序一致、签名字节一致),否则报 BadRequestError: ... signature mismatch。所以多轮历史里不能用 msg.text() 把 thinking 抹平。

4.7 第三步:上层业务统一调用(完全无感知厂商)

python 复制代码
llm, adapter = factory.create(llm_use, llm_params)
message = llm.invoke("你好")          # 返回 AIMessage

meta = adapter.extract_metadata(message)   # 自动归一化,无需 if 判断模型

五、适配器模式在本项目解决的具体问题

# 问题 适配器如何处理
5.1 Reasoning Contract 推理契约 :thinking=True / reasoning_effort="high" / thinking={"budget":1024} 各家格式不同 适配器内部统一翻译,上层只用一套配置
5.2 元数据不统一:思考内容、token 字段各家命名混乱 适配器统一剥离、归一化输出
5.3 多厂商 if-else 泛滥 厂商越多收益越大,代码越干净

六、【修正 5】BaseLlmAdapter 严格说不是「纯 GoF 适配器」

这是原稿在模式定位上值得精准化的一点。

GoF 原义的 Adapter:Adapter 自身实现 Target 接口,客户端直接调用 Adapter 的方法,Adapter 内部转发给 Adaptee。

对照本文的 BaseLlmAdapter:

方法 真实职责 对应模式
build_model(raw_params) 返回一个构建好的对象,而非"被调用后转发" 工厂 / 构建器(Factory / Builder)
extract_metadata(msg) 把厂商返回结构转换成统一结构 适配器(真正的适配部分)

也就是说,这个"适配器"实际是三种模式的组合体:

  • 适配器模式:抹平厂商返回差异、统一元数据(核心)
  • 工厂 / 构建器模式:统一创建入口、统一参数校验、统一限流
  • 注册器模式:用 Map 注册表干掉所有 if-else

原稿第 6 节已经点出"三位一体",但没意识到第 4 节的基类本身就夹带了工厂职责 ------ build_model 不是适配,是构建。把这点说清楚,可以避免团队里对"到底在适配什么"的长期争论。

建议的接口切分(如果要做纯粹):

python 复制代码
class LlmFactory(ABC):       # 工厂:负责构建
    @abstractmethod
    def build_model(self, raw_params: dict) -> BaseChatModel: ...

class MetadataAdapter(ABC):  # 适配器:负责归一化
    @abstractmethod
    def extract_metadata(self, message: AIMessage) -> dict: ...

七、适用场景(严格总结)

满足以下任意场景,必须使用适配器模式:

  1. 多供应商、多三方异构接口
  2. 入参、出参结构不统一,需要归一化
  3. 后续会持续新增供应商,需要低侵入扩展
  4. 上层业务需要稳定、不受底层迭代影响

八、什么时候不需要适配器(避坑:防止过度设计)

厂商数量 ≤ 3 个、差异极少时,不需要适配器。 此时简单 if-else 代码更短、更好维护、调试更快,抽象反而增加负担。

这正是 Rule of Three(三次法则):第一次直接写,第二次复制容忍,第三次出现重复再抽象。

抽象的时机:出现重复粘贴代码、厂商 ≥ 4 个时再重构。


九、总结

适配器模式的本质:隔离变化、统一契约、收敛差异。

底层各家模型千奇百怪,适配器负责把所有"不标准",统一翻译成系统的"标准"。 最终实现:底层随便扩展,上层永远不动的高可扩展架构。

一句话背诵版:

三角色:Target 定契约、Adaptee 是异构、Adapter 做翻译; 两原则:单一职责分公私、开闭原则加厂商; 一红线:厂商 ≤ 3 别抽象,invoke 回来是 AIMessage。

相关推荐
for_ever_love__1 小时前
LangChain Text-to-SQL:让不会写 SQL 的人查数据库
数据库·sql·langchain·大模型·agent·rag·问答
木子n12 小时前
软件架构设计实战-架构18.从 Classic 到 Adaptive:SOA 架构在 MATLAB 里的模样
架构·autosar·性能工程·soa架构
mldong2 小时前
给 Django Admin 加审批流:一行前端代码没写,298 行接上工作流引擎
后端·python·架构
逆风飞翔的小叔2 小时前
【AI智能体】Langchain 工具调用实战操作详解
langchain·langchain 工具调用·langchain 工具使用·langchain 工具详解·langchain 工具总结·langchain 工具·langchain tool
小白学大数据11 小时前
拼多多反爬对抗实战:Scrapy 中间件化采集架构解析
开发语言·scrapy·中间件·架构
茵Cindy11 小时前
AI+HR智能体架构如何落地?从入口到生态底座的工程实现
人工智能·架构·ai+hr
骇客野人12 小时前
微服务多级缓存架构设计
缓存·微服务·架构
408高手12 小时前
23种设计模式
设计模式
闻哥12 小时前
分布式任务调度框架:XXL-Job / ElasticJob / DolphinScheduler / SchedulerX 架构对比与选型
分布式·架构·wpf