模型适配与迁移
模型适配与迁移 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。
学习目标
读完本课你能:解释「统一业务接口」如何把提供方差异挡在一个文件里;切换模型提供方时,按消息、工具、流式、参数四张核对单逐项确认差异;用连接测试和固定样例回归证明迁移没有破坏功能。
问题场景
你用的模型服务宣布涨价,换一家的念头出现了。第一反应是「把代码里的请求全部改一遍」——这意味着聊天、工具调用、流式解析到处都要动。而本站的做法是:业务代码根本不知道提供方是谁,切换只改三个环境变量。这个差距就是适配层设计的价值,也是本课要讲清的东西。
核心解释
本站的隔离层就是 lib/ai/adapter.ts:对外暴露 streamChatCompletion、ChatMessage、AiError 这套自有接口,对内才写 OpenAI 兼容的协议细节。业务代码(如 app/api/ai/chat/route.ts)只 import 这套接口,从不碰具体提供方。切换提供方 = 改 AI_BASE_URL / AI_API_KEY / AI_MODEL 三个环境变量(lib/ai/config.ts 读取),代码零改动——前提是新提供方兼容 OpenAI chat/completions 协议,且你核对过下面四张单子:
消息层:role 是否都认(system/user/assistant/tool);工具结果回传要不要 tool_call_id;数组型 content(图文混合,A37)是否支持。
工具层:tool_calls 的流式增量怎么拼——本站按 index 累积 id/name/arguments 分片(toolAccum);finish_reason 的取值是 "tool_calls" 还是别的词。
流式层:SSE 行格式与 [DONE] 结尾是否一致;多字节中文被分包切断时 TextDecoder 流式解码是否照常(这是协议通用行为,但有的网关会改分帧)。
参数层:max_tokens 还是 max_completion_tokens;temperature 范围;stream_options.include_usage 支持与否——不支持就拿不到用量,A34 的账本直接变空。
错误处理为什么稳?因为 classifyHttpError 按状态码分类,状态码语义是各家公认约定,比解析错误文案可靠得多:
// lib/ai/adapter.ts(节选):按状态码分类,不猜文案
export function classifyHttpError(status: number, bodyText: string): AiError {
if (status === 401 || status === 403)
return new AiError("not_configured", "密钥无效或无权限,请检查 AI 配置。", status);
if (status === 429)
return new AiError("rate_limit", "请求频率或额度受限,稍后再试。", status);
if (status === 402)
return new AiError("quota", "账户余额或额度不足。", status);
return new AiError(
"model_error",
`模型服务返回 ${status}:${bodyText.slice(0, 200)}`,
status
);
}
示例拆解
一次完整迁移的执行顺序(每步有验证):
# 1. 备份当前配置,改三个环境变量(.env.local)
# AI_BASE_URL=https://新提供方地址 AI_API_KEY=sk-xxx AI_MODEL=新模型名
# 2. 连接测试:最便宜的真相
curl -s -X POST localhost:3000/api/ai/status
# 期待 {"ok":true,...,"latencyMs":...};失败会带 category
# 3. 核对四张单子:重点看流式工具调用与 usage 是否照常落 ai_call 表
# 4. 回归:A31 固定样例对比新旧模型输出 + 核心路径
node scripts/e2e-core.mjs
关键点:连接测试先行——POST /api/ai/status 用一条最小请求验证「地址、密钥、模型名」三件套,比直接在聊天里试错便宜;用量表是迁移探针——ai_call 有没有新记录、prompt_tokens 是否为 null,立刻暴露 include_usage 的支持差异;回归对齐 A33——固定样例的通过率变化就是迁移的质量报告。
动手任务
- 跑实验区:两家「提供方」的流式报文形状不同,归一化之后输出完全一致——体会「差异挡在适配层」。
- 本机把
AI_MODEL改成一个不存在的名字,重启后请求/api/ai/status,确认错误类别是model_error且提示可读,改回来。 - 写下你的核对单:对照本课四张单子,逐项标注「已核对 / 本站未用到」。
常见错误
- 业务代码直接拼上游 URL:绕过适配层,迁移时每处都要找出来改,漏一处就是一个隐性 bug。
- 只测文本对话就切完:工具调用和流式是差异重灾区,不回归这两样等于没测(A15-A17 的能力会静默坏掉)。
- 忽略 usage 缺失:新提供方不支持
include_usage,账本悄悄变空,几周后才发现没数据可复盘。 - 按错误文案写判断:文案各家不同还随时改,按状态码分类才稳定。
检查与复盘
- 能画出「业务代码 → adapter → 上游」的分层,说出切换提供方要改什么、不用改什么
- 四张核对单(消息/工具/流式/参数)各能说出至少一个真实差异点
- 做过一次(哪怕是模拟的)迁移:连接测试与回归检查都有结果记录
参考资料
- OpenAI 文档:Chat Completions API —— 兼容协议的基准定义
- OpenAI 文档:Function calling —— 工具调用与增量拼装的标准行为
- 智谱开放平台文档 —— GLM 系列模型的参数与兼容性说明
读完这一课了?
标记完成后,工作台会安排接下来的学习。