你有没有遇到过这种情况:升级一个SDK,以为只是改个版本号,结果要翻遍15个文件,修复四家供应商的破坏性变更,然后提心吊胆地祈祷没漏掉什么。这是第二次了,我知道还会有第三次。
如果你做过生产环境的LLM系统,应该能闻到这股味道:SDK小版本把maxTokens改成maxOutputTokens,15个文件在运行时崩溃,编译期却毫无察觉;想把一个分类任务从Claude换成更便宜的模型,得在业务逻辑里改import路径和类型签名;你写了classifyEmail、scoreLead、triageTicket、categorizeRequest,本质上是同一个函数换了个提示词。
![]()
这不是SDK的问题,是架构问题。我定了一条规则,最后开源了一个库。
![]()
两条文件规则
整个代码库里,只有两份文件允许引入LLM SDK。一份适配器,把我的接口翻译成SDK调用;一份供应商注册表,根据配置创建客户端。其他所有代码只跟一个带类型的接口打交道,完全不知道背后用的是哪家供应商、哪个模型、哪个SDK。
这就是六边形架构(端口与适配器模式)在LLM上的应用。你对数据库和消息队列早就是这么做的——没人会把裸SQL撒得满业务逻辑都是。LLM供应商属于同一类别:它们是基础设施,不是应用逻辑。
依赖流向从这样:
应用代码
├─ 直接SDK调用
├─ 直接SDK调用
└─ 模型路由器泄漏SDK类型
变成这样:
应用代码
↓ llmClassify(), llmDraft(), llmScore() ...
能力层
↓
LLM端口(TypeScript接口,零SDK引入)
↓
适配器 + 供应商注册表(唯二接触SDK的文件)
↓
OpenAI / Anthropic / Gemini / Ollama / Vercel AI SDK
调用方声明要什么(taskType: "triage"),基础设施决定怎么做。没有模型名参数,没有供应商参数,策略推迟到配置里。
实证:一次无痛的SDK升级
真正的考验来了——一次包含破坏性变更的大版本跳跃(maxTokens改maxOutputTokens,CoreMessage改ModelMessage等等)。迁移提交长这样:
• 2个文件变更(适配器和运行时),外加1处小修复
• 全部18个活动文件未动
• 全部10个智能体文件未动
• 最终迁移删除的代码比新增的多:192行插入,688行删除
31个文件里28个没变,因为它们根本不知道SDK存在。如果核心依赖升级会触及你的业务逻辑,说明边界画错了。
意外发现:到处都在重复同样的7种操作
我最初只是为了隔离SDK。然后发现了更大的问题:我不是在21个不同的地方调用LLM,而是在用细微差别重复实现同样的七种认知操作。
五个活动用五种不同的提示结构做内容分类。九个智能体用九种不同的重试逻辑做草稿生成。到处都是重复造轮子——不同的温度参数、不同的超时设置、不同的错误处理,全都是为了本质上相同的任务。
这七种操作覆盖了生产LLM系统的绝大部分场景:
1. 分类(Classify):把非结构化内容标上预定义标签
2. 提取(Extract):从文本中拉取结构化数据
3. 草稿(Draft):生成内容,人类会编辑或批准
4. 评分(Score):返回数值评估(0-100,1-10等)
5. 选择(Select):从选项列表中挑一个
6. 结构化(Structure):把非结构化输入变成特定格式
7. 工具(Tool):调用函数或API
每个操作都有明确定义的接口、标准化的错误处理、可观测性钩子,以及基于任务类型的默认配置。业务代码说"把这个邮件分类为垃圾/正常/优先",基础设施决定用哪个模型、多少token、什么温度。
开源库:LLM-Toolkit
这套模式稳定运行几个月后,我把它整理成了开源库。核心就两个概念:
LLMPort:定义七种操作的TypeScript接口。零SDK依赖,纯类型定义。
createLLMClient(config):根据配置返回LLMPort实现。支持OpenAI、Anthropic、Gemini、Ollama、Vercel AI SDK,通过统一接口。
配置驱动一切。不硬编码模型名,不在业务逻辑里传供应商参数。一个环境变量切换整个系统的供应商,或者按任务类型路由(分类用便宜模型,草稿用强模型)。
错误处理也标准化了。重试、退避、降级、超时,全在适配器层解决。业务代码拿到的是Result,不用处理供应商特定的异常格式。
实际迁移案例
一个中等规模的代码库,之前LLM调用散落在23个文件里。迁移过程:
![]()
第一周:识别所有调用点,归类到七种操作。发现4个分类实现、3个草稿实现、2个评分实现,全是重复代码。
第二周:用适配器替换直接SDK调用,逐个文件迁移。每迁移一个,删除对应的重复工具函数。
第三周:统一配置,按任务类型设置默认模型和参数。删除硬编码的模型名和温度值。
结果:23个文件变成2个基础设施文件 + 21个只关心业务逻辑的干净文件。后续添加新供应商(比如从OpenAI切到Azure OpenAI)只需要改配置,零代码变更。
边界情况的处理
这套模式不是银弹。有些场景需要特别处理:
流式响应:七种操作都支持流式变体(draftStream、classifyStream等),返回异步生成器。适配器负责把供应商特定的流格式转换成统一接口。
多模态输入:分类和提取操作接受ContentPart[],可以是文本或图像。适配器处理不同供应商的图像编码差异(base64前缀、URL格式等)。
工具调用循环:Tool操作支持自动循环——模型请求工具调用,执行,把结果喂回去,直到模型给出最终答案。循环次数和超时在配置里控制,业务代码无感知。
供应商特定功能:偶尔需要用到某家独有的功能(比如Anthropic的PDF支持)。这时候在LLMPort上加可选扩展接口,或者直接用原生SDK——但限制在那两条文件规则之内。
为什么不是现有方案
市面上有不少LLM抽象库,但大多没解决核心问题:
统一API客户端(如Vercel AI SDK):确实统一了调用方式,但还是在业务代码里直接import。升级SDK照样要改到处。
模型路由器(如LiteLLM):在基础设施层做路由,但通常要求你按它的接口重构,迁移成本高。
框架内置抽象(如LangChain):往往带来另一层复杂度,而且锁死在特定生态。
两条文件规则的关键是渐进式迁移。不用重写系统,逐个文件替换即可。适配器可以慢慢完善,业务代码可以慢慢迁移,风险可控。
配置示例
实际使用长这样:
```typescript
// 配置层:环境变量或配置文件
const config = {
defaultProvider: 'openai',
taskRouting: {
classify: { provider: 'anthropic', model: 'claude-3-haiku' },
draft: { provider: 'openai', model: 'gpt-4' },
score: { provider: 'openai', model: 'gpt-3.5-turbo' }
},
fallback: { provider: 'gemini', model: 'gemini-pro' }
};
// 应用层:完全不知道供应商
const result = await llm.classify({
content: emailBody,
categories: ['spam', 'normal', 'priority']
});
// result: { category: 'priority', confidence: 0.94 }
```
想切到Azure OpenAI?改配置里的defaultProvider。想给分类任务换更便宜的模型?改taskRouting.classify。应用代码一行不动。
可观测性
所有七种操作都内置标准化埋点:
• 延迟(首token时间、总时间)
• Token用量(输入/输出,按供应商计费单位)
• 任务类型和模型路由决策
• 错误类型和重试次数
• 成本估算(基于供应商公开定价)
这些在适配器层统一实现,业务代码自动获得。不用在每个调用点手动加日志。
最后的检查清单
如果你也想实施类似方案,核心问题就这几个:
1. 你的代码库里有多少文件直接import了LLM SDK?
2. 同样的认知操作(分类、提取、草稿等)有多少种实现?
3. 切换供应商或模型需要改多少文件?
4. SDK升级时,你的业务逻辑会不会崩溃?
如果答案让你不舒服,可能是时候画条边界了。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.