Rust 终端 AI 编码助手的多模型 Provider 抽象实践
这是一篇示例方案笔记,用于演示 /tech/ 栏目写作格式,正式发布前可整篇替换。
做终端 AI 编码助手绕不开一个问题:模型服务商是会换的。价格会变、限流会收紧、某家的代码能力突然退步——如果把对话链路写死在某一家 SDK 上,每次更换都是一场手术。QIDI Code 的做法是在内核层收敛出一个足够薄的 Provider 抽象。
抽象层的边界在哪里
我们只抽象三件事:
- 请求形态:系统提示 + 消息历史 + 工具定义,归一成一个内部结构;
- 事件流:各家 SSE/WebSocket 的增量差异(思考块、工具调用分片、用量统计)统一成一组内部事件;
- 能力声明:是否支持并行工具调用、上下文长度、是否返回 token 用量。
一切厂商私有参数(temperature 取值范围、stop 序列格式)留在各自的适配器里,不向上 leaks。抽象层一旦贪多,就会退化成"最小公分母 + 一堆 if"。
流式协议差异的抹平
最典型的坑是工具调用分片:有的服务每帧返回完整 JSON,有的返回参数名的增量拼接。内部事件模型里我们只用一类事件:
enum StreamEvent {
TextDelta(String),
ToolCallBegin { id: String, name: String },
ToolCallArgDelta { id: String, delta: String },
Usage { input: u64, output: u64 },
Done(FinishReason),
}
适配器负责把厂商格式翻译成这个序列,上层组装器完全不感知来源。这样新增一家服务只需要写翻译,不需要动任何业务逻辑。
故障降级
- 请求失败区分可重试(超时、5xx)与不可重试(上下文超长、内容审核);
- 上下文超长不直接报错,而是触发历史压缩后重试一次;
- 会话持久化让中断的对话可以在切换模型后继续——消息历史里保留每条的产生时间与来源模型,便于复盘。
小结
Provider 抽象的价值不在"支持多少家",而在让"换一家"的成本从改内核降到改适配器。接口越薄,适配越稳。