做 Agent 开发的人迟早会面对一个问题:今天用 GPT,明天想试试 Claude,后天又要接入国产模型。每次都改一堆调用代码,改到怀疑人生。
有没有一种办法,换模型只改一个配置文件,代码零动?
这篇文章拆解的就是这套方案的核心 -- Provider 模块。它解决的就一件事:用一套统一的调用流程,适配所有大模型提供商的接口差异。
主要聊三个问题:
1)新模型怎么注册进来 -- 开闭原则下的双配置机制
2)接口差异怎么抹平 -- 策略模式 + 统一响应结构
3)流式、重试这些能力怎么组合 -- 套壳式解耦设
换模型不该动代码,这个道理大家都认同。关键是哪些东西该放在代码里,哪些该交给运行时配置。
这里的设计是两层:
第一层:ProviderSpec -- 静态注册表
代码里维护一个字典(或者叫元组),每种大模型提供商对应一条 ProviderSpec 记录。里面存的是什么?是该模型"天生就带"的那些参数 -- 比如模型名称关键字、是否支持深度搜索、是否支持联网、默认的超时设置等等。这些东西跟运维参数无关,是模型本身的特性,所以跟着代码版本走,更新频率很低。
第二层:ProviderConf -- 运行时配置
用户在 JSON 文件里填的东西就实在多了:API 密钥、访问地址、要用的具体模型名。这些是经常变的,可能今天换一个 key,下周换个 endpoint,所以单独抽出来,启动时动态加载。
那这两套配置怎么串起来?靠一个字段:模型名称。
创建实例的时候,流程是这样的:先用 JSON 里用户指定的模型名,去静态注册表里查出对应的 ProviderSpec,再把 Spec 和 Conf 一并传给 Provider 类的构造函数,返回一个可用的实例。名字就是桥梁。
为什么非要分成两套?让用户一次性配所有参数不行吗?可以,但体验很差。想想看,普通用户哪里知道什么深度搜索开关、推理参数默认值这种东西。把"用户关心的"和"开发者在意的"分开,两边都清爽。
不同模型的 API 调用方式天差地别:请求格式不一样、返回结构不一样、错误码体系也不一样。如果不做抽象,每个模型写一套调用逻辑,后面加新模型就是灾难。
解决方案很直接:策略模式。
定义一个 LLM Provider 作为抽象基类,规定好统一的接口契约。核心方法有两个:chat 和 chat_stream。子类必须实现这两个方法 -- 这是真正去调模型 API 的地方。
一个典型的子类实现长这样:先把通用参数拼成目标 API 需要的格式,发起 HTTP 请求,然后把厂商自己的返回结构解析成统一的 LMResponse 对象。
这个 LMResponse 是关键。它定义了一套标准字段集,不管底层用的是哪家模型,上层拿到的都是同一个数据结构。后续的重试判断、内容脱敏、回调处理,全部基于这套标准字段来做,不用关心底层是谁。
有个细节值得说:基类里的 chat_stream 方法其实是一个"假流式"。它的作用是兜底 -- 当子类的真流式抛出异常时,降级为等完整结果一次性返回。生产环境里有个 fallback 总归是好的。
实际使用中,调用的需求组合有好几种:要流式的、不要流式的、要重试的、不要重试的。四种排列组合,如果每个都写一遍,代码量直接翻倍。
观察一下接口命名就能看出门道:chat、chat_stream、chat_with_retry、chat_stream_with_retry。四个方法,两种维度交叉。
流式与非流式的差异在于数据回写方式:流式走 SSE(Server-Sent Events),边生成边推;非流式等结果完整了再返回。这个由 chat_stream vs chat 分别承担。
重试与非重试的差异则被封装成了一个独立的"壳子"。
chat_with_retry 的实现思路很简单:接收一个 callable(可以是 chat 也可以是 chat_stream),在一个循环里反复调用它,拿到 LMResponse 之后根据错误码决定继续重试还是直接返回。重试逻辑本身不关心你传进来的是流式还是非流式 -- 它只管"要不要再来一次"。
这就是套壳设计的妙处:重试逻辑和实际调用逻辑完全解耦。你想给哪个方法加重试能力,把它包进 with_retry 就行。
重试不是简单的 while 循环。一个靠谱的重试机制需要处理几个问题:
什么时候该重试?依据 LMResponse 里的错误码来判断 -- 网络超时、限流、服务不可用这类" transient 错误"才值得重试,参数错误、鉴权失败这类"永久错误"重试一百遍也没用。
重试几次?间隔多久?这些策略在重试引擎内部配置,可以根据不同错误类型设置不同的退避策略。
整个链路跑下来就是这样:用户选模型名 -> 匹配 Spec + 加载 Conf -> 创建 Provider 实例 -> 按需选择流式/重试组合 -> 统一收到 LMResponse -> 后续处理。这样一套下来,需要换模型,改 JSON 里的名字和密钥,完事。
Provider 模块的本质其实就是一个适配器层。上层的 Agent 逻辑不需要知道底层用的是 GPT 还是通义千问,它只认 LMResponse 这一套标准返回;下层的各家 API 差异被封装在各自的子类里,互不干扰。加上开闭原则的注册机制和可组合的能力套壳,整条链路的扩展性和稳定性都有了基本保证。
架构设计里没有银弹,但这种"分层隔离 + 策略替换 + 能力组合"的套路,在做 SDK 封装和多后端适配的场景下,确实经得起考验。