接入6家大模型API后的适配器设计模式总结
我总结的6家大模型API接入实战经验与踩坑实录
前不久接了一个企业内部智能助手的项目,客户是一家拥有数千名员工的传统制造企业。他们希望把内部知识库和几个主流的大模型能力打通,做一个能问业务数据、也能写代码辅助的聊天机器人。说实话,一开始我觉得这很简单,不就是调个API吗?结果真正落地时发现,各家厂商的鉴权方式、参数结构、流式响应格式简直是一团乱麻。最后我们接入了6家大模型厂商,为了统一对外接口,我花了一周时间重构了适配层。这个过程坑不少,但也让我对设计模式在工程化中的应用有了全新的理解。
统一抽象与适配器模式的抉择
在项目初期,产品需求很明确:前端只需要一个chat接口,传进去用户的问题,返回答案。但后端对接的时候,噩梦开始了。A厂商用API Key放在Header里,B厂商要求Bearer Token;C厂商的参数叫prompt,D厂商叫input,E厂商甚至还要传一个复杂的JSON结构定义流式字段。如果我在Controller层直接写6个if-else判断,那代码量至少膨胀三倍,而且以后每加一家模型都要改核心逻辑,维护成本太高。
当时我有两个方案。方案A是简单粗暴地用策略模式+工厂模式,每个模型实现一个独立的Service类。方案B是引入适配器模式,在所有底层API之上再包一层统一的ModelAdapter接口,屏蔽具体厂商差异。
试了一圈发现,方案A虽然直观,但每个Service里都重复处理了鉴权重试、错误码转换这些通用逻辑。而我选方案B,是因为我想把“如何调用”和“怎么调用”彻底分离。于是,我定义了一个核心接口LargeLanguageModelClient,里面只保留三个方法:authenticate()、generateResponse()和streamGenerate()。
```java
public interface LargeLanguageModelClient {
// 统一认证接口
void authenticate() throws AuthException;
// 同步生成
String generateRequest(GenAIRequest request) throws GenAIOperationException;
// 流式生成
Flux streamGenerateRequest(GenAIRequest request);
}
```
有意思的是,对于每家厂商,我并不是直接实现这个接口,而是先写一个AbstractModelClient基类。基类里封装了通用的HTTP客户端初始化、超时设置和基础的日志记录。这样,具体到某家厂商(比如通义千问或文心一言)的实现类,只需要关注参数映射和响应解析。
说实话,刚开始我觉得这样分层有点过度设计,毕竟只有6家模型。但当我遇到第三个厂商的签名算法完全不一致时,我庆幸自己做了抽象。不然每次新增模型,我都要去复制粘贴那些繁琐的HTTP Header构建代码。
流式响应与异常处理的深坑
接入过程中,最让我头疼的不是参数对齐,而是流式响应(Streaming)的处理。大部分大模型都支持SSE(Server-Sent Events),但各家返回的数据格式千差万别。有的直接在body里返回data: {"content": "hello"},有的要把整个JSON包在event:标签里,还有的甚至会在非数据帧之间插入空行或者特殊字符。
我当时觉得这样就行,直接把底层流读出来转发给前端WebSocket。结果测试的时候发现,偶尔会出现数据截断或者乱码。排查了半天,才发现是底层的HttpComponents客户端在处理chunked transfer编码时,缓冲区的默认大小太小,导致长文本被切分后重新组装时丢失了边界信息。
解决这个问题的过程挺折腾。我首先换用了WebClient,它的反应式流处理更优雅。但即使换了客户端,解析逻辑还是得写。我设计了一个StreamParser接口,针对每家厂商写不同的解析器。
```java
public interface StreamParser {
boolean isDataFrame(String rawLine);
String extractContent(String rawLine);
boolean isEndOfStream(String rawLine);
}
```
这里有个大坑。有些厂商在流结束时,不会发送明确的[DONE]标记,而是直接关闭连接。而有些厂商会在中间发送心跳包。如果不仔细处理,前端要么一直转圈等待结束,要么收到一堆空数据。
我最后的解决方案是,在适配器层增加一个StreamBuffer。它不直接转发给前端,而是先缓存数据,检测是否是一个完整的语义单元(比如以句号或换行结尾),然后再推送。这样做虽然增加了一点延迟(大概200ms),但用户体验好太多了。
另一个踩坑点是错误处理。各家模型的错误码完全不通约。有的返回HTTP 400,报错信息在JSON的error.message字段;有的直接返回HTTP 500,但实际是限流。我原本想统一捕获所有异常,然后抛出自定义的BusinessException。结果有一次线上故障,因为某家厂商返回的错误信息包含中文乱码,导致我的JSON序列化器崩溃,整个服务挂了。
后来我学乖了,在适配器层增加了严格的异常隔离。不管底层抛什么怪异的异常,上层统一转换为标准的GenAIOperationException,并且只记录脱敏后的错误码,绝不把原始响应体直接透传给前端。这让我意识到,防御性编程不是废话,是保命符。
性能权衡与最终总结
接入6家模型后,我还做了一个性能压测。发现适配器层的引入确实带来了一定的开销,主要是对象序列化和反序列化的次数增加了。为了优化这点,我没有每次都新建请求对象,而是引入了Builder模式来复用配置。
此外,我还加了熔断机制。当某家模型连续失败超过阈值时,自动切换到备用模型。这个功能虽然是后来加的,但得益于最初的适配器架构,扩展起来非常轻松。只需要新增一个BackupModelClient实现,并注册到熔断管理器中即可,完全不需要改动业务逻辑代码。
回顾这个项目,从最初的手忙脚乱到后来的从容应对,最大的收获就是理解了“稳定”二字的重量。API对接看似只是简单的CRUD操作,实则是对稳定性、兼容性和可维护性的极致考验。适配器模式在这里不仅仅是一个设计模式,更是一种工程治理手段。它让我们在面对多变的外部依赖时,能够保持内部核心的纯净与稳定。
本文基于实际项目经验整理,欢迎在评论区交流技术问题。
