一套代码接入五家大模型:Silicon-AI 多 LLM 抽象层的技术亮点拆解

简介: 接五家大模型,业务代码一行不改?本文拆解硅基边界 Silicon-AI 多 LLM 抽象层:统一 OpenAI 兼容底座与工厂装配,新厂商即插即用;一个开关翻译五种思考协议,显式开关防结构化输出受限;补齐 reasoning_content 解析,思考与正文分离渲染;下架模型自动路由不断裂;多模态差异收编适配器;回调式计量计费全局生效;密钥加密与结构化输出分层。全文七段示意代码随文配套。

本文是「成都硅基边界」零代码构建平台旗下 Silicon-AI 智能体引擎实战复盘系列第七篇。前面聊过智能体、RAG、MCP、工作流引擎与数据库,这一篇讲平台的地基之一——多 LLM 抽象层:Qwen、DeepSeek、Doubao、GLM、OpenAI 五家模型,业务代码如何做到"换模型只换配置、不改一行代码"。文中代码均为示意代码(非项目真实源码),按生产级 Python 规范书写。

一、背景:"接模型"为什么会越接越乱

接一家新模型,表面看只是换个 API 地址,实际要处理一堆"隐形差异":

  • 思考模式协议不同:同是"开启深度思考",Qwen 是 enable_thinking,GLM/DeepSeek/Doubao 是 extra_body.thinking,OpenAI 是 reasoning_effort
  • 推理内容格式不同:思考过程的增量藏在 delta.reasoning_content,部分 SDK 根本不解析;
  • 模型上下架频繁:用户历史配置里的模型名,三个月后可能已下架;
  • 多模态参数各异:同样是"传一段音频",有的要显式 format 字段,有的不用;
  • 密钥安全:API Key 绝不能明文落库;
  • 计量计费:每家价格结构不同(输入/输出差价、计价单位),流式调用还要想办法拿到 usage。

这些差异如果散落在业务代码里,每接一家模型就要改一遍所有调用点。Silicon-AI 的解法是建一层抽象,把差异全部收编。整体结构如下:

flowchart TD
    BIZ[业务代码<br/>智能体 / 工作流 / RAG] --> FAC[LLMComponent 统一门面<br/>bind_tools / invoke / astream / 结构化输出]
    FAC --> F{providerFormat 工厂}
    F --> Q[ChatQwen<br/>阿里百炼]
    F --> D[ChatDeepSeek<br/>DeepSeek 官方]
    F --> DB[ChatDoubao<br/>火山方舟]
    F --> G[ChatGLM<br/>智谱 BigModel]
    F --> O[ChatOpenAI 兜底<br/>任意 OpenAI 兼容厂商]
    Q & D & DB & G & O --> CB[LLMUsageCallback<br/>统一计量计费]
    CB --> DB2[(用量 / 费用落库)]

二、亮点一:统一 OpenAI 兼容底座 + 工厂装配

五家模型全部基于 OpenAI 兼容协议接入:Qwen 走百炼兼容模式、GLM 走智谱兼容模式、Doubao 走火山方舟(原生兼容 OpenAI SDK)、DeepSeek 用官方 ChatDeepSeek。工厂按 providerFormat 装配,业务侧面对的是同一个 LLMComponent 门面:

"""llm_factory.py —— 模型工厂:按 providerFormat 装配统一的对话模型实例。"""
from __future__ import annotations

from collections.abc import Sequence
from typing import Any

from langchain_core.callbacks import BaseCallbackHandler
from langchain_core.language_models.chat_models import BaseChatModel
from langchain_openai import ChatOpenAI

from model.enums.llm_provider_enum import LLMProviderEnum


def build_chat_model(
    llm: "LLMChat",
    callbacks: Sequence[BaseCallbackHandler],
) -> BaseChatModel:
    """按厂商格式装配对话模型实例。

    Args:
        llm: 已解密凭据的模型配置(model / base_url / api_key / temperature 等)。
        callbacks: 生命周期回调(计量计费等),对所有厂商统一挂载。

    Returns:
        可直接用于 invoke / astream / bind_tools 的对话模型实例。

    Raises:
        ValueError: model 或 api_key 为空时抛出。
    """
    common: dict[str, Any] = {
   
        "model": llm.model,
        "base_url": llm.base_url,
        "api_key": llm.api_key,
        "temperature": llm.temperature,
        "timeout": llm.timeout,
        "streaming": llm.streaming,
        "stream_usage": True,        # 流式调用也要返回 usage,否则 token 统计恒为 0
        "callbacks": callbacks,
    }

    match llm.format:
        case LLMProviderEnum.QWEN:
            return ChatQwen(**common, enable_thinking=llm.enable_thinking)
        case LLMProviderEnum.GLM:
            return ChatGLM(**common, enable_thinking=llm.enable_thinking)
        case LLMProviderEnum.DEEPSEEK | LLMProviderEnum.DOUBAO:
            return ChatDeepSeek(**common, extra_body=thinking_body(llm)) \
                if llm.format is LLMProviderEnum.DEEPSEEK \
                else ChatDoubao(**common, extra_body=thinking_body(llm))
        case _:
            # 兜底:任意 OpenAI 兼容厂商,配置 base_url 即插即用
            return ChatOpenAI(**common)

注意最后的兜底分支:凡是 OpenAI 兼容的新厂商,不用写任何适配代码,配置 base_url 即插即用。"支持 N 家模型"的成本,从 O(N) 次业务改造降为 O(1) 次配置。

三、亮点二:一个开关,五种"思考"协议

"深度思考"这个业务开关,在各家模型上的协议写法完全不同。抽象层把它收敛为一个 enable_thinking 字段,由适配器统一翻译:

厂商 开启思考的写法
Qwen(思考模型) enable_thinking=True,可配 thinking_budget
GLM extra_body={"thinking": {"type": "enabled"}}
DeepSeek extra_body={"thinking": {"type": "enabled"/"disabled"}}
Doubao extra_body={"thinking": {"type": "enabled"/"disabled"}}
OpenAI reasoning_effort="high"

这里藏着一个真实踩过的坑:部分思考模型默认开启 thinking,而思考开启时 tool_choice 等结构化输出能力受限——智能体一旦绑定工具就报错,且报错信息完全不指向根因。解法是显式传 enabled/disabled,绝不依赖服务端默认值

def thinking_body(llm: "LLMChat") -> dict[str, Any]:
    """将统一的 enable_thinking 开关翻译为 DeepSeek/Doubao 私有协议。

    必须显式传入 enabled/disabled:部分思考模型默认开启 thinking,
    会限制 tool_choice 等结构化输出能力,表现为"绑定工具后调用失败",
    且报错信息与真实原因相距甚远——绝不依赖服务端默认值。
    """
    mode = "enabled" if llm.enable_thinking else "disabled"
    return {
   "thinking": {
   "type": mode}}

四、亮点三:reasoning_content 解析补丁,推理/正文分离

深度思考模型的"思考过程"以 delta.reasoning_content 增量返回,而所用版本的 langchain_openai 父类不解析这个字段——思考内容会直接丢失。各适配子类统一做了流式 chunk 解析的 override,把思考内容归并进 additional_kwargs["reasoning_content"]

class ChatQwen(ChatOpenAI):
    """Qwen 对话模型(百炼 OpenAI 兼容模式),补齐 reasoning_content 解析。"""

    enable_thinking: bool | None = Field(default=None)
    thinking_budget: int | None = Field(default=None)

    @staticmethod
    def merge_reasoning(
        chunk: AIMessageChunk,
        delta: "ChoiceDelta",
    ) -> AIMessageChunk:
        """将流式增量中的 reasoning_content 归并进 additional_kwargs。

        父类(langchain_openai 1.1.6)不解析 delta.reasoning_content,
        思考内容会在流式路径上丢失;此处显式归并,供前端分离渲染
        「思考过程 / 正式回答」,并供回调层测量思考耗时。
        """
        if (reasoning := getattr(delta, "reasoning_content", None)) is None:
            return chunk
        merged = chunk.additional_kwargs.get("reasoning_content", "") + reasoning
        return chunk.model_copy(
            update={
   "additional_kwargs": {
   **chunk.additional_kwargs,
                                          "reasoning_content": merged}}
        )

收益立竿见影:前端可以把"思考过程"和"正式回答"分开渲染(打字机先出思考、再出正文),回调层还能据此测量思考耗时;同一套分离逻辑在 Qwen / GLM / Doubao 三家复用。

五、亮点四:下架模型自动路由,旧配置不断裂

模型厂商上下架频繁,用户三个月前保存的"qwen-plus"可能已经下架,直接报错会把用户旧数据变成"死配置"。适配层用不可变映射表 + model_validator 在实例化前自动路由:

from types import MappingProxyType
from typing import Final

_DEPRECATED_MODELS: Final[Mapping[str, str]] = MappingProxyType({
   
    "qwen-plus": "qwen3-plus",
    "qwen-flash": "qwen3-flash",
    "qwq-plus": "qwq-32b",
})


class ChatQwen(ChatOpenAI):

    @model_validator(mode="before")
    @classmethod
    def _route_deprecated_model(cls, values: dict[str, Any]) -> dict[str, Any]:
        """实例化前将已下架模型名路由至官方替代型号,保证旧配置持续可用。"""
        if isinstance(values, dict) and (model := values.get("model")) in _DEPRECATED_MODELS:
            replacement = _DEPRECATED_MODELS[model]
            logger.warning("对话模型 %s 已下架,自动路由至 %s", model, replacement)
            values = {
   **values, "model": replacement}
        return values

两个专业细节:映射表用 MappingProxyType 包成只读,防止运行时被误改;model_validator(mode="before") 在字段校验前拦截,替换发生在任何请求发出之前。下架不等于断裂:旧配置自动升级到替代模型,只在日志里留痕。

六、亮点五:多模态 content blocks 转换,抹平各家参数差异

多模态输入(图片、音频、视频、文档)在各家协议里同样不对齐。适配层统一做 content blocks 转换:

  • 图片:多数厂商 image_url 原生支持,直接透传;
  • 音频:火山方舟要求显式 format 字段——从 URL 路径后缀推断,推断不出时兜底 mp3,并转成 OpenAI 标准 input_audio 格式;
  • 文档:不支持文档输入的厂商(如火山方舟 Chat API 不收 PDF)直接不进 content blocks,引导走文件解析工具。
from pathlib import PurePosixPath
from urllib.parse import urlparse

_AUDIO_FORMAT_BY_EXT: Final[Mapping[str, str]] = MappingProxyType({
   
    ".mp3": "mp3", ".wav": "wav", ".m4a": "m4a",
    ".aac": "aac", ".flac": "flac", ".ogg": "ogg", ".opus": "opus",
})
_DEFAULT_AUDIO_FORMAT: Final[str] = "mp3"


def guess_audio_format(url: str) -> str:
    """从 URL 路径后缀推断音频格式。

    火山方舟 Chat API 的音频输入必须显式携带 format 字段;
    后缀缺失或不可识别时回退 mp3,避免请求被服务端直接拒绝。
    """
    suffix = PurePosixPath(urlparse(url).path).suffix.lower()
    return _AUDIO_FORMAT_BY_EXT.get(suffix, _DEFAULT_AUDIO_FORMAT)

多模态差异和思考模式一样,全部消化在适配器内部——业务代码只管把"一段音频"扔进来。

七、亮点六:回调式计量计费,一次挂载全局生效

抽象层挂了一个 LLMUsageCallback(LangChain 回调处理器),把计量计费从业务代码里彻底剥离:

from dataclasses import dataclass
from decimal import Decimal


@dataclass(slots=True, frozen=True)
class UsageSnapshot:
    """一次模型调用的用量快照。"""
    prompt_tokens: int = 0
    completion_tokens: int = 0
    elapsed_ms: int = 0
    payment: Decimal = Decimal("0")


class LLMUsageCallback(BaseCallbackHandler):
    """模型调用生命周期回调:统一完成计量、计费与落库。

    在工厂装配阶段挂载,业务代码零感知;
    on_llm_error 同样落库(费用记零),保证账单闭环可对账。
    """

    def __init__(self, llm_chat: "LLMChat", balance: "BalanceConsumePO | None" = None) -> None:
        self._llm = llm_chat
        self._balance = balance
        self._t0: float | None = None

    def on_llm_start(self, serialized, prompts, **kwargs) -> None:
        self._t0 = time.monotonic()

    def on_llm_end(self, response: LLMResult, **kwargs) -> None:
        usage: dict = (response.llm_output or {
   }).get("token_usage") or {
   }
        in_tok = int(usage.get("prompt_tokens") or 0)
        out_tok = int(usage.get("completion_tokens") or 0)
        in_price, out_price = self._llm.resolve_price(in_tok)   # 输入/输出差价
        payment = ceil_particle(
            Decimal(in_tok) * Decimal(str(in_price))
            + Decimal(out_tok) * Decimal(str(out_price))
        )
        elapsed_ms = int((time.monotonic() - self._t0) * 1000) if self._t0 else 0
        self._persist(UsageSnapshot(in_tok, out_tok, elapsed_ms, payment))

    def on_llm_error(self, error: BaseException, **kwargs) -> None:
        self._persist(UsageSnapshot())    # 失败调用也落库,费用记零

    def _persist(self, snapshot: UsageSnapshot) -> None:
        ...  # 写入 LLMChatStatPO 并累加 balance_consume,此处省略

几个细节值得强调:stream_usage=True 保证流式调用也能拿到 usage(否则流式场景 token 统计永远是 0);输入/输出差价计价单位换算都在 resolve_price 一处收敛;失败的调用也要落库(费用记零),否则账单对不上;快照用 frozen dataclass,计量数据一旦生成不可篡改。这一层挂上后,智能体、工作流、RAG 所有调用点零改动自动具备计量能力。

八、亮点七:密钥加密落库 + 结构化输出 + 小模型分层

还有三处容易被忽略的工程细节:

  1. API Key 密文落库:模型服务商的 Key 加密存储,使用时才解密注入——数据库拖走也不泄露密钥;
  2. 结构化输出:门面提供 invoke_structured_output(schema),支持从"输出变量定义"递归构建 Pydantic 模型(含嵌套子结构),工作流 LLM 节点的"多输出变量"就靠它实现:
_PYTHON_TYPES: Final[Mapping[str, type]] = MappingProxyType({
   
    "string": str, "integer": int, "number": float, "boolean": bool,
})


def build_output_model(variables: Sequence["OutputVariable"]) -> type[BaseModel]:
    """由输出变量定义递归构建 Pydantic 模型(支持嵌套对象与列表)。

    Args:
        variables: 工作流节点上声明的输出变量(可能带 children 子结构)。

    Returns:
        可直接传给 with_structured_output 的动态模型类型。
    """
    fields: dict[str, tuple[Any, FieldInfo]] = {
   }
    for var in variables:
        if var.children:
            child = build_output_model(var.children)          # 嵌套 → 递归建子模型
            annotation: Any = list[child] if var.is_list else child
        else:
            annotation = _PYTHON_TYPES[var.type]
        fields[var.name] = (annotation,
                            Field(description=var.description or var.name))
    return create_model("WorkflowOutput",
                        __config__=ConfigDict(strict=False), **fields)
  1. 小模型分层:问题重写、推荐问题、意图分类这类轻任务统一走 ChatSmallLLM(小模型),重任务才用旗舰模型——按任务分级选模型,是成本优化的第一杠杆

九、结语:抽象层的价值是"业务零感知"

回看这层抽象做了什么:协议差异(思考模式、reasoning、多模态)收进适配器,生命周期事件(上下架、密钥、计量计费)收进回调和校验器,业务代码只面对一个门面。换模型 = 改一条配置;接新厂商 = 写一个继承 ChatOpenAI 的适配类;计费策略调整 = 只改回调一处。

如果你也要做多模型平台,建议按这个优先级投入:先统一底座(OpenAI 兼容 + 工厂),再收编计量计费(回调挂载),最后打磨体验细节(思考分离、下架路由、多模态转换)。模型市场变化越快,这层抽象的复利就越大。

相关文章
|
3天前
|
人工智能 API 内存技术
刚刚 DeepSeek V4.1 Flash 开启内测,1 分钟教你用上!
刚刚 DeepSeek 内测群发布了 DeepSeek V4.1 Flash 中间版本内测的消息,这次的模型采用了新的结构,原生支持多模态、能力更强、速度更快、且成本更低。
1618 4
|
7天前
|
人工智能 运维 BI
阿里云千问办公QwenWork深度解析:基于Qwen3.8,六大核心能力重构企业全自动化工作流与计费选型指南
传统AI办公工具大多停留在对话问答、文档摘要、简单文案生成层面,只能完成单点碎片化任务,无法自主拆解复杂业务流程,很难串联多工具、多文档、外部业务系统完成端到端完整工作交付。很多企业在落地AI办公的时候,需要组合多款不同工具,来回切换界面,手动复制粘贴中间结果,智能化改造落地门槛居高不下。千问办公QwenWork是整合多款智能体产品能力打造的一体化企业办公智能体平台,底层基座依托Qwen3.8大模型,打通桌面端Agent、云端Agent、企业协同Agent三种运行形态,不再局限简单问答,接收业务目标之后自主拆解任务步骤,调用各类工具,处理文档、表格、浏览器自动化、数据查询,直接输出可交付的办公
1598 0
|
4天前
|
SQL 人工智能 前端开发
QoderWake 1.0 正式发布:从桌面里的 Agent,到工作现场的数字员工
QoderWake v1.0正式发布:企业级数字员工团队平台。支持“一句话建岗”,预置10类特训岗位;Waker常驻钉钉/飞书群,@即响应、自动协作、跨任务记忆;具备定时/事件/API多触发方式与统一任务看板;已沉淀27.6万条记忆、12.3万项技能,助力组织实现人机协同增效。
698 0
|
16天前
|
人工智能 自然语言处理 安全
阿里云千问办公、Qoder Teams、Qoder CN区别与选择指南:模型能力、适用场景与最新活动参考
本文聚焦阿里云2026年推出的三款自研AI办公产品,清晰拆解千问办公、Qoder Teams、Qoder CN的差异化定位与能力边界:千问办公主打职场全场景提效,支持自然语言指令一键完成PPT生成、数据分析等高频办公任务;Qoder Teams面向程序员团队,深度整合AI代码生成、团队协同与企业知识库能力;Qoder CN则专为金融、政务等强合规场景打造,实现数据不出境与VPC私有化部署。文章同步给出分场景选型指南与最新活动定价,帮助不同类型的企业按需组合产品,实现业务岗、研发岗与强合规场景的AI能力全覆盖。
3843 5
阿里云千问办公、Qoder Teams、Qoder CN区别与选择指南:模型能力、适用场景与最新活动参考
|
7天前
|
人工智能 自然语言处理 安全
阿里云AI数智鉴密:AI 生成内容如何拿到一张"防篡改的身份证"
隐形水印 + C2PA签名:让AI生成内容“持证上岗”。
1141 0
|
8天前
|
网络协议 Linux iOS开发
【2026实测】Wireshark下载+安装+汉化+使用教程(图文版,巨详细)
Wireshark 是一款免费开源的网络协议分析工具,可实时捕获、解析并可视化数据包,助你诊断网络故障、分析通信协议(如HTTP、DNS、TCP等)。支持Windows/macOS/Linux,含中文界面,新手入门便捷。(239字)
|
2天前
|
缓存 测试技术 API
DeepSeek V4.1 Flash 内测接入:改个模型名即可调用(附代码)
DeepSeek V4.1 Flash 内测不用申请,base_url 不变、改个模型名就能调,9/10 到期。本文讲清接入、计费限流与多模态注意点。
643 0
DeepSeek V4.1 Flash 内测接入:改个模型名即可调用(附代码)