当前位置: 首页 > news >正文

LLM 抽象层怎么设计?让 AI 应用不被某一个模型厂商绑定

上一篇文章讨论了 AI 应用的配置系统。

配置系统解决的是一个问题:

项目运行时应该使用哪个模型、哪个 API Key、哪个服务地址、哪个超时时间。

但配置系统本身并不负责调用模型。

当配置准备好以后,AI 应用马上会遇到下一个问题:

业务代码应该如何调用大模型?

很多 Demo 会这样写:

fromopenaiimportOpenAI client=OpenAI(api_key="sk-xxx")response=client.chat.completions.create(model="gpt-4o",messages=[{"role":"user","content":"请总结这段文字"}],)print(response.choices[0].message.content)

这段代码很直观,也很适合快速验证想法。

但如果项目继续发展,就会出现很多工程问题。

今天使用 OpenAI。

明天可能要切到 DeepSeek。

后天可能要同时支持 Qwen。

某些场景需要流式输出。

某些场景需要更长的超时时间。

某些场景需要记录 token 用量。

某些场景需要在主模型失败时降级到备用模型。

如果业务代码里到处都是某个厂商 SDK 的调用方式,项目就会很难维护。

所以,在 AI Scaffold 这样的工程化脚手架里,需要有一层专门负责模型调用。

这一层就是 LLM 抽象层。

一、为什么不能在业务代码里直接调用模型 SDK?

直接调用模型 SDK 最大的问题,是业务逻辑和基础设施逻辑混在了一起。

业务代码真正关心的通常是:

给模型一段输入。

拿到模型返回的结果。

必要时处理流式输出。

必要时拿到 token 用量。

但具体使用哪个模型厂商、怎么初始化客户端、参数名字叫什么、返回结构是什么,很多时候不应该由业务代码直接关心。

例如 OpenAI 风格的调用可能是:

response=client.chat.completions.create(model="gpt-4o",messages=messages,temperature=0.7,)

另一个模型供应商可能是:

response=client.chat(model_name="xxx",prompt=prompt,stream=False,)

返回结果也可能不同。

有的结果在response.choices[0].message.content里。

有的结果在response.output_text里。

有的结果会返回一个事件流。

有的结果会额外包含 token 统计。

如果每个业务模块都自己适配这些差异,项目会慢慢变成这样:

ifprovider=="openai":# OpenAI 调用逻辑...elifprovider=="deepseek":# DeepSeek 调用逻辑...elifprovider=="qwen":# Qwen 调用逻辑...

短期看,这是最快的写法。

长期看,这是最难维护的写法。

因为模型供应商的差异会扩散到整个项目。

一旦要切换模型,就需要改很多地方。

一旦要增加重试,就需要复制很多逻辑。

一旦要做测试,就很难把模型调用替换成 mock。

所以,模型调用应该集中在一层里。

业务代码不应该直接依赖具体 SDK,而应该依赖一个统一接口。

二、LLM 抽象层到底解决什么问题?

LLM 抽象层不是为了把代码写得更复杂。

它的目标,是把模型供应商差异收敛起来。

一个合理的 LLM 抽象层,至少应该解决几个问题。

第一,统一调用方式。

不管底层是 OpenAI、DeepSeek、Qwen,业务代码都使用同一种调用方法。

例如:

result=llm.chat(messages)

第二,统一返回结构。

业务代码不需要关心不同 SDK 的原始返回格式,而是拿到一个统一的结果对象。

例如:

print(result.content)print(result.model)print(result.usage)

第三,统一配置入口。

模型供应商、模型名称、API Key、超时时间、温度参数都从配置系统读取。

业务模块不应该自己读取环境变量。

第四,统一错误处理。

超时、限流、鉴权失败、网络失败、模型不可用,都应该有统一的异常类型或处理策略。

第五,支持流式输出。

很多 AI 应用需要边生成边展示,所以抽象层应该支持普通调用和流式调用。

第六,支持扩展新模型。

当项目需要接入新的模型供应商时,最好只新增一个 Provider,而不是修改所有业务模块。

第七,方便测试。

测试时可以使用 Fake LLM 或 Mock LLM,不必真实调用外部模型服务。

这些能力放在一起,就构成了 AI 应用工程化里的模型调用底座。

三、不要把 LLM 抽象层设计成万能框架

设计 LLM 抽象层时,容易走向另一个极端:

一开始就想支持所有模型、所有参数、所有调用模式。

这会让抽象层变得很重。

对于脚手架项目来说,更好的做法是先抓住最核心的能力。

例如第一版只需要支持:

  • 普通对话调用。
  • 流式对话调用。
  • 基础参数传入。
  • 统一返回结果。
  • 基础错误处理。
  • 新 Provider 扩展能力。

暂时不需要一开始就把所有高级能力都塞进去。

例如批量推理、函数调用、多模态输入、结构化输出、缓存、路由和降级,都可以作为后续扩展。

抽象层的价值不是一开始覆盖所有场景。

而是先建立一个稳定边界:

上层业务不直接依赖模型厂商。

底层模型供应商可以替换和扩展。

只要这个边界建立起来,后续功能就可以逐步生长。

四、一个简单的 LLM 接口应该长什么样?

在 Python 项目里,可以先定义一个基础接口。

例如:

fromabcimportABC,abstractmethodfromtypingimportIterableclassBaseLLM(ABC):@abstractmethoddefchat(self,messages:list[dict])->"LLMResponse":pass@abstractmethoddefstream_chat(self,messages:list[dict])->Iterable["LLMChunk"]:pass

这个接口先表达两个核心能力:

普通调用。

流式调用。

普通调用一次性返回完整结果。

流式调用逐步返回模型生成的片段。

然后定义统一的返回对象。

fromdataclassesimportdataclass@dataclassclassLLMUsage:prompt_tokens:int|None=Nonecompletion_tokens:int|None=Nonetotal_tokens:int|None=None@dataclassclassLLMResponse:content:strmodel:str|None=Noneprovider:str|None=Noneusage:LLMUsage|None=Noneraw:object|None=None@dataclassclassLLMChunk:content:strdone:bool=Falseraw:object|None=None

这里有一个细节:

返回对象里保留了raw字段。

这是为了在需要排查问题时,还能拿到原始响应。

但业务代码默认不应该依赖raw

如果业务代码大量读取raw,说明抽象层没有真正隔离底层差异。

五、Provider 是 LLM 抽象层的核心

LLM 抽象层通常可以分成两部分:

统一接口。

具体 Provider。

统一接口定义项目内部怎么调用模型。

Provider 负责适配具体模型供应商。

例如项目结构可以这样设计:

llm/ ├── __init__.py ├── base.py ├── schema.py ├── factory.py └── providers/ ├── __init__.py ├── openai_provider.py ├── deepseek_provider.py └── qwen_provider.py

其中:

base.py放基础接口。

schema.py放统一返回结构。

factory.py根据配置创建具体 LLM 实例。

providers/放不同模型供应商的适配代码。

这样业务代码只需要这样写:

fromllm.factoryimportcreate_llm llm=create_llm()result=llm.chat([{"role":"user","content":"请总结这段内容"}])print(result.content)

业务代码不需要知道底层到底是 OpenAI、DeepSeek 还是 Qwen。

这就是 Provider 模式的价值。

六、Factory 应该和配置系统配合

上一篇文章提到,AI 应用需要统一配置系统。

LLM 抽象层应该直接使用这套配置。

例如配置对象里可以有这些字段:

classSettings:llm_provider:str="openai"llm_model:str="gpt-4o"llm_temperature:float=0.7llm_timeout:int=60openai_api_key:str|None=Nonedeepseek_api_key:str|None=Noneqwen_api_key:str|None=None

Factory 根据配置创建不同 Provider。

fromconfig.settingsimportsettingsfromllm.providers.openai_providerimportOpenAIProviderfromllm.providers.deepseek_providerimportDeepSeekProviderfromllm.providers.qwen_providerimportQwenProviderdefcreate_llm():ifsettings.llm_provider=="openai":returnOpenAIProvider(api_key=settings.openai_api_key,model=settings.llm_model,temperature=settings.llm_temperature,timeout=settings.llm_timeout,)ifsettings.llm_provider=="deepseek":returnDeepSeekProvider(api_key=settings.deepseek_api_key,model=settings.llm_model,temperature=settings.llm_temperature,timeout=settings.llm_timeout,)ifsettings.llm_provider=="qwen":returnQwenProvider(api_key=settings.qwen_api_key,model=settings.llm_model,temperature=settings.llm_temperature,timeout=settings.llm_timeout,)raiseValueError(f"Unsupported LLM provider:{settings.llm_provider}")

这样配置系统和 LLM 抽象层就串起来了。

如果想从 OpenAI 切换到 DeepSeek,理论上只需要修改配置:

LLM_PROVIDER=deepseek LLM_MODEL=deepseek-chat DEEPSEEK_API_KEY=your-api-key

上层业务代码不需要改。

这就是工程化设计带来的稳定性。

七、Provider 内部应该适配差异,而不是暴露差异

Provider 的职责是把具体厂商 SDK 转换成项目内部统一接口。

例如 OpenAIProvider 可以这样写:

fromopenaiimportOpenAIfromllm.baseimportBaseLLMfromllm.schemaimportLLMResponse,LLMUsageclassOpenAIProvider(BaseLLM):def__init__(self,api_key:str,model:str,temperature:float,timeout:int):self.client=OpenAI(api_key=api_key,timeout=timeout)self.model=model self.temperature=temperaturedefchat(self,messages:list[dict])->LLMResponse:response=self.client.chat.completions.create(model=self.model,messages=messages,temperature=self.temperature,)usage=Noneifresponse.usage:usage=LLMUsage(prompt_tokens=response.usage.prompt_tokens,completion_tokens=response.usage.completion_tokens,total_tokens=response.usage.total_tokens,)returnLLMResponse(content=response.choices[0].message.contentor"",model=self.model,provider="openai",usage=usage,raw=response,)

这里业务代码看不到 OpenAI SDK 的细节。

业务代码只拿到LLMResponse

如果以后切换到 DeepSeekProvider,只要 DeepSeekProvider 也返回LLMResponse,上层代码就不需要变化。

Provider 内部可以有差异。

Provider 对外应该尽量一致。

八、流式输出也应该统一

很多 AI 应用需要流式输出。

例如聊天机器人、写作助手、代码生成工具,都希望模型边生成边展示。

如果不做抽象,不同供应商的流式接口差异会直接进入业务层。

所以 LLM 抽象层需要提供统一的流式接口。

例如:

forchunkinllm.stream_chat(messages):print(chunk.content,end="")

OpenAIProvider 内部可以这样适配:

fromllm.schemaimportLLMChunkdefstream_chat(self,messages:list[dict]):stream=self.client.chat.completions.create(model=self.model,messages=messages,temperature=self.temperature,stream=True,)foreventinstream:content=event.choices[0].delta.contentor""ifcontent:yieldLLMChunk(content=content,raw=event)yieldLLMChunk(content="",done=True)

上层业务不关心底层事件结构。

只关心每次拿到一个LLMChunk

这样无论后续是 WebSocket、SSE 还是命令行输出,都可以基于统一的流式接口实现。

九、错误处理不能到处写 try except

模型调用经常会失败。

常见原因包括:

  • API Key 未配置。
  • API Key 无效。
  • 网络超时。
  • 请求频率过高。
  • 模型名称错误。
  • 上下文长度超限。
  • 供应商服务不可用。

如果每个业务模块都自己写try except,错误处理会非常分散。

LLM 抽象层可以定义统一异常。

例如:

classLLMError(Exception):passclassLLMAuthError(LLMError):passclassLLMTimeoutError(LLMError):passclassLLMRateLimitError(LLMError):passclassLLMProviderError(LLMError):pass

Provider 内部负责把具体 SDK 异常转换成这些统一异常。

业务层只需要处理项目内部异常。

例如:

try:result=llm.chat(messages)exceptLLMTimeoutError:return"模型响应超时,请稍后再试"exceptLLMRateLimitError:return"当前请求较多,请稍后重试"exceptLLMError:return"模型服务暂时不可用"

这样错误处理会更清晰。

同时也方便后续接入日志、告警和降级策略。

十、超时、重试和降级应该放在哪里?

LLM 调用是外部服务调用。

外部服务调用就一定要考虑超时。

如果没有超时控制,一个请求可能卡住很久。

如果没有重试控制,短暂网络波动就会导致用户请求失败。

如果没有降级策略,主模型不可用时整个应用就不可用。

但这些逻辑不应该散落在业务代码里。

更合理的位置,是放在 LLM 抽象层或更上层的模型路由层。

例如第一版可以先在 Provider 初始化时支持 timeout:

OpenAI(api_key=api_key,timeout=timeout)

后续再加入简单重试:

defchat_with_retry(llm,messages,retries:int=2):last_error=Nonefor_inrange(retries+1):try:returnllm.chat(messages)exceptLLMTimeoutErroraserror:last_error=errorraiselast_error

再往后,可以设计模型路由:

primary_llm=create_llm(provider="openai")fallback_llm=create_llm(provider="deepseek")try:result=primary_llm.chat(messages)exceptLLMError:result=fallback_llm.chat(messages)

不过对于脚手架早期版本来说,不一定要马上实现复杂路由。

先把接口边界设计好,后面扩展会更自然。

十一、Prompt 和 LLM 抽象层要保持边界

有些项目会把 Prompt 拼接逻辑写进 LLM Provider。

这通常不是一个好习惯。

LLM Provider 应该负责模型调用。

Prompt 管理应该属于 Prompt 层或业务层。

例如不要这样设计:

llm.summarize_article(article_text)llm.generate_title(topic)llm.extract_keywords(content)

这些方法看起来方便,但它们把业务语义塞进了 LLM 层。

更合理的方式是:

messages=prompt_manager.render(name="summarize_article",variables={"article_text":article_text},)result=llm.chat(messages)

LLM 层只关心 messages。

Prompt 层负责生成 messages。

业务层负责决定使用哪个 Prompt。

这样模块边界会更清楚。

后续如果要做 Prompt 版本管理,也不会影响 LLM Provider。

十二、测试时需要 Fake LLM

AI 应用测试有一个很现实的问题:

不能每次测试都真实调用模型。

原因很简单。

真实调用会产生费用。

真实调用速度慢。

真实调用结果不稳定。

真实调用还依赖网络和外部服务状态。

所以 LLM 抽象层应该方便替换成 Fake LLM。

例如:

fromllm.baseimportBaseLLMfromllm.schemaimportLLMResponse,LLMChunkclassFakeLLM(BaseLLM):def__init__(self,content:str="fake response"):self.content=contentdefchat(self,messages:list[dict])->LLMResponse:returnLLMResponse(content=self.content,model="fake-model",provider="fake",)defstream_chat(self,messages:list[dict]):yieldLLMChunk(content=self.content)yieldLLMChunk(content="",done=True)

测试业务逻辑时,可以注入 FakeLLM。

deftest_summary_workflow():llm=FakeLLM(content="这是一段摘要")workflow=SummaryWorkflow(llm=llm)result=workflow.run("很长的文章内容")assertresult.summary=="这是一段摘要"

这样测试就不会依赖真实模型服务。

这也是抽象层非常重要的价值。

它不仅让生产环境可切换,也让测试环境可控制。

十三、LLM 抽象层在 AI Scaffold 里的位置

在 AI Scaffold 里,LLM 抽象层可以作为核心模块之一。

项目结构可以这样安排:

app/ ├── main.py ├── workflows/ │ └── summary_workflow.py ├── prompts/ │ └── summary_prompt.md ├── llm/ │ ├── __init__.py │ ├── base.py │ ├── schema.py │ ├── factory.py │ ├── exceptions.py │ └── providers/ │ ├── openai_provider.py │ ├── deepseek_provider.py │ └── qwen_provider.py ├── config/ │ └── settings.py └── tests/ └── test_summary_workflow.py

CLI 创建项目时,可以默认生成:

llm/base.py llm/schema.py llm/factory.py llm/exceptions.py llm/providers/

同时在.env.example里给出模型相关配置:

LLM_PROVIDER=openai LLM_MODEL=gpt-4o LLM_TEMPERATURE=0.7 LLM_TIMEOUT=60 OPENAI_API_KEY= DEEPSEEK_API_KEY= QWEN_API_KEY=

这样开发者生成项目后,就能很清楚地知道:

模型配置在哪里。

模型调用在哪里。

业务代码应该依赖什么接口。

新增模型供应商应该改哪个位置。

这比把所有模型调用写在业务函数里更适合长期维护。

十四、总结

LLM 抽象层是 AI 应用工程化里非常关键的一层。

它解决的不是“怎么调用某一个模型 API”。

它解决的是“如何让整个项目不被某一个模型厂商绑定”。

一个合理的 LLM 抽象层,至少应该做到:

  • 业务代码不直接依赖具体模型 SDK。
  • 使用统一的chatstream_chat接口。
  • 使用统一的返回结构。
  • Provider 内部适配不同厂商差异。
  • Factory 根据配置系统创建具体模型实例。
  • 错误处理、超时、重试可以逐步集中管理。
  • 测试环境可以使用 Fake LLM。
  • 后续新增模型供应商时,不影响上层业务代码。

对于 AI Scaffold 来说,配置系统决定使用哪个模型。

LLM 抽象层决定如何调用这个模型。

当这两层建立起来以后,项目就不再是简单的 Prompt Demo,而是开始具备真正的工程化基础。

下一篇文章可以继续讨论 Prompt 管理:

为什么 Prompt 不应该散落在业务代码里,以及如何把 Prompt 做成可维护、可复用、可版本化的项目资产。

http://www.jsqmd.com/news/1168571/

相关文章:

  • 5分钟玩转FanControl:你的Windows电脑风扇智能管家终极指南
  • 纽扣电池增强器NBM5100A与PIC32MZ的低功耗设计实践
  • Windows 11终极优化指南:如何用WinUtil一键搞定系统设置与软件安装
  • 凌晨还在排队的火锅店怎么选?网红庭院火锅推荐避坑指南 - 品牌2026推荐
  • PPS种群预测:中心点与流形解析
  • 2026年温岭手机维修哪家强?这家公司上榜top7实践经验分享 - GrowUME
  • 如何快速上手Nemotron-Labs-Audex-2B?5分钟完成环境搭建与首次推理
  • UE5内存泄漏排查实战:Memreport与Unreal Insights精准定位性能顽疾
  • AI论文写作工具哪家好用?热门平台深度测评与对比
  • CANN混合矩阵乘优化
  • 2026河源黄金奢侈品回收避坑指南:全城免费上门,这4家正规实体店实测推荐(主推源奢汇) - 生活测评小能手
  • PUBG罗技鼠标宏:从弹道控制到竞技进阶的全方位指南
  • 2026年7月最新北京真力时官方售后客服中心地址电话及服务网点分布 - 亨得利官方服务中心
  • MCP3551 22位Δ-Σ ADC与PIC18微控制器的精密测量应用
  • 2026阜阳装修市场深度测评:十大靠谱家装品牌甄选及装修消费指南 - 装修新知
  • MiniMax-M2.7-MXFP4模型优化指南:从配置文件到推理引擎的全方位调优
  • SolarWinds NTA 12.0 企业网络异常检测:基于NetFlow的3类威胁识别实战
  • 2026 年 7 月卡地亚维保选店攻略:无锡门店维修好不好?师傅技术与设备专业度深度测评 - 卡地亚中国售后中心
  • RAG检索增强生成技术:从原理到生产级实现
  • palera1n终极指南:基于checkm8漏洞的iOS越狱技术深度解析
  • 2026珠三角东莞网站建设外贸独立站开发服务商实力测评报告
  • 2026广东随身wifi行业评测报告:选购指南与关键指标权重分析 - GrowUME
  • 熊猫烧香病毒 3 大自启动机制剖析:注册表、文件隐藏与进程守护
  • Nemotron-Labs-Audex-2B音频编码器原理:从NV-Whisper到特征投影全解析
  • 如何将QRCode集成到你的iOS项目:Carthage、Cocoapods和手动安装指南
  • MySQL安装教程:图文教程从零开始,手把手教你搭建稳定高效的数据库环境
  • 【武汉学费便宜的民办高中推荐】2026级武汉思久高级中学招生简章|录取分数线|班型|收费标准 - 湖北升学规划
  • 一个坏毛病——粗枝大叶!
  • MA12070音频放大器与STM32F302VC的集成设计解析
  • 2026 年现阶段,雨湖值得关注的瓷板画激光雕刻机制造商哪家可靠,别再用传统方式!这台设备如何颠覆你的瓷板设计? - 行业鉴选官